Skip to content

Instantly share code, notes, and snippets.

@fourgates
Last active August 26, 2020 22:10
Show Gist options
  • Save fourgates/e23f30ca2da5fb325facbae27b6a8359 to your computer and use it in GitHub Desktop.
Save fourgates/e23f30ca2da5fb325facbae27b6a8359 to your computer and use it in GitHub Desktop.
AngularJS Tooltip
/**
Tooltip Directive
This directive will add an information icon to the page. By default the tooltip is triggered by hover events.
You can change this to an onClick by simply adding an on-click attribute. The tooltip defaults to the left of the info icon
but will change based on its position on the screen.
There are two ways to use this directive:
1. Transclude the tooltip content, this is the easiest and most strait forward implementation
2. You can provide a url for a template and data to be rendered in your template.
a. Template - e.g. "someplace/toolip.html"
b. Data - toolTipData must contain toolTipData.customCell.getData(). this allows for dynamic rendering of tooltips
this function must return a promise
this function should return the scope for whatever is being rendered in the given template.
if getData takes a parameter it should be bound to toolTipData.data
-e.g. if your template has an ng-repeat over a list of players for a given team, getData() should return
a list of players that will be rendered. the teamId should be bound to toolTipData.data and will be passed
to getData(toolTipData.data)
Styles
There are three classes that can be used for styling:
1. tooltip-content-left (default)
2. tooltip-content-top
3. tooltip-content-right
**/
function Tooltip($sce, $q, $window, $timeout, $document){
'ngInject';
return{
scope:{
toolTipData: '<',
template:'@',
iconClass: '@' //info icon class
},
transclude: true,
templateUrl: 'common/tooltip/c-tool-tip.html',
link: function(scope, element, attrs){
if(scope.template) secureTemplateUrl();
scope.hidden = true;
scope.dataLoaded = false;
// events
if(attrs.onClick || scope.toolTipData.customCell.onClick){
element.on('click', toggleHide);
}
else{
element.on('mouseenter',show);
element.on('mouseleave', hide);
}
function toggleHide(e){
//want to prevent any unwanted click listeners from getting called (like row click)
e.stopPropagation();
if(scope.toolTipData.customCell.onClick && !scope.hidden){
//tooltip is being closed so remove the click listener on window object
angular.element($window).off('click', processClick);
}
//call any listeners on the window object explicitly since we stopped propagation on the event
//to prevent unwanted actions like row-clicks
$window.dispatchEvent(new Event('click'));
scope.parentEl = e.target;
if(scope.toolTipData.customCell.onClick && scope.hidden){
angular.element($window).on('click', processClick);
}
//cleanup reposition listeners
if(scope.toolTipData.customCell.onClick
&& scope.toolTipData.customCell.fixedPosition
&& !scope.hidden){
angular.element($window).off('resize scroll', scope.reposition);
if(scope.toolTipData.customCell.containerId){
scope.container.off('scroll',scope.reposition);
}
}
scope.$apply('hidden = !hidden')
toggleClassOnTd();
loadData();
}
function show(e){
scope.$apply('hidden = false');
toggleClassOnTd();
loadData();
}
function hide(){
scope.$apply('hidden = true');
toggleClassOnTd();
}
//updates the z-index on the table cell when the tooltip is opened or closed
function toggleClassOnTd(){
var td = $(element).parent().parent()[0];
$(td).toggleClass('open-tooltip');
}
//want to hide tooltip when clicked anywhere
function processClick(e){
hide();
angular.element($window).off('click', processClick);
}
// on pages that have a lot of tooltip, it may be a good idea
// to not have to load all the data at once
function loadData(){
// get bound scope data and extend the current scope
if(!scope.dataLoaded && scope.toolTipData){
$q.when(scope.toolTipData.customCell.getData(scope.toolTipData.data)).then(
function(d){
scope.toolTipData.data = d;
let data = scope.toolTipData;
angular.extend(scope, data);
scope.dataLoaded = true;
$timeout(positionToolTip, 1);
}
)
}
else{
if(!scope.hidden){
$timeout(positionToolTip, 1);
}
}
}
function positionToolTip(){
let content = $(element).find('.'+scope.iconClass).children().children();
if(scope.toolTipData.customCell.onClick && scope.toolTipData.customCell.fixedPosition){
positionToolTipFixed(content);
}
else{
positionToolTipAbsolute(content);
}
}
function positionToolTipFixed(content){
scope.reposition = function(){
let rect = scope.parentEl.getBoundingClientRect();
let left = Math.round(rect.right)+10;
let top = Math.round(rect.top)-14;
content[0].style.left = left+'px';
content[0].style.top = top+'px';
}
content.addClass('tooltip-content-right');
scope.reposition();
angular.element($window).on('resize scroll', scope.reposition);
if(scope.toolTipData.customCell.containerId){
scope.container = $('#'+scope.toolTipData.customCell.containerId);
scope.container.on('scroll',scope.reposition);
}
}
// position the tooltip based on its initial position
// tooltip appears to the right of (i) icon by default
function positionToolTipAbsolute(content){
var contentContainerId = scope.toolTipData.customCell.containerId;
if(contentContainerId && content.prevObject[0]){
// change position here otherwise the calculations are off
content.prevObject[0].style.position = 'absolute';
}
let width = content.outerWidth();
let offset = content.offset();
if(!width || ! offset) return;
let elemTop = offset.top;
let elemBottom = elemTop + content.height();
// content left & right
let rightEdge = width + offset.left;
let leftEdge = offset.left;
let screenWidth = $($window).width();
// content bottom
let docViewTop = $($window).scrollTop();
let docViewBottom = docViewTop + $($window).height();
let scrollContainer;
// check to see if there is a container id that will be used to get scroll values
// this is is set on the c-tool-tip.directive(getPlayerInfoCell, getEvalInfoCell )
if(contentContainerId){
// check for prospect table to get the correct overflow
let isProspect = $('#'+contentContainerId).hasClass('prospect-overflow-auto');
if(isProspect){
scrollContainer = $('#'+contentContainerId);
}
else{
$('#'+contentContainerId)[0].style.position = 'relative';
scrollContainer = $('#'+contentContainerId).find('.overflow-auto');
}
let documentScrollTop = $(document).scrollTop();
docViewBottom = $(scrollContainer).offset().top + $(scrollContainer).height() + $(scrollContainer).scrollTop();
}
// booleans
let tooltipContentLeft = rightEdge > screenWidth || leftEdge > width;
let tooltipContentTop = elemBottom >= docViewBottom;
let tooltipContentBottom = elemTop <= docViewTop;
removeClasses(content);
let isIpadWidth = screenWidth <= 1024;
let forceBottom = scope.toolTipData && scope.toolTipData.customCell && scope.toolTipData.customCell.forceTooltip;
let scrollL = scrollContainer ? $(scrollContainer).scrollLeft() : 0;
let scrollT = scrollContainer ? $(scrollContainer).scrollTop() : 0;
if(forceBottom ||(isIpadWidth && !tooltipContentTop)){
content[0].style.right = (-275 + scrollL).toString() + 'px';
content[0].style.top = (10 -scrollT).toString() + 'px';
content.addClass('tooltip-content-bottom');
}
else if(isIpadWidth){
content[0].style.left = 'auto';
content[0].style.right = (247 -width + scrollL).toString() + 'px';
content[0].style.top = (-content.height()-45).toString() + 'px';
content.addClass('tooltip-content-top');
}
else if(tooltipContentLeft && tooltipContentTop){
content[0].style.right = (10 + scrollL).toString() + 'px';
content[0].style.left = 'inherit';
content[0].style.top = (0 - content.height() - 7 - scrollT).toString() + 'px';
content.addClass('tooltip-content-top-left');
}
else if(tooltipContentLeft){
content[0].style.right = (10 + scrollL).toString() + 'px';
content[0].style.left = 'inherit';
content[0].style.top = (-25 - scrollT).toString() +'px';
content.addClass('tooltip-content-left');
}
else if(tooltipContentTop){
content[0].style.top = (0 - content.height() - 55).toString() + 'px';
content.addClass('tooltip-content-top');
}
// default to tooltip is on the right side of the info icon
else{
content[0].style.left = '23px';
content[0].style.right = 'inherit';
content[0].style.top = '-25px';
content.addClass('tooltip-content-right');
}
}
function removeClasses(element){
element.removeClass('tooltip-content-left');
element.removeClass('tooltip-content-top');
element.removeClass('tooltip-content-right');
element.removeClass('tooltip-content-bottom');
element.removeClass('det-tool-tip-split');
}
// this logic is if you want to transclude an ng-include
// this is useful in tables
function secureTemplateUrl(){
// trust url -- needed for ng-include
scope.secureUrl = $sce.trustAsResourceUrl(attrs.template)
scope.getTemplate = function(){
return scope.secureUrl;
}
}
// cleanup
scope.$on('$destroy', function() {
if(attrs.onClick){
element.off('click', toggleHide);
}
else{
element.off('mouseenter',show);
element.off('mouseleave', hide);
}
});
}
}
}
export default Tooltip;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment