Last active
August 26, 2020 22:10
-
-
Save fourgates/e23f30ca2da5fb325facbae27b6a8359 to your computer and use it in GitHub Desktop.
AngularJS Tooltip
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
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