Created
November 27, 2017 21:09
-
-
Save tiago-seventh/d9b43cf26ea605830df36cf2cd844ed7 to your computer and use it in GitHub Desktop.
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
function MdTooltipDirective($timeout, $window, $$rAF, $document, $interpolate, | |
$mdUtil, $mdPanel, $$mdTooltipRegistry) { | |
var ENTER_EVENTS = 'focus touchstart mouseenter'; | |
var LEAVE_EVENTS = 'blur touchcancel mouseleave'; | |
var TOOLTIP_DEFAULT_Z_INDEX = 100; | |
var TOOLTIP_DEFAULT_SHOW_DELAY = 0; | |
var TOOLTIP_DEFAULT_DIRECTION = 'bottom'; | |
var TOOLTIP_DIRECTIONS = { | |
top: { x: $mdPanel.xPosition.CENTER, y: $mdPanel.yPosition.ABOVE }, | |
right: { x: $mdPanel.xPosition.OFFSET_END, y: $mdPanel.yPosition.CENTER }, | |
bottom: { x: $mdPanel.xPosition.CENTER, y: $mdPanel.yPosition.BELOW }, | |
left: { x: $mdPanel.xPosition.OFFSET_START, y: $mdPanel.yPosition.CENTER } | |
}; | |
return { | |
restrict: 'E', | |
priority: 210, // Before ngAria | |
scope: { | |
mdZIndex: '=?mdZIndex', | |
mdDelay: '=?mdDelay', | |
mdVisible: '=?mdVisible', | |
mdAutohide: '=?mdAutohide', | |
mdDirection: '@?mdDirection' // Do not expect expressions. | |
}, | |
link: linkFunc | |
}; | |
function linkFunc(scope, element, attr) { | |
// Set constants. | |
var parent = $mdUtil.getParentWithPointerEvents(element); | |
var debouncedOnResize = $$rAF.throttle(updatePosition); | |
var mouseActive = false; | |
var origin, position, panelPosition, panelRef, autohide, showTimeout, | |
elementFocusedOnWindowBlur = null; | |
// Set defaults | |
setDefaults(); | |
// Set parent aria-label. | |
addAriaLabel(); | |
// Remove the element from its current DOM position. | |
element.detach(); | |
updatePosition(); | |
bindEvents(); | |
configureWatchers(); | |
function setDefaults() { | |
scope.mdZIndex = scope.mdZIndex || TOOLTIP_DEFAULT_Z_INDEX; | |
scope.mdDelay = scope.mdDelay || TOOLTIP_DEFAULT_SHOW_DELAY; | |
if (!TOOLTIP_DIRECTIONS[scope.mdDirection]) { | |
scope.mdDirection = TOOLTIP_DEFAULT_DIRECTION; | |
} | |
} | |
function addAriaLabel(override) { | |
if (override || !parent.attr('aria-label')) { | |
// Only interpolate the text from the HTML element because otherwise the custom text | |
// could be interpolated twice and cause XSS violations. | |
var interpolatedText = override || $interpolate(element.text().trim())(scope.$parent); | |
parent.attr('aria-label', interpolatedText); | |
} | |
} | |
function updatePosition() { | |
setDefaults(); | |
// If the panel has already been created, remove the current origin | |
// class from the panel element. | |
if (panelRef && panelRef.panelEl) { | |
panelRef.panelEl.removeClass(origin); | |
} | |
// Set the panel element origin class based off of the current | |
// mdDirection. | |
origin = 'md-origin-' + scope.mdDirection; | |
// Create the position of the panel based off of the mdDirection. | |
position = TOOLTIP_DIRECTIONS[scope.mdDirection]; | |
// Using the newly created position object, use the MdPanel | |
// panelPosition API to build the panel's position. | |
panelPosition = $mdPanel.newPanelPosition() | |
.relativeTo(parent) | |
.addPanelPosition(position.x, position.y); | |
// If the panel has already been created, add the new origin class to | |
// the panel element and update it's position with the panelPosition. | |
if (panelRef && panelRef.panelEl) { | |
panelRef.panelEl.addClass(origin); | |
panelRef.updatePosition(panelPosition); | |
} | |
} | |
function bindEvents() { | |
// Add a mutationObserver where there is support for it and the need | |
// for it in the form of viable host(parent[0]). | |
if (parent[0] && 'MutationObserver' in $window) { | |
// Use a mutationObserver to tackle #2602. | |
var attributeObserver = new MutationObserver(function(mutations) { | |
if (isDisabledMutation(mutations)) { | |
$mdUtil.nextTick(function() { | |
setVisible(false); | |
}); | |
} | |
}); | |
attributeObserver.observe(parent[0], { | |
attributes: true | |
}); | |
} | |
elementFocusedOnWindowBlur = false; | |
$$mdTooltipRegistry.register('scroll', windowScrollEventHandler, true); | |
$$mdTooltipRegistry.register('blur', windowBlurEventHandler); | |
$$mdTooltipRegistry.register('resize', debouncedOnResize); | |
scope.$on('$destroy', onDestroy); | |
// To avoid 'synthetic clicks', we listen to mousedown instead of | |
// 'click'. | |
if (parent.is("span")){ | |
parent.parent(0).on('mousedown', mousedownEventHandler); | |
parent.parent(0).on(ENTER_EVENTS, enterEventHandler); | |
}else{ | |
parent.on('mousedown', mousedownEventHandler); | |
parent.on(ENTER_EVENTS, enterEventHandler); | |
} | |
function isDisabledMutation(mutations) { | |
mutations.some(function(mutation) { | |
return mutation.attributeName === 'disabled' && parent[0].disabled; | |
}); | |
return false; | |
} | |
function windowScrollEventHandler() { | |
setVisible(false); | |
} | |
function windowBlurEventHandler() { | |
elementFocusedOnWindowBlur = document.activeElement === parent[0]; | |
} | |
function enterEventHandler($event) { | |
// Prevent the tooltip from showing when the window is receiving | |
// focus. | |
if ($event.type === 'focus' && elementFocusedOnWindowBlur) { | |
elementFocusedOnWindowBlur = false; | |
} else if (!scope.mdVisible) { | |
if (parent.is("span")){ | |
parent.parent(0).on(LEAVE_EVENTS, leaveEventHandler); | |
}else{ | |
parent.on(LEAVE_EVENTS, leaveEventHandler); | |
} | |
setVisible(true); | |
// If the user is on a touch device, we should bind the tap away | |
// after the 'touched' in order to prevent the tooltip being | |
// removed immediately. | |
if ($event.type === 'touchstart') { | |
parent.one('touchend', function() { | |
$mdUtil.nextTick(function() { | |
$document.one('touchend', leaveEventHandler); | |
}, false); | |
}); | |
} | |
} | |
} | |
function leaveEventHandler() { | |
autohide = scope.hasOwnProperty('mdAutohide') ? | |
scope.mdAutohide : | |
attr.hasOwnProperty('mdAutohide'); | |
if (autohide || mouseActive || | |
$document[0].activeElement !== parent[0]) { | |
// When a show timeout is currently in progress, then we have | |
// to cancel it, otherwise the tooltip will remain showing | |
// without focus or hover. | |
if (showTimeout) { | |
$timeout.cancel(showTimeout); | |
setVisible.queued = false; | |
showTimeout = null; | |
} | |
parent.off(LEAVE_EVENTS, leaveEventHandler); | |
parent.triggerHandler('blur'); | |
setVisible(false); | |
} | |
mouseActive = false; | |
} | |
function mousedownEventHandler() { | |
mouseActive = true; | |
} | |
function onDestroy() { | |
$$mdTooltipRegistry.deregister('scroll', windowScrollEventHandler, true); | |
$$mdTooltipRegistry.deregister('blur', windowBlurEventHandler); | |
$$mdTooltipRegistry.deregister('resize', debouncedOnResize); | |
parent | |
.off(ENTER_EVENTS, enterEventHandler) | |
.off(LEAVE_EVENTS, leaveEventHandler) | |
.off('mousedown', mousedownEventHandler); | |
// Trigger the handler in case any of the tooltips are | |
// still visible. | |
leaveEventHandler(); | |
attributeObserver && attributeObserver.disconnect(); | |
} | |
} | |
function configureWatchers() { | |
if (element[0] && 'MutationObserver' in $window) { | |
var attributeObserver = new MutationObserver(function(mutations) { | |
mutations.forEach(function(mutation) { | |
if (mutation.attributeName === 'md-visible' && | |
!scope.visibleWatcher ) { | |
scope.visibleWatcher = scope.$watch('mdVisible', | |
onVisibleChanged); | |
} | |
}); | |
}); | |
attributeObserver.observe(element[0], { | |
attributes: true | |
}); | |
// Build watcher only if mdVisible is being used. | |
if (attr.hasOwnProperty('mdVisible')) { | |
scope.visibleWatcher = scope.$watch('mdVisible', | |
onVisibleChanged); | |
} | |
} else { | |
// MutationObserver not supported | |
scope.visibleWatcher = scope.$watch('mdVisible', onVisibleChanged); | |
} | |
// Direction watcher | |
scope.$watch('mdDirection', updatePosition); | |
// Clean up if the element or parent was removed via jqLite's .remove. | |
// A couple of notes: | |
// - In these cases the scope might not have been destroyed, which | |
// is why we destroy it manually. An example of this can be having | |
// `md-visible="false"` and adding tooltips while they're | |
// invisible. If `md-visible` becomes true, at some point, you'd | |
// usually get a lot of tooltips. | |
// - We use `.one`, not `.on`, because this only needs to fire once. | |
// If we were using `.on`, it would get thrown into an infinite | |
// loop. | |
// - This kicks off the scope's `$destroy` event which finishes the | |
// cleanup. | |
element.one('$destroy', onElementDestroy); | |
parent.one('$destroy', onElementDestroy); | |
scope.$on('$destroy', function() { | |
setVisible(false); | |
panelRef && panelRef.destroy(); | |
attributeObserver && attributeObserver.disconnect(); | |
element.remove(); | |
}); | |
// Updates the aria-label when the element text changes. This watch | |
// doesn't need to be set up if the element doesn't have any data | |
// bindings. | |
if (element.text().indexOf($interpolate.startSymbol()) > -1) { | |
scope.$watch(function() { | |
return element.text().trim(); | |
}, addAriaLabel); | |
} | |
function onElementDestroy() { | |
scope.$destroy(); | |
} | |
} | |
function setVisible(value) { | |
// Break if passed value is already in queue or there is no queue and | |
// passed value is current in the controller. | |
if (setVisible.queued && setVisible.value === !!value || | |
!setVisible.queued && scope.mdVisible === !!value) { | |
return; | |
} | |
setVisible.value = !!value; | |
if (!setVisible.queued) { | |
if (value) { | |
setVisible.queued = true; | |
showTimeout = $timeout(function() { | |
scope.mdVisible = setVisible.value; | |
setVisible.queued = false; | |
showTimeout = null; | |
if (!scope.visibleWatcher) { | |
onVisibleChanged(scope.mdVisible); | |
} | |
}, scope.mdDelay); | |
} else { | |
$mdUtil.nextTick(function() { | |
scope.mdVisible = false; | |
if (!scope.visibleWatcher) { | |
onVisibleChanged(false); | |
} | |
}); | |
} | |
} | |
} | |
function onVisibleChanged(isVisible) { | |
isVisible ? showTooltip() : hideTooltip(); | |
} | |
function showTooltip() { | |
// Do not show the tooltip if the text is empty. | |
if (!element[0].textContent.trim()) { | |
throw new Error('Text for the tooltip has not been provided. ' + | |
'Please include text within the mdTooltip element.'); | |
} | |
if (!panelRef) { | |
var id = 'tooltip-' + $mdUtil.nextUid(); | |
var attachTo = angular.element(document.body); | |
var panelAnimation = $mdPanel.newPanelAnimation() | |
.openFrom(parent) | |
.closeTo(parent) | |
.withAnimation({ | |
open: 'md-show', | |
close: 'md-hide' | |
}); | |
var panelConfig = { | |
id: id, | |
attachTo: attachTo, | |
contentElement: element, | |
propagateContainerEvents: true, | |
panelClass: 'md-tooltip ' + origin, | |
animation: panelAnimation, | |
position: panelPosition, | |
zIndex: scope.mdZIndex, | |
focusOnOpen: false | |
}; | |
panelRef = $mdPanel.create(panelConfig); | |
} | |
panelRef.open().then(function() { | |
panelRef.panelEl.attr('role', 'tooltip'); | |
}); | |
} | |
function hideTooltip() { | |
panelRef && panelRef.close(); | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment