UNPKG

tippy.js

Version:

Vanilla JS Tooltip & Popover Library

1,806 lines (1,527 loc) 49.3 kB
/*! * Tippy.js v3.1.1 * (c) 2017-2018 atomiks * MIT */ (function (global, factory) { typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory(require('popper.js')) : typeof define === 'function' && define.amd ? define(['popper.js'], factory) : (global.tippy = factory(global.Popper)); }(this, (function (Popper) { 'use strict'; Popper = Popper && Popper.hasOwnProperty('default') ? Popper['default'] : Popper; var version = "3.1.1"; var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; var Defaults = { a11y: true, content: '', placement: 'top', livePlacement: true, trigger: 'mouseenter focus', hideOnClick: true, animation: 'shift-away', animateFill: true, arrow: false, delay: [0, 20], duration: [325, 275], interactive: false, interactiveBorder: 2, interactiveDebounce: 0, theme: 'dark', size: 'regular', distance: 10, offset: 0, multiple: false, followCursor: false, inertia: false, updateDuration: 200, sticky: false, appendTo: function appendTo() { return document.body; }, zIndex: 9999, touchHold: false, performance: false, flip: true, flipBehavior: 'flip', arrowType: 'sharp', arrowTransform: '', target: '', allowHTML: true, showOnInit: false, popperOptions: {}, lazy: true, touch: true, wait: null, shouldPopperHideOnBlur: function shouldPopperHideOnBlur() { return true; }, onShow: function onShow() {}, onShown: function onShown() {}, onHide: function onHide() {}, onHidden: function onHidden() {}, onMount: function onMount() {} }; var setDefaults = function setDefaults(partialDefaults) { Defaults = _extends({}, Defaults, partialDefaults); }; /** * If the set() method encounters one of these, the popperInstance must be * recreated */ var POPPER_INSTANCE_RELATED_PROPS = ['placement', 'popperOptions', 'flip', 'flipBehavior', 'distance', 'offset']; var Selectors = { POPPER: '.tippy-popper', TOOLTIP: '.tippy-tooltip', CONTENT: '.tippy-content', BACKDROP: '.tippy-backdrop', ARROW: '.tippy-arrow', ROUND_ARROW: '.tippy-roundarrow' }; /** * Firefox extensions doesn't allow 'innerHTML' to be set but we can trick it * + aid for minifiers not to remove the trick */ var FF_EXTENSION_TRICK = { x: true /** * Determines if the runtime is a browser */ };var isBrowser = typeof window !== 'undefined'; /** * Determines if the browser is supported */ /** * Injects a string of CSS styles to the style node in the document head */ /** * Ponyfill for Array.from; converts iterable values to an array */ var toArray$1 = function toArray$$1(value) { return [].slice.call(value); }; /** * Sets the content of a tooltip */ var setContent = function setContent(contentEl, props) { if (props.content instanceof Element) { setInnerHTML(contentEl, ''); contentEl.appendChild(props.content); } else { contentEl[props.allowHTML ? 'innerHTML' : 'textContent'] = props.content; } }; /** * Determines if an element can receive focus */ var elementCanReceiveFocus = function elementCanReceiveFocus(el) { return el instanceof Element ? matches.call(el, 'a[href],area[href],button,details,input,textarea,select,iframe,[tabindex]') && !el.hasAttribute('disabled') : true; }; /** * Applies a transition duration to a list of elements */ var applyTransitionDuration = function applyTransitionDuration(els, value) { els.filter(Boolean).forEach(function (el) { el.style.transitionDuration = value + 'ms'; }); }; /** * Returns the child elements of a popper element */ var getChildren = function getChildren(popper) { var select = function select(s) { return popper.querySelector(s); }; return { tooltip: select(Selectors.TOOLTIP), backdrop: select(Selectors.BACKDROP), content: select(Selectors.CONTENT), arrow: select(Selectors.ARROW) || select(Selectors.ROUND_ARROW) }; }; /** * Determines if a value is a plain object */ var isPlainObject = function isPlainObject(value) { return {}.toString.call(value) === '[object Object]'; }; /** * Creates and returns a div element */ var div = function div() { return document.createElement('div'); }; /** * Sets the innerHTML of an element while tricking linters & minifiers */ var setInnerHTML = function setInnerHTML(el, html) { el[FF_EXTENSION_TRICK.x && 'innerHTML'] = html instanceof Element ? html[FF_EXTENSION_TRICK.x && 'innerHTML'] : html; }; /** * Returns an array of elements based on the value */ var getArrayOfElements = function getArrayOfElements(value) { if (value instanceof Element || isPlainObject(value)) { return [value]; } if (value instanceof NodeList) { return toArray$1(value); } if (Array.isArray(value)) { return value; } try { return toArray$1(document.querySelectorAll(value)); } catch (e) { return []; } }; /** * Determines if a value is numeric */ var isNumeric = function isNumeric(value) { return !isNaN(value) && !isNaN(parseFloat(value)); }; /** * Returns a value at a given index depending on if it's an array or number */ var getValue = function getValue(value, index, defaultValue) { if (Array.isArray(value)) { var v = value[index]; return v == null ? defaultValue : v; } return value; }; /** * Creates an arrow element and returns it */ var createArrowElement = function createArrowElement(arrowType) { var arrow = div(); if (arrowType === 'round') { arrow.className = 'tippy-roundarrow'; setInnerHTML(arrow, '<svg viewBox="0 0 24 8" xmlns="http://www.w3.org/2000/svg"><path d="M3 8s2.021-.015 5.253-4.218C9.584 2.051 10.797 1.007 12 1c1.203-.007 2.416 1.035 3.761 2.782C19.012 8.005 21 8 21 8H3z"/></svg>'); } else { arrow.className = 'tippy-arrow'; } return arrow; }; /** * Creates a backdrop element and returns it */ var createBackdropElement = function createBackdropElement() { var backdrop = div(); backdrop.className = 'tippy-backdrop'; backdrop.setAttribute('data-state', 'hidden'); return backdrop; }; /** * Adds interactive attributes */ var addInteractive = function addInteractive(popper, tooltip) { popper.setAttribute('tabindex', '-1'); tooltip.setAttribute('data-interactive', ''); }; /** * Removes interactive attributes */ var removeInteractive = function removeInteractive(popper, tooltip) { popper.removeAttribute('tabindex'); tooltip.removeAttribute('data-interactive'); }; /** * Adds inertia attribute */ var addInertia = function addInertia(tooltip) { tooltip.setAttribute('data-inertia', ''); }; /** * Removes inertia attribute */ var removeInertia = function removeInertia(tooltip) { tooltip.removeAttribute('data-inertia'); }; /** * Constructs the popper element and returns it */ var createPopperElement = function createPopperElement(id, props) { var popper = div(); popper.className = 'tippy-popper'; popper.setAttribute('role', 'tooltip'); popper.id = 'tippy-' + id; popper.style.zIndex = props.zIndex; var tooltip = div(); tooltip.className = 'tippy-tooltip'; tooltip.setAttribute('data-size', props.size); tooltip.setAttribute('data-animation', props.animation); tooltip.setAttribute('data-state', 'hidden'); props.theme.split(' ').forEach(function (t) { tooltip.classList.add(t + '-theme'); }); var content = div(); content.className = 'tippy-content'; content.setAttribute('data-state', 'hidden'); if (props.interactive) { addInteractive(popper, tooltip); } if (props.arrow) { tooltip.appendChild(createArrowElement(props.arrowType)); } if (props.animateFill) { tooltip.appendChild(createBackdropElement()); tooltip.setAttribute('data-animatefill', ''); } if (props.inertia) { tooltip.setAttribute('data-inertia', ''); } setContent(content, props); tooltip.appendChild(content); popper.appendChild(tooltip); popper.addEventListener('focusout', function (e) { if (e.relatedTarget && popper._tippy && !closestCallback(e.relatedTarget, function (el) { return el === popper; }) && e.relatedTarget !== popper._tippy.reference && popper._tippy.props.shouldPopperHideOnBlur(e)) { popper._tippy.hide(); } }); return popper; }; /** * Updates the popper element based on the new props */ var updatePopperElement = function updatePopperElement(popper, prevProps, nextProps) { var _getChildren = getChildren(popper), tooltip = _getChildren.tooltip, content = _getChildren.content, backdrop = _getChildren.backdrop, arrow = _getChildren.arrow; popper.style.zIndex = nextProps.zIndex; tooltip.setAttribute('data-size', nextProps.size); tooltip.setAttribute('data-animation', nextProps.animation); if (prevProps.content !== nextProps.content) { setContent(content, nextProps); } // animateFill if (!prevProps.animateFill && nextProps.animateFill) { tooltip.appendChild(createBackdropElement()); tooltip.setAttribute('data-animatefill', ''); } else if (prevProps.animateFill && !nextProps.animateFill) { tooltip.removeChild(backdrop); tooltip.removeAttribute('data-animatefill'); } // arrow if (!prevProps.arrow && nextProps.arrow) { tooltip.appendChild(createArrowElement(nextProps.arrowType)); } else if (prevProps.arrow && !nextProps.arrow) { tooltip.removeChild(arrow); } // arrowType if (prevProps.arrow && nextProps.arrow && prevProps.arrowType !== nextProps.arrowType) { tooltip.replaceChild(createArrowElement(nextProps.arrowType), arrow); } // interactive if (!prevProps.interactive && nextProps.interactive) { addInteractive(popper, tooltip); } else if (prevProps.interactive && !nextProps.interactive) { removeInteractive(popper, tooltip); } // inertia if (!prevProps.inertia && nextProps.inertia) { addInertia(tooltip); } else if (prevProps.inertia && !nextProps.inertia) { removeInertia(tooltip); } // theme if (prevProps.theme !== nextProps.theme) { prevProps.theme.split(' ').forEach(function (theme) { tooltip.classList.remove(theme + '-theme'); }); nextProps.theme.split(' ').forEach(function (theme) { tooltip.classList.add(theme + '-theme'); }); } }; /** * Hides all visible poppers on the document */ var hideAllPoppers = function hideAllPoppers(excludeTippy) { toArray$1(document.querySelectorAll(Selectors.POPPER)).forEach(function (popper) { var tip = popper._tippy; if (tip && tip.props.hideOnClick === true && (!excludeTippy || popper !== excludeTippy.popper)) { tip.hide(); } }); }; /** * Returns an object of optional props from data-tippy-* attributes */ var getDataAttributeOptions = function getDataAttributeOptions(reference) { return Object.keys(Defaults).reduce(function (acc, key) { var valueAsString = (reference.getAttribute('data-tippy-' + key) || '').trim(); if (!valueAsString) { return acc; } if (key === 'content') { acc[key] = valueAsString; } else if (valueAsString === 'true') { acc[key] = true; } else if (valueAsString === 'false') { acc[key] = false; } else if (isNumeric(valueAsString)) { acc[key] = Number(valueAsString); } else if (valueAsString[0] === '[' || valueAsString[0] === '{') { acc[key] = JSON.parse(valueAsString); } else { acc[key] = valueAsString; } return acc; }, {}); }; /** * Polyfills the virtual reference (plain object) with needed props * Mutating because DOM elements are mutated, adds _tippy property */ var polyfillVirtualReferenceProps = function polyfillVirtualReferenceProps(virtualReference) { var polyfills = { isVirtual: true, attributes: virtualReference.attributes || {}, setAttribute: function setAttribute(key, value) { virtualReference.attributes[key] = value; }, getAttribute: function getAttribute(key) { return virtualReference.attributes[key]; }, removeAttribute: function removeAttribute(key) { delete virtualReference.attributes[key]; }, hasAttribute: function hasAttribute(key) { return key in virtualReference.attributes; }, addEventListener: function addEventListener() {}, removeEventListener: function removeEventListener() {}, classList: { classNames: {}, add: function add(key) { virtualReference.classList.classNames[key] = true; }, remove: function remove(key) { delete virtualReference.classList.classNames[key]; }, contains: function contains(key) { return key in virtualReference.classList.classNames; } } }; for (var key in polyfills) { virtualReference[key] = polyfills[key]; } return virtualReference; }; /** * Ponyfill for Element.prototype.matches */ var matches = function () { if (isBrowser) { var e = Element.prototype; return e.matches || e.matchesSelector || e.webkitMatchesSelector || e.mozMatchesSelector || e.msMatchesSelector; } }(); /** * Ponyfill for Element.prototype.closest */ var closest = function closest(element, parentSelector) { return (Element.prototype.closest || function (selector) { var el = this; while (el) { if (matches.call(el, selector)) return el; el = el.parentElement; } }).call(element, parentSelector); }; /** * Works like Element.prototype.closest, but uses a callback instead */ var closestCallback = function closestCallback(element, callback) { while (element) { if (callback(element)) return element; element = element.parentElement; } }; /** * Focuses an element while preventing a scroll jump if it's not within the viewport */ var focus = function focus(el) { var x = window.scrollX || window.pageXOffset; var y = window.scrollY || window.pageYOffset; el.focus(); scroll(x, y); }; /** * Triggers reflow */ var reflow = function reflow(popper) { void popper.offsetHeight; }; /** * Transforms the x/y axis ased on the placement */ var transformAxisBasedOnPlacement = function transformAxisBasedOnPlacement(axis, isVertical) { return (isVertical ? axis : { X: 'Y', Y: 'X' }[axis]) || ''; }; /** * Transforms the scale/translate numbers based on the placement */ var transformNumbersBasedOnPlacement = function transformNumbersBasedOnPlacement(type, numbers, isVertical, isReverse) { /** * Avoid destructuring because a large boilerplate function is generated * by Babel */ var a = numbers[0]; var b = numbers[1]; if (!a && !b) { return ''; } var transforms = { scale: function () { if (!b) { return '' + a; } else { return isVertical ? a + ', ' + b : b + ', ' + a; } }(), translate: function () { if (!b) { return isReverse ? -a + 'px' : a + 'px'; } else { if (isVertical) { return isReverse ? a + 'px, ' + -b + 'px' : a + 'px, ' + b + 'px'; } else { return isReverse ? -b + 'px, ' + a + 'px' : b + 'px, ' + a + 'px'; } } }() }; return transforms[type]; }; /** * Returns the axis for a CSS function (translate or scale) */ var getTransformAxis = function getTransformAxis(str, cssFunction) { var match = str.match(new RegExp(cssFunction + '([XY])')); return match ? match[1] : ''; }; /** * Returns the numbers given to the CSS function */ var getTransformNumbers = function getTransformNumbers(str, regex) { var match = str.match(regex); return match ? match[1].split(',').map(parseFloat) : []; }; var TRANSFORM_NUMBER_RE = { translate: /translateX?Y?\(([^)]+)\)/, scale: /scaleX?Y?\(([^)]+)\)/ /** * Computes the arrow's transform so that it is correct for any placement */ };var computeArrowTransform = function computeArrowTransform(arrow, arrowTransform) { var placement = getPopperPlacement(closest(arrow, Selectors.POPPER)); var isVertical = placement === 'top' || placement === 'bottom'; var isReverse = placement === 'right' || placement === 'bottom'; var matches = { translate: { axis: getTransformAxis(arrowTransform, 'translate'), numbers: getTransformNumbers(arrowTransform, TRANSFORM_NUMBER_RE.translate) }, scale: { axis: getTransformAxis(arrowTransform, 'scale'), numbers: getTransformNumbers(arrowTransform, TRANSFORM_NUMBER_RE.scale) } }; var computedTransform = arrowTransform.replace(TRANSFORM_NUMBER_RE.translate, 'translate' + transformAxisBasedOnPlacement(matches.translate.axis, isVertical) + '(' + transformNumbersBasedOnPlacement('translate', matches.translate.numbers, isVertical, isReverse) + ')').replace(TRANSFORM_NUMBER_RE.scale, 'scale' + transformAxisBasedOnPlacement(matches.scale.axis, isVertical) + '(' + transformNumbersBasedOnPlacement('scale', matches.scale.numbers, isVertical, isReverse) + ')'); arrow.style[typeof document.body.style.transform !== 'undefined' ? 'transform' : 'webkitTransform'] = computedTransform; }; /** * Sets the visibility state of a popper so it can begin to transition in or out */ var setVisibilityState = function setVisibilityState(els, type) { els.filter(Boolean).forEach(function (el) { el.setAttribute('data-state', type); }); }; /** * Runs the callback after the popper's position has been updated * update() is debounced with setTimeout(0) and scheduleUpdate() is * update() wrapped in requestAnimationFrame(). */ var afterPopperPositionUpdates = function afterPopperPositionUpdates(popperInstance, callback) { var popper = popperInstance.popper, options = popperInstance.options; var onCreate = options.onCreate, onUpdate = options.onUpdate; options.onCreate = options.onUpdate = function () { reflow(popper); callback(); onUpdate(); options.onCreate = onCreate; options.onUpdate = onUpdate; }; }; /** * Defers a function's execution until the call stack has cleared */ var defer = function defer(fn) { setTimeout(fn, 1); }; /** * Determines if the mouse cursor is outside of the popper's interactive border * region */ var isCursorOutsideInteractiveBorder = function isCursorOutsideInteractiveBorder(popperPlacement, popperRect, event, props) { if (!popperPlacement) { return true; } var x = event.clientX, y = event.clientY; var interactiveBorder = props.interactiveBorder, distance = props.distance; var exceedsTop = popperRect.top - y > (popperPlacement === 'top' ? interactiveBorder + distance : interactiveBorder); var exceedsBottom = y - popperRect.bottom > (popperPlacement === 'bottom' ? interactiveBorder + distance : interactiveBorder); var exceedsLeft = popperRect.left - x > (popperPlacement === 'left' ? interactiveBorder + distance : interactiveBorder); var exceedsRight = x - popperRect.right > (popperPlacement === 'right' ? interactiveBorder + distance : interactiveBorder); return exceedsTop || exceedsBottom || exceedsLeft || exceedsRight; }; /** * Returns the distance offset, taking into account the default offset due to * the transform: translate() rule in CSS */ var getOffsetDistanceInPx = function getOffsetDistanceInPx(distance, defaultDistance) { return -(distance - defaultDistance) + 'px'; }; /** * Returns the popper's placement, ignoring shifting (top-start, etc) */ var getPopperPlacement = function getPopperPlacement(popper) { var fullPlacement = popper.getAttribute('x-placement'); return fullPlacement ? fullPlacement.split('-')[0] : ''; }; /** * Evaluates props */ var evaluateProps = function evaluateProps(reference, props) { var out = _extends({}, props, props.performance ? {} : getDataAttributeOptions(reference)); if (out.arrow) { out.animateFill = false; } if (typeof out.appendTo === 'function') { out.appendTo = props.appendTo(reference); } if (typeof out.content === 'function') { out.content = props.content(reference); } return out; }; /** * Add/remove transitionend listener from tooltip */ var toggleTransitionEndListener = function toggleTransitionEndListener(tooltip, action, listener) { tooltip[action + 'EventListener']('transitionend', listener); }; /** * Debounce utility */ var debounce = function debounce(fn, ms) { var timeoutId = void 0; return function () { var _this = this, _arguments = arguments; clearTimeout(timeoutId); timeoutId = setTimeout(function () { return fn.apply(_this, _arguments); }, ms); }; }; /** * Validates an object of options with the valid default props object */ var validateOptions = function validateOptions(options, props) { for (var option in options || {}) { if (!(option in props)) { throw Error('[tippy]: `' + option + '` is not a valid option'); } } }; var nav = isBrowser ? navigator : {}; var win = isBrowser ? window : {}; var isIE = /MSIE |Trident\//.test(nav.userAgent); var isIOS = /iPhone|iPad|iPod/.test(nav.platform) && !win.MSStream; var supportsTouch = 'ontouchstart' in win; var isUsingTouch = false; var onDocumentTouch = function onDocumentTouch() { if (isUsingTouch) { return; } isUsingTouch = true; if (isIOS) { document.body.classList.add('tippy-iOS'); } if (window.performance) { document.addEventListener('mousemove', onDocumentMouseMove); } }; var lastMouseMoveTime = 0; var onDocumentMouseMove = function onDocumentMouseMove() { var now = performance.now(); // Chrome 60+ is 1 mousemove per animation frame, use 20ms time difference if (now - lastMouseMoveTime < 20) { isUsingTouch = false; document.removeEventListener('mousemove', onDocumentMouseMove); if (!isIOS) { document.body.classList.remove('tippy-iOS'); } } lastMouseMoveTime = now; }; var onDocumentClick = function onDocumentClick(_ref) { var target = _ref.target; // Simulated events dispatched on the document if (!(target instanceof Element)) { return hideAllPoppers(); } // Clicked on an interactive popper var popper = closest(target, Selectors.POPPER); if (popper && popper._tippy && popper._tippy.props.interactive) { return; } // Clicked on a reference var reference = closestCallback(target, function (el) { return el._tippy && el._tippy.reference === el; }); if (reference) { var tip = reference._tippy; var isClickTrigger = tip.props.trigger.indexOf('click') > -1; if (isUsingTouch || isClickTrigger) { return hideAllPoppers(tip); } if (tip.props.hideOnClick !== true || isClickTrigger) { return; } tip.clearDelayTimeouts(); } hideAllPoppers(); }; var onWindowBlur = function onWindowBlur() { var _document = document, activeElement = _document.activeElement; if (activeElement && activeElement.blur && activeElement._tippy) { activeElement.blur(); } }; var onWindowResize = function onWindowResize() { toArray$1(document.querySelectorAll(Selectors.POPPER)).forEach(function (popper) { var tippyInstance = popper._tippy; if (!tippyInstance.props.livePlacement) { tippyInstance.popperInstance.scheduleUpdate(); } }); }; /** * Adds the needed global event listeners */ function bindEventListeners(useCapture) { document.addEventListener('click', onDocumentClick, useCapture); document.addEventListener('touchstart', onDocumentTouch); window.addEventListener('blur', onWindowBlur); window.addEventListener('resize', onWindowResize); if (!supportsTouch && (navigator.maxTouchPoints || navigator.msMaxTouchPoints)) { document.addEventListener('pointerdown', onDocumentTouch); } } var idCounter = 1; function createTippy(reference, collectionProps) { var props = evaluateProps(reference, collectionProps); // If the reference shouldn't have multiple tippys, return null early if (!props.multiple && reference._tippy) { return null; } /* ======================= 🔒 Private members 🔒 ======================= */ var popperMutationObserver = null; var lastTriggerEvent = {}; var lastMouseMoveEvent = null; var showTimeoutId = 0; var hideTimeoutId = 0; var isPreparingToShow = false; var transitionEndListener = function transitionEndListener() {}; var listeners = []; var referenceJustProgrammaticallyFocused = false; var firstPopperInstanceInit = false; var debouncedOnMouseMove = props.interactiveDebounce > 0 ? debounce(onMouseMove, props.interactiveDebounce) : onMouseMove; /* ======================= 🔑 Public members 🔑 ======================= */ var id = idCounter++; var popper = createPopperElement(id, props); var popperChildren = getChildren(popper); var state = { isEnabled: true, isVisible: false, isDestroyed: false, isMounted: false, isShown: false }; var popperInstance = null; // 🌟 tippy instance var tip = { // properties id: id, reference: reference, popper: popper, popperChildren: popperChildren, popperInstance: popperInstance, props: props, state: state, // methods clearDelayTimeouts: clearDelayTimeouts, set: set$$1, setContent: setContent$$1, show: show, hide: hide, enable: enable, disable: disable, destroy: destroy }; addTriggersToReference(); reference.addEventListener('click', onReferenceClick); if (!props.lazy) { tip.popperInstance = createPopperInstance(); tip.popperInstance.disableEventListeners(); } if (props.showOnInit) { /** * Firefox has a bug where the tooltip will be placed incorrectly due to * strange layout on load, `setTimeout` gives the layout time to adjust * properly */ setTimeout(prepareShow, 20); } // Ensure the reference element can receive focus (and is not a delegate) if (props.a11y && !props.target && !elementCanReceiveFocus(reference)) { reference.setAttribute('tabindex', '0'); } // Install shortcuts reference._tippy = tip; popper._tippy = tip; return tip; /* ======================= 🔒 Private methods 🔒 ======================= */ /** * If the reference was clicked, it also receives focus */ function onReferenceClick() { defer(function () { referenceJustProgrammaticallyFocused = false; }); } /** * Positions the virtual reference near the mouse cursor */ function positionVirtualReferenceNearCursor(event) { var _lastMouseMoveEvent = lastMouseMoveEvent = event, clientX = _lastMouseMoveEvent.clientX, clientY = _lastMouseMoveEvent.clientY; if (!tip.popperInstance) { return; } var rect = tip.reference.getBoundingClientRect(); var followCursor = tip.props.followCursor; var isHorizontal = followCursor === 'horizontal'; var isVertical = followCursor === 'vertical'; tip.popperInstance.reference = { getBoundingClientRect: function getBoundingClientRect() { return { width: 0, height: 0, top: isHorizontal ? rect.top : clientY, bottom: isHorizontal ? rect.bottom : clientY, left: isVertical ? rect.left : clientX, right: isVertical ? rect.right : clientX }; }, clientWidth: 0, clientHeight: 0 }; tip.popperInstance.scheduleUpdate(); } /** * Creates the tippy instance for a delegate when it's been triggered */ function createDelegateChildTippy(event) { var targetEl = closest(event.target, tip.props.target); if (targetEl && !targetEl._tippy) { createTippy(targetEl, _extends({}, tip.props, { target: '', showOnInit: true })); prepareShow(event); } } /** * Setup before show() is invoked (delays, etc.) */ function prepareShow(event) { clearDelayTimeouts(); if (tip.state.isVisible) { return; } // Is a delegate, create an instance for the child target if (tip.props.target) { return createDelegateChildTippy(event); } isPreparingToShow = true; if (tip.props.wait) { return tip.props.wait(tip, event); } /** * If the tooltip has a delay, we need to be listening to the mousemove as * soon as the trigger event is fired so that it's in the correct position * upon mount */ if (hasFollowCursorBehavior()) { if (popperChildren.arrow) { popperChildren.arrow.style.margin = '0'; } document.addEventListener('mousemove', positionVirtualReferenceNearCursor); } var delay = getValue(tip.props.delay, 0, Defaults.delay); if (delay) { showTimeoutId = setTimeout(function () { show(); }, delay); } else { show(); } } /** * Setup before hide() is invoked (delays, etc.) */ function prepareHide() { clearDelayTimeouts(); if (!tip.state.isVisible) { return removeFollowCursorListener(); } isPreparingToShow = false; var delay = getValue(tip.props.delay, 1, Defaults.delay); if (delay) { hideTimeoutId = setTimeout(function () { if (tip.state.isVisible) { hide(); } }, delay); } else { hide(); } } /** * Removes the follow cursor listener */ function removeFollowCursorListener() { document.removeEventListener('mousemove', positionVirtualReferenceNearCursor); lastMouseMoveEvent = null; } /** * Cleans up old listeners */ function cleanupOldMouseMoveListeners() { document.body.removeEventListener('mouseleave', prepareHide); document.removeEventListener('mousemove', debouncedOnMouseMove); } /** * Event listener invoked upon trigger */ function onTrigger(event) { if (!tip.state.isEnabled || isEventListenerStopped(event)) { return; } if (!tip.state.isVisible) { lastTriggerEvent = event; } // Toggle show/hide when clicking click-triggered tooltips if (event.type === 'click' && tip.props.hideOnClick !== false && tip.state.isVisible) { prepareHide(); } else { prepareShow(event); } } /** * Event listener used for interactive tooltips to detect when they should hide */ function onMouseMove(event) { var referenceTheCursorIsOver = closestCallback(event.target, function (el) { return el._tippy; }); var isCursorOverPopper = closest(event.target, Selectors.POPPER) === tip.popper; var isCursorOverReference = referenceTheCursorIsOver === tip.reference; if (isCursorOverPopper || isCursorOverReference) { return; } if (isCursorOutsideInteractiveBorder(getPopperPlacement(tip.popper), tip.popper.getBoundingClientRect(), event, tip.props)) { cleanupOldMouseMoveListeners(); prepareHide(); } } /** * Event listener invoked upon mouseleave */ function onMouseLeave(event) { if (isEventListenerStopped(event)) { return; } if (tip.props.interactive) { document.body.addEventListener('mouseleave', prepareHide); document.addEventListener('mousemove', debouncedOnMouseMove); return; } prepareHide(); } /** * Event listener invoked upon blur */ function onBlur(event) { if (event.target !== tip.reference) { return; } if (tip.props.interactive) { if (!event.relatedTarget) { return; } if (closest(event.relatedTarget, Selectors.POPPER)) { return; } } prepareHide(); } /** * Event listener invoked when a child target is triggered */ function onDelegateShow(event) { if (closest(event.target, tip.props.target)) { prepareShow(event); } } /** * Event listener invoked when a child target should hide */ function onDelegateHide(event) { if (closest(event.target, tip.props.target)) { prepareHide(); } } /** * Determines if an event listener should stop further execution due to the * `touchHold` option. */ function isEventListenerStopped(event) { var isTouchEvent = event.type.indexOf('touch') > -1; var caseA = supportsTouch && isUsingTouch && tip.props.touchHold && !isTouchEvent; var caseB = isUsingTouch && !tip.props.touchHold && isTouchEvent; return caseA || caseB; } /** * Creates the popper instance for the tip */ function createPopperInstance() { var tooltip = tip.popperChildren.tooltip; var popperOptions = tip.props.popperOptions; var arrowSelector = Selectors[tip.props.arrowType === 'round' ? 'ROUND_ARROW' : 'ARROW']; var arrow = tooltip.querySelector(arrowSelector); var config = _extends({ placement: tip.props.placement }, popperOptions || {}, { modifiers: _extends({}, popperOptions ? popperOptions.modifiers : {}, { arrow: _extends({ element: arrowSelector }, popperOptions && popperOptions.modifiers ? popperOptions.modifiers.arrow : {}), flip: _extends({ enabled: tip.props.flip, padding: tip.props.distance + 5 /* 5px from viewport boundary */ , behavior: tip.props.flipBehavior }, popperOptions && popperOptions.modifiers ? popperOptions.modifiers.flip : {}), offset: _extends({ offset: tip.props.offset }, popperOptions && popperOptions.modifiers ? popperOptions.modifiers.offset : {}) }), onCreate: function onCreate() { tooltip.style[getPopperPlacement(tip.popper)] = getOffsetDistanceInPx(tip.props.distance, Defaults.distance); if (arrow && tip.props.arrowTransform) { computeArrowTransform(arrow, tip.props.arrowTransform); } }, onUpdate: function onUpdate() { var styles = tooltip.style; styles.top = ''; styles.bottom = ''; styles.left = ''; styles.right = ''; styles[getPopperPlacement(tip.popper)] = getOffsetDistanceInPx(tip.props.distance, Defaults.distance); if (arrow && tip.props.arrowTransform) { computeArrowTransform(arrow, tip.props.arrowTransform); } } }); /** * Ensure the popper's position stays correct if its dimensions change. * Use .update() over .scheduleUpdate() so there is no 1 frame flash * due to async update. */ var observer = new MutationObserver(function () { tip.popperInstance.update(); }); observer.observe(tip.popper, { childList: true, subtree: true }); if (popperMutationObserver) { popperMutationObserver.disconnect(); } popperMutationObserver = observer; // fixes https://github.com/atomiks/tippyjs/issues/193 if (!firstPopperInstanceInit) { firstPopperInstanceInit = true; tip.popper.addEventListener('mouseenter', function (event) { if (tip.props.interactive && tip.state.isVisible && lastTriggerEvent.type === 'mouseenter') { prepareShow(event); } }); tip.popper.addEventListener('mouseleave', function (event) { if (tip.props.interactive && lastTriggerEvent.type === 'mouseenter' && tip.props.interactiveDebounce === 0 && isCursorOutsideInteractiveBorder(getPopperPlacement(tip.popper), tip.popper.getBoundingClientRect(), event, tip.props)) { prepareHide(); } }); } return new Popper(tip.reference, tip.popper, config); } /** * Mounts the tooltip to the DOM, callback to show tooltip is run **after** * popper's position has updated */ function mount(callback) { if (!tip.popperInstance) { tip.popperInstance = createPopperInstance(); if (!tip.props.livePlacement) { tip.popperInstance.disableEventListeners(); } } else { if (!hasFollowCursorBehavior()) { tip.popperInstance.scheduleUpdate(); } if (tip.props.livePlacement && !hasFollowCursorBehavior()) { tip.popperInstance.enableEventListeners(); } } /** * If the instance previously had followCursor behavior, it will be * positioned incorrectly if triggered by `focus` afterwards. * Update the reference back to the real DOM element */ tip.popperInstance.reference = tip.reference; if (hasFollowCursorBehavior()) { if (tip.popperChildren.arrow) { tip.popperChildren.arrow.style.margin = ''; } var delay = getValue(tip.props.delay, 0, Defaults.delay); if (lastTriggerEvent.type) { positionVirtualReferenceNearCursor(delay && lastMouseMoveEvent ? lastMouseMoveEvent : lastTriggerEvent); } } afterPopperPositionUpdates(tip.popperInstance, callback); if (!tip.props.appendTo.contains(tip.popper)) { tip.props.appendTo.appendChild(tip.popper); tip.props.onMount(tip); tip.state.isMounted = true; } } /** * Determines if the instance is in `followCursor` mode */ function hasFollowCursorBehavior() { return tip.props.followCursor && !isUsingTouch && lastTriggerEvent.type !== 'focus'; } /** * Updates the tooltip's position on each animation frame + timeout */ function makeSticky() { applyTransitionDuration([tip.popper], isIE ? 0 : tip.props.updateDuration); var updatePosition = function updatePosition() { if (tip.popperInstance) { tip.popperInstance.scheduleUpdate(); } if (tip.state.isMounted) { requestAnimationFrame(updatePosition); } else { applyTransitionDuration([tip.popper], 0); } }; updatePosition(); } /** * Invokes a callback once the tooltip has fully transitioned out */ function onTransitionedOut(duration, callback) { onTransitionEnd(duration, function () { if (!tip.state.isVisible && tip.props.appendTo.contains(tip.popper)) { callback(); } }); } /** * Invokes a callback once the tooltip has fully transitioned in */ function onTransitionedIn(duration, callback) { onTransitionEnd(duration, callback); } /** * Invokes a callback once the tooltip's CSS transition ends */ function onTransitionEnd(duration, callback) { // Make callback synchronous if duration is 0 if (duration === 0) { return callback(); } var tooltip = tip.popperChildren.tooltip; var listener = function listener(e) { if (e.target === tooltip) { toggleTransitionEndListener(tooltip, 'remove', listener); callback(); } }; toggleTransitionEndListener(tooltip, 'remove', transitionEndListener); toggleTransitionEndListener(tooltip, 'add', listener); transitionEndListener = listener; } /** * Adds an event listener to the reference */ function on(eventType, handler, acc) { tip.reference.addEventListener(eventType, handler); acc.push({ eventType: eventType, handler: handler }); } /** * Adds event listeners to the reference based on the `trigger` prop */ function addTriggersToReference() { listeners = tip.props.trigger.trim().split(' ').reduce(function (acc, eventType) { if (eventType === 'manual') { return acc; } if (!tip.props.target) { on(eventType, onTrigger, acc); if (tip.props.touchHold) { on('touchstart', onTrigger, acc); on('touchend', onMouseLeave, acc); } switch (eventType) { case 'mouseenter': on('mouseleave', onMouseLeave, acc); break; case 'focus': on(isIE ? 'focusout' : 'blur', onBlur, acc); break; } } else { switch (eventType) { case 'mouseenter': on('mouseover', onDelegateShow, acc); on('mouseout', onDelegateHide, acc); break; case 'focus': on('focusin', onDelegateShow, acc); on('focusout', onDelegateHide, acc); break; case 'click': on(eventType, onDelegateShow, acc); break; } } return acc; }, []); } /** * Removes event listeners from the reference */ function removeTriggersFromReference() { listeners.forEach(function (_ref) { var eventType = _ref.eventType, handler = _ref.handler; tip.reference.removeEventListener(eventType, handler); }); } /* ======================= 🔑 Public methods 🔑 ======================= */ /** * Enables the instance to allow it to show or hide */ function enable() { tip.state.isEnabled = true; } /** * Disables the instance to disallow it to show or hide */ function disable() { tip.state.isEnabled = false; } /** * Clears pending timeouts related to the `delay` prop if any */ function clearDelayTimeouts() { clearTimeout(showTimeoutId); clearTimeout(hideTimeoutId); } /** * Sets new props for the instance and redraws the tooltip */ function set$$1(options) { validateOptions(options, Defaults); var prevProps = tip.props; var nextProps = evaluateProps(tip.reference, _extends({}, tip.props, options, { performance: true })); nextProps.performance = options.performance || prevProps.performance; tip.props = nextProps; if ('trigger' in options || 'touchHold' in options) { removeTriggersFromReference(); addTriggersToReference(); } if ('interactiveDebounce' in options) { cleanupOldMouseMoveListeners(); debouncedOnMouseMove = debounce(onMouseMove, options.interactiveDebounce); } updatePopperElement(tip.popper, prevProps, nextProps); tip.popperChildren = getChildren(tip.popper); if (tip.popperInstance && POPPER_INSTANCE_RELATED_PROPS.some(function (prop) { return prop in options; })) { tip.popperInstance.destroy(); tip.popperInstance = createPopperInstance(); if (!tip.state.isVisible) { tip.popperInstance.disableEventListeners(); } if (tip.props.followCursor && lastMouseMoveEvent) { positionVirtualReferenceNearCursor(lastMouseMoveEvent); } } } /** * Shortcut for .set({ content: newContent }) */ function setContent$$1(content) { set$$1({ content: content }); } /** * Shows the tooltip */ function show() { var duration = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : getValue(tip.props.duration, 0, Defaults.duration[0]); if (tip.state.isDestroyed || !tip.state.isEnabled || isUsingTouch && !tip.props.touch) { return; } // Destroy tooltip if the reference element is no longer on the DOM if (!tip.reference.isVirtual && !document.documentElement.contains(tip.reference)) { return destroy(); } // Do not show tooltip if the reference element has a `disabled` attribute if (tip.reference.hasAttribute('disabled')) { return; } // If the reference was just programmatically focused for accessibility reasons if (referenceJustProgrammaticallyFocused) { referenceJustProgrammaticallyFocused = false; return; } if (tip.props.onShow(tip) === false) { return; } tip.popper.style.visibility = 'visible'; tip.state.isVisible = true; // Prevent a transition if the popper is at the opposite placement applyTransitionDuration([tip.popper, tip.popperChildren.tooltip, tip.popperChildren.backdrop], 0); mount(function () { if (!tip.state.isVisible) { return; } // Arrow will sometimes not be positioned correctly. Force another update. if (!hasFollowCursorBehavior()) { tip.popperInstance.update(); } applyTransitionDuration([tip.popperChildren.tooltip, tip.popperChildren.backdrop, tip.popperChildren.content], duration); if (tip.popperChildren.backdrop) { tip.popperChildren.content.style.transitionDelay = Math.round(duration / 6) + 'ms'; } if (tip.props.interactive) { tip.reference.classList.add('tippy-active'); } if (tip.props.sticky) { makeSticky(); } setVisibilityState([tip.popperChildren.tooltip, tip.popperChildren.backdrop, tip.popperChildren.content], 'visible'); onTransitionedIn(duration, function () { if (tip.props.updateDuration === 0) { tip.popperChildren.tooltip.classList.add('tippy-notransition'); } if (tip.props.interactive && ['focus', 'click'].indexOf(lastTriggerEvent.type) > -1) { focus(tip.popper); } tip.reference.setAttribute('aria-describedby', tip.popper.id); tip.props.onShown(tip); tip.state.isShown = true; }); }); } /** * Hides the tooltip */ function hide() { var duration = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : getValue(tip.props.duration, 1, Defaults.duration[1]); if (tip.state.isDestroyed || !tip.state.isEnabled) { return; } if (tip.props.onHide(tip) === false) { return; } if (tip.props.updateDuration === 0) { tip.popperChildren.tooltip.classList.remove('tippy-notransition'); } if (tip.props.interactive) { tip.reference.classList.remove('tippy-active'); } tip.popper.style.visibility = 'hidden'; tip.state.isVisible = false; tip.state.isShown = false; applyTransitionDuration([tip.popperChildren.tooltip, tip.popperChildren.backdrop, tip.popperChildren.content], duration); setVisibilityState([tip.popperChildren.tooltip, tip.popperChildren.backdrop, tip.popperChildren.content], 'hidden'); if (tip.props.interactive && !referenceJustProgrammaticallyFocused && ['focus', 'click'].indexOf(lastTriggerEvent.type) > -1) { if (lastTriggerEvent.type === 'focus') { referenceJustProgrammaticallyFocused = true; } focus(tip.reference); } onTransitionedOut(duration, function () { if (!isPreparingToShow) { removeFollowCursorListener(); } tip.reference.removeAttribute('aria-describedby'); tip.popperInstance.disableEventListeners(); tip.props.appendTo.removeChild(tip.popper); tip.state.isMounted = false; tip.props.onHidden(tip); }); } /** * Destroys the tooltip */ function destroy(destroyTargetInstances) { if (tip.state.isDestroyed) { return; } // Ensure the popper is hidden if (tip.state.isVisible) { hide(0); } removeTriggersFromReference(); tip.reference.removeEventListener('click', onReferenceClick); delete tip.reference._tippy; if (tip.props.target && destroyTargetInstances) { toArray$1(tip.reference.querySelectorAll(tip.props.target)).forEach(function (child) { return child._tippy && child._tippy.destroy(); }); } if (tip.popperInstance) { tip.popperInstance.destroy(); } if (popperMutationObserver) { popperMutationObserver.disconnect(); } tip.state.isDestroyed = true; } } var eventListenersBound = false; var useCapture = false; function tippy$1(targets, options, one) { validateOptions(options, Defaults); if (!eventListenersBound) { bindEventListeners(useCapture); eventListenersBound = true; } var props = _extends({}, Defaults, options); /** * If they are specifying a virtual positioning reference, we need to polyfill * some native DOM props */ if (isPlainObject(targets)) { polyfillVirtualReferenceProps(targets); } var references = getArrayOfElements(targets); var firstReference = references[0]; var instances = (one && firstReference ? [firstReference] : references).reduce(function (acc, reference) { var tip = reference && createTippy(reference, props); if (tip) { acc.push(tip); } return acc; }, []); return { targets: targets, props: props, instances: instances, destroyAll: function destroyAll() { this.instances.forEach(function (instance) { instance.destroy(); }); this.instances = []; } }; } /** * Static props */ tippy$1.version = version; tippy$1.defaults = Defaults; /** * Static methods */ tippy$1.one = function (targets, options) { return tippy$1(targets, options, true).instances[0]; }; tippy$1.setDefaults = function (partialDefaults) { setDefaults(partialDefaults); tippy$1.defaults = Defaults; }; tippy$1.disableAnimations = function () { tippy$1.setDefaults({ duration: 0, updateDuration: 0, animateFill: false }); }; tippy$1.hideAllPoppers = hideAllPoppers; tippy$1.useCapture = function () { useCapture = true; }; /** * Auto-init tooltips for elements with a `data-tippy="..."` attribute */ var autoInit = function autoInit() { toArray$1(document.querySelectorAll('[data-tippy]')).forEach(function (el) { var content = el.getAttribute('data-tippy'); if (content) { tippy$1(el, { content: content }); } }); }; if (isBrowser) { setTimeout(autoInit); } return tippy$1; }))); //# sourceMappingURL=tippy.standalone.js.map