UNPKG

tippy.js

Version:
1,751 lines (1,442 loc) 48 kB
/*! * Tippy.js v2.5.2 * (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 = "2.5.2"; var isBrowser = typeof window !== 'undefined'; var isIE = isBrowser && /MSIE |Trident\//.test(navigator.userAgent); var browser = {}; if (isBrowser) { browser.supported = 'requestAnimationFrame' in window; browser.supportsTouch = 'ontouchstart' in window; browser.usingTouch = false; browser.dynamicInputDetection = true; browser.iOS = /iPhone|iPad|iPod/.test(navigator.platform) && !window.MSStream; browser.onUserInputChange = function () {}; } /** * Selector constants used for grabbing elements */ var selectors = { POPPER: '.tippy-popper', TOOLTIP: '.tippy-tooltip', CONTENT: '.tippy-content', BACKDROP: '.tippy-backdrop', ARROW: '.tippy-arrow', ROUND_ARROW: '.tippy-roundarrow', REFERENCE: '[data-tippy]' }; var defaults = { placement: 'top', livePlacement: true, trigger: 'mouseenter focus', animation: 'shift-away', html: false, animateFill: true, arrow: false, delay: 0, duration: [350, 300], interactive: false, interactiveBorder: 2, theme: 'dark', size: 'regular', distance: 10, offset: 0, hideOnClick: true, multiple: false, followCursor: false, inertia: false, updateDuration: 350, sticky: false, appendTo: function appendTo() { return document.body; }, zIndex: 9999, touchHold: false, performance: false, dynamicTitle: false, flip: true, flipBehavior: 'flip', arrowType: 'sharp', arrowTransform: '', maxWidth: '', target: null, allowTitleHTML: true, popperOptions: {}, createPopperInstanceOnInit: false, onShow: function onShow() {}, onShown: function onShown() {}, onHide: function onHide() {}, onHidden: function onHidden() {} }; /** * The keys of the defaults object for reducing down into a new object * Used in `getIndividualOptions()` */ var defaultsKeys = browser.supported && Object.keys(defaults); /** * Determines if a value is an object literal * @param {*} value * @return {Boolean} */ function isObjectLiteral(value) { return {}.toString.call(value) === '[object Object]'; } /** * Ponyfill for Array.from * @param {*} value * @return {Array} */ function toArray(value) { return [].slice.call(value); } /** * Returns an array of elements based on the selector input * @param {String|Element|Element[]|NodeList|Object} selector * @return {Element[]} */ function getArrayOfElements(selector) { if (selector instanceof Element || isObjectLiteral(selector)) { return [selector]; } if (selector instanceof NodeList) { return toArray(selector); } if (Array.isArray(selector)) { return selector; } try { return toArray(document.querySelectorAll(selector)); } catch (_) { return []; } } /** * Polyfills needed props/methods for a virtual reference object * NOTE: in v3.0 this will be pure * @param {Object} reference */ function polyfillVirtualReferenceProps(reference) { reference.refObj = true; reference.attributes = reference.attributes || {}; reference.setAttribute = function (key, val) { reference.attributes[key] = val; }; reference.getAttribute = function (key) { return reference.attributes[key]; }; reference.removeAttribute = function (key) { delete reference.attributes[key]; }; reference.hasAttribute = function (key) { return key in reference.attributes; }; reference.addEventListener = function () {}; reference.removeEventListener = function () {}; reference.classList = { classNames: {}, add: function add(key) { return reference.classList.classNames[key] = true; }, remove: function remove(key) { delete reference.classList.classNames[key]; return true; }, contains: function contains(key) { return key in reference.classList.classNames; } }; } /** * Returns the supported prefixed property - only `webkit` is needed, `moz`, `ms` and `o` are obsolete * @param {String} property * @return {String} - browser supported prefixed property */ function prefix(property) { var prefixes = ['', 'webkit']; var upperProp = property.charAt(0).toUpperCase() + property.slice(1); for (var i = 0; i < prefixes.length; i++) { var _prefix = prefixes[i]; var prefixedProp = _prefix ? _prefix + upperProp : property; if (typeof document.body.style[prefixedProp] !== 'undefined') { return prefixedProp; } } return null; } /** * Creates a div element * @return {Element} */ function div() { return document.createElement('div'); } /** * Creates a popper element then returns it * @param {Number} id - the popper id * @param {String} title - the tooltip's `title` attribute * @param {Object} options - individual options * @return {Element} - the popper element */ function createPopperElement(id, title, options) { var popper = div(); popper.setAttribute('class', 'tippy-popper'); popper.setAttribute('role', 'tooltip'); popper.setAttribute('id', 'tippy-' + id); popper.style.zIndex = options.zIndex; popper.style.maxWidth = options.maxWidth; var tooltip = div(); tooltip.setAttribute('class', 'tippy-tooltip'); tooltip.setAttribute('data-size', options.size); tooltip.setAttribute('data-animation', options.animation); tooltip.setAttribute('data-state', 'hidden'); options.theme.split(' ').forEach(function (t) { tooltip.classList.add(t + '-theme'); }); var content = div(); content.setAttribute('class', 'tippy-content'); if (options.arrow) { var arrow = div(); arrow.style[prefix('transform')] = options.arrowTransform; if (options.arrowType === 'round') { arrow.classList.add('tippy-roundarrow'); arrow.innerHTML = '<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.classList.add('tippy-arrow'); } tooltip.appendChild(arrow); } if (options.animateFill) { // Create animateFill circle element for animation tooltip.setAttribute('data-animatefill', ''); var backdrop = div(); backdrop.classList.add('tippy-backdrop'); backdrop.setAttribute('data-state', 'hidden'); tooltip.appendChild(backdrop); } if (options.inertia) { // Change transition timing function cubic bezier tooltip.setAttribute('data-inertia', ''); } if (options.interactive) { tooltip.setAttribute('data-interactive', ''); } var html = options.html; if (html) { var templateId = void 0; if (html instanceof Element) { content.appendChild(html); templateId = '#' + (html.id || 'tippy-html-template'); } else { // trick linters: https://github.com/atomiks/tippyjs/issues/197 content[true && 'innerHTML'] = document.querySelector(html)[true && 'innerHTML']; templateId = html; } popper.setAttribute('data-html', ''); tooltip.setAttribute('data-template-id', templateId); if (options.interactive) { popper.setAttribute('tabindex', '-1'); } } else { content[options.allowTitleHTML ? 'innerHTML' : 'textContent'] = title; } tooltip.appendChild(content); popper.appendChild(tooltip); return popper; } /** * Creates a trigger by adding the necessary event listeners to the reference element * @param {String} eventType - the custom event specified in the `trigger` setting * @param {Element} reference * @param {Object} handlers - the handlers for each event * @param {Object} options * @return {Array} - array of listener objects */ function createTrigger(eventType, reference, handlers, options) { var onTrigger = handlers.onTrigger, onMouseLeave = handlers.onMouseLeave, onBlur = handlers.onBlur, onDelegateShow = handlers.onDelegateShow, onDelegateHide = handlers.onDelegateHide; var listeners = []; if (eventType === 'manual') return listeners; var on = function on(eventType, handler) { reference.addEventListener(eventType, handler); listeners.push({ event: eventType, handler: handler }); }; if (!options.target) { on(eventType, onTrigger); if (browser.supportsTouch && options.touchHold) { on('touchstart', onTrigger); on('touchend', onMouseLeave); } if (eventType === 'mouseenter') { on('mouseleave', onMouseLeave); } if (eventType === 'focus') { on(isIE ? 'focusout' : 'blur', onBlur); } } else { if (browser.supportsTouch && options.touchHold) { on('touchstart', onDelegateShow); on('touchend', onDelegateHide); } if (eventType === 'mouseenter') { on('mouseover', onDelegateShow); on('mouseout', onDelegateHide); } if (eventType === 'focus') { on('focusin', onDelegateShow); on('focusout', onDelegateHide); } if (eventType === 'click') { on('click', onDelegateShow); } } return listeners; } var classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }; var createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); 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; }; /** * Returns an object of settings to override global settings * @param {Element} reference * @param {Object} instanceOptions * @return {Object} - individual options */ function getIndividualOptions(reference, instanceOptions) { var options = defaultsKeys.reduce(function (acc, key) { var val = reference.getAttribute('data-tippy-' + key.toLowerCase()) || instanceOptions[key]; // Convert strings to booleans if (val === 'false') val = false; if (val === 'true') val = true; // Convert number strings to true numbers if (isFinite(val) && !isNaN(parseFloat(val))) { val = parseFloat(val); } // Convert array strings to actual arrays if (key !== 'target' && typeof val === 'string' && val.trim().charAt(0) === '[') { val = JSON.parse(val); } acc[key] = val; return acc; }, {}); return _extends({}, instanceOptions, options); } /** * Evaluates/modifies the options object for appropriate behavior * @param {Element|Object} reference * @param {Object} options * @return {Object} modified/evaluated options */ function evaluateOptions(reference, options) { // animateFill is disabled if an arrow is true if (options.arrow) { options.animateFill = false; } if (options.appendTo && typeof options.appendTo === 'function') { options.appendTo = options.appendTo(); } if (typeof options.html === 'function') { options.html = options.html(reference); } return options; } /** * Returns inner elements of the popper element * @param {Element} popper * @return {Object} */ function getInnerElements(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) }; } /** * Removes the title from an element, setting `data-original-title` * appropriately * @param {Element} el */ function removeTitle(el) { var title = el.getAttribute('title'); // Only set `data-original-title` attr if there is a title if (title) { el.setAttribute('data-original-title', title); } el.removeAttribute('title'); } /** * Triggers document reflow. * Use void because some minifiers or engines think simply accessing the property * is unnecessary. * @param {Element} popper */ function reflow(popper) { void popper.offsetHeight; } /** * Wrapper util for popper position updating. * Updates the popper's position and invokes the callback on update. * Hackish workaround until Popper 2.0's update() becomes sync. * @param {Popper} popperInstance * @param {Function} callback: to run once popper's position was updated * @param {Boolean} updateAlreadyCalled: was scheduleUpdate() already called? */ function updatePopperPosition(popperInstance, callback, updateAlreadyCalled) { var popper = popperInstance.popper, options = popperInstance.options; var onCreate = options.onCreate; var onUpdate = options.onUpdate; options.onCreate = options.onUpdate = function () { reflow(popper), callback && callback(), onUpdate(); options.onCreate = onCreate; options.onUpdate = onUpdate; }; if (!updateAlreadyCalled) { popperInstance.scheduleUpdate(); } } /** * Returns the core placement ('top', 'bottom', 'left', 'right') of a popper * @param {Element} popper * @return {String} */ function getPopperPlacement(popper) { return popper.getAttribute('x-placement').replace(/-.+/, ''); } /** * Determines if the mouse's cursor is outside the interactive border * @param {MouseEvent} event * @param {Element} popper * @param {Object} options * @return {Boolean} */ function cursorIsOutsideInteractiveBorder(event, popper, options) { if (!popper.getAttribute('x-placement')) return true; var x = event.clientX, y = event.clientY; var interactiveBorder = options.interactiveBorder, distance = options.distance; var rect = popper.getBoundingClientRect(); var placement = getPopperPlacement(popper); var borderWithDistance = interactiveBorder + distance; var exceeds = { top: rect.top - y > interactiveBorder, bottom: y - rect.bottom > interactiveBorder, left: rect.left - x > interactiveBorder, right: x - rect.right > interactiveBorder }; switch (placement) { case 'top': exceeds.top = rect.top - y > borderWithDistance; break; case 'bottom': exceeds.bottom = y - rect.bottom > borderWithDistance; break; case 'left': exceeds.left = rect.left - x > borderWithDistance; break; case 'right': exceeds.right = x - rect.right > borderWithDistance; break; } return exceeds.top || exceeds.bottom || exceeds.left || exceeds.right; } /** * Transforms the `arrowTransform` numbers based on the placement axis * @param {String} type 'scale' or 'translate' * @param {Number[]} numbers * @param {Boolean} isVertical * @param {Boolean} isReverse * @return {String} */ function transformNumbersBasedOnPlacementAxis(type, numbers, isVertical, isReverse) { if (!numbers.length) return ''; var transforms = { scale: function () { if (numbers.length === 1) { return '' + numbers[0]; } else { return isVertical ? numbers[0] + ', ' + numbers[1] : numbers[1] + ', ' + numbers[0]; } }(), translate: function () { if (numbers.length === 1) { return isReverse ? -numbers[0] + 'px' : numbers[0] + 'px'; } else { if (isVertical) { return isReverse ? numbers[0] + 'px, ' + -numbers[1] + 'px' : numbers[0] + 'px, ' + numbers[1] + 'px'; } else { return isReverse ? -numbers[1] + 'px, ' + numbers[0] + 'px' : numbers[1] + 'px, ' + numbers[0] + 'px'; } } }() }; return transforms[type]; } /** * Transforms the `arrowTransform` x or y axis based on the placement axis * @param {String} axis 'X', 'Y', '' * @param {Boolean} isVertical * @return {String} */ function transformAxis(axis, isVertical) { if (!axis) return ''; var map = { X: 'Y', Y: 'X' }; return isVertical ? axis : map[axis]; } /** * Computes and applies the necessary arrow transform * @param {Element} popper * @param {Element} arrow * @param {String} arrowTransform */ function computeArrowTransform(popper, arrow, arrowTransform) { var placement = getPopperPlacement(popper); var isVertical = placement === 'top' || placement === 'bottom'; var isReverse = placement === 'right' || placement === 'bottom'; var getAxis = function getAxis(re) { var match = arrowTransform.match(re); return match ? match[1] : ''; }; var getNumbers = function getNumbers(re) { var match = arrowTransform.match(re); return match ? match[1].split(',').map(parseFloat) : []; }; var re = { translate: /translateX?Y?\(([^)]+)\)/, scale: /scaleX?Y?\(([^)]+)\)/ }; var matches = { translate: { axis: getAxis(/translate([XY])/), numbers: getNumbers(re.translate) }, scale: { axis: getAxis(/scale([XY])/), numbers: getNumbers(re.scale) } }; var computedTransform = arrowTransform.replace(re.translate, 'translate' + transformAxis(matches.translate.axis, isVertical) + '(' + transformNumbersBasedOnPlacementAxis('translate', matches.translate.numbers, isVertical, isReverse) + ')').replace(re.scale, 'scale' + transformAxis(matches.scale.axis, isVertical) + '(' + transformNumbersBasedOnPlacementAxis('scale', matches.scale.numbers, isVertical, isReverse) + ')'); arrow.style[prefix('transform')] = computedTransform; } /** * Returns the distance taking into account the default distance due to * the transform: translate setting in CSS * @param {Number} distance * @return {String} */ function getOffsetDistanceInPx(distance) { return -(distance - defaults.distance) + 'px'; } /** * Waits until next repaint to execute a fn * @param {Function} fn */ function defer(fn) { requestAnimationFrame(function () { setTimeout(fn, 1); }); } var matches = {}; if (isBrowser) { var e = Element.prototype; matches = e.matches || e.matchesSelector || e.webkitMatchesSelector || e.mozMatchesSelector || e.msMatchesSelector || function (s) { var matches = (this.document || this.ownerDocument).querySelectorAll(s); var i = matches.length; while (--i >= 0 && matches.item(i) !== this) {} // eslint-disable-line no-empty return i > -1; }; } var matches$1 = matches; /** * Ponyfill to get the closest parent element * @param {Element} element - child of parent to be returned * @param {String} parentSelector - selector to match the parent if found * @return {Element} */ function closest(element, parentSelector) { var fn = Element.prototype.closest || function (selector) { var el = this; while (el) { if (matches$1.call(el, selector)) { return el; } el = el.parentElement; } }; return fn.call(element, parentSelector); } /** * Returns the value taking into account the value being either a number or array * @param {Number|Array} value * @param {Number} index * @return {Number} */ function getValue(value, index) { return Array.isArray(value) ? value[index] : value; } /** * Sets the visibility state of an element for transition to begin * @param {Element[]} els - array of elements * @param {String} type - 'visible' or 'hidden' */ function setVisibilityState(els, type) { els.forEach(function (el) { if (!el) return; el.setAttribute('data-state', type); }); } /** * Sets the transition property to each element * @param {Element[]} els - Array of elements * @param {String} value */ function applyTransitionDuration(els, value) { els.filter(Boolean).forEach(function (el) { el.style[prefix('transitionDuration')] = value + 'ms'; }); } /** * Focuses an element while preventing a scroll jump if it's not entirely within the viewport * @param {Element} el */ function focus(el) { var x = window.scrollX || window.pageXOffset; var y = window.scrollY || window.pageYOffset; el.focus(); scroll(x, y); } var key = {}; var store = function store(data) { return function (k) { return k === key && data; }; }; var Tippy = function () { function Tippy(config) { classCallCheck(this, Tippy); for (var _key in config) { this[_key] = config[_key]; } this.state = { destroyed: false, visible: false, enabled: true }; this._ = store({ mutationObservers: [] }); } /** * Enables the tooltip to allow it to show or hide * @memberof Tippy * @public */ createClass(Tippy, [{ key: 'enable', value: function enable() { this.state.enabled = true; } /** * Disables the tooltip from showing or hiding, but does not destroy it * @memberof Tippy * @public */ }, { key: 'disable', value: function disable() { this.state.enabled = false; } /** * Shows the tooltip * @param {Number} duration in milliseconds * @memberof Tippy * @public */ }, { key: 'show', value: function show(duration) { var _this = this; if (this.state.destroyed || !this.state.enabled) return; var popper = this.popper, reference = this.reference, options = this.options; var _getInnerElements = getInnerElements(popper), tooltip = _getInnerElements.tooltip, backdrop = _getInnerElements.backdrop, content = _getInnerElements.content; // If the `dynamicTitle` option is true, the instance is allowed // to be created with an empty title. Make sure that the tooltip // content is not empty before showing it if (options.dynamicTitle && !reference.getAttribute('data-original-title')) return; // Do not show tooltip if reference contains 'disabled' attribute. FF fix for #221 if (reference.hasAttribute('disabled')) return; // Destroy tooltip if the reference element is no longer on the DOM if (!reference.refObj && !document.documentElement.contains(reference)) { this.destroy(); return; } options.onShow.call(popper, this); duration = getValue(duration !== undefined ? duration : options.duration, 0); // Prevent a transition when popper changes position applyTransitionDuration([popper, tooltip, backdrop], 0); popper.style.visibility = 'visible'; this.state.visible = true; _mount.call(this, function () { if (!_this.state.visible) return; if (!_hasFollowCursorBehavior.call(_this)) { // FIX: Arrow will sometimes not be positioned correctly. Force another update. _this.popperInstance.scheduleUpdate(); } // Set initial position near the cursor if (_hasFollowCursorBehavior.call(_this)) { _this.popperInstance.disableEventListeners(); var delay = getValue(options.delay, 0); var lastTriggerEvent = _this._(key).lastTriggerEvent; if (lastTriggerEvent) { _this._(key).followCursorListener(delay && _this._(key).lastMouseMoveEvent ? _this._(key).lastMouseMoveEvent : lastTriggerEvent); } } // Re-apply transition durations applyTransitionDuration([tooltip, backdrop, backdrop ? content : null], duration); if (backdrop) { getComputedStyle(backdrop)[prefix('transform')]; } if (options.interactive) { reference.classList.add('tippy-active'); } if (options.sticky) { _makeSticky.call(_this); } setVisibilityState([tooltip, backdrop], 'visible'); _onTransitionEnd.call(_this, duration, function () { if (!options.updateDuration) { tooltip.classList.add('tippy-notransition'); } if (options.interactive) { focus(popper); } reference.setAttribute('aria-describedby', 'tippy-' + _this.id); options.onShown.call(popper, _this); }); }); } /** * Hides the tooltip * @param {Number} duration in milliseconds * @memberof Tippy * @public */ }, { key: 'hide', value: function hide(duration) { var _this2 = this; if (this.state.destroyed || !this.state.enabled) return; var popper = this.popper, reference = this.reference, options = this.options; var _getInnerElements2 = getInnerElements(popper), tooltip = _getInnerElements2.tooltip, backdrop = _getInnerElements2.backdrop, content = _getInnerElements2.content; options.onHide.call(popper, this); duration = getValue(duration !== undefined ? duration : options.duration, 1); if (!options.updateDuration) { tooltip.classList.remove('tippy-notransition'); } if (options.interactive) { reference.classList.remove('tippy-active'); } popper.style.visibility = 'hidden'; this.state.visible = false; applyTransitionDuration([tooltip, backdrop, backdrop ? content : null], duration); setVisibilityState([tooltip, backdrop], 'hidden'); if (options.interactive && options.trigger.indexOf('click') > -1) { focus(reference); } this.popperInstance.disableEventListeners(); /* * This call is deferred because sometimes when the tooltip is still transitioning in but hide() * is called before it finishes, the CSS transition won't reverse quickly enough, meaning * the CSS transition will finish 1-2 frames later, and onHidden() will run since the JS set it * more quickly. It should actually be onShown(). Seems to be something Chrome does, not Safari */ defer(function () { _onTransitionEnd.call(_this2, duration, function () { if (_this2.state.visible || !options.appendTo.contains(popper)) return; if (!_this2._(key).isPreparingToShow) { document.removeEventListener('mousemove', _this2._(key).followCursorListener); _this2._(key).lastMouseMoveEvent = null; } reference.removeAttribute('aria-describedby'); options.appendTo.removeChild(popper); options.onHidden.call(popper, _this2); }); }); } /** * Destroys the tooltip instance * @param {Boolean} destroyTargetInstances - relevant only when destroying delegates * @memberof Tippy * @public */ }, { key: 'destroy', value: function destroy() { var _this3 = this; var destroyTargetInstances = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : true; if (this.state.destroyed) return; // Ensure the popper is hidden if (this.state.visible) { this.hide(0); } this.listeners.forEach(function (listener) { _this3.reference.removeEventListener(listener.event, listener.handler); }); // Restore title if (this.title) { this.reference.setAttribute('title', this.title); } delete this.reference._tippy; var attributes = ['data-original-title', 'data-tippy', 'data-tippy-delegate']; attributes.forEach(function (attr) { _this3.reference.removeAttribute(attr); }); if (this.options.target && destroyTargetInstances) { toArray(this.reference.querySelectorAll(this.options.target)).forEach(function (child) { return child._tippy && child._tippy.destroy(); }); } if (this.popperInstance) { this.popperInstance.destroy(); } this._(key).mutationObservers.forEach(function (observer) { observer.disconnect(); }); this.state.destroyed = true; } }]); return Tippy; }(); /** * ------------------------------------------------------------------------ * Private methods * ------------------------------------------------------------------------ * Standalone functions to be called with the instance's `this` context to make * them truly private and not accessible on the prototype */ /** * Determines if the tooltip instance has followCursor behavior * @return {Boolean} * @memberof Tippy * @private */ function _hasFollowCursorBehavior() { var lastTriggerEvent = this._(key).lastTriggerEvent; return this.options.followCursor && !browser.usingTouch && lastTriggerEvent && lastTriggerEvent.type !== 'focus'; } /** * Creates the Tippy instance for the child target of the delegate container * @param {Event} event * @memberof Tippy * @private */ function _createDelegateChildTippy(event) { var targetEl = closest(event.target, this.options.target); if (targetEl && !targetEl._tippy) { var title = targetEl.getAttribute('title') || this.title; if (title) { targetEl.setAttribute('title', title); tippy$1(targetEl, _extends({}, this.options, { target: null })); _enter.call(targetEl._tippy, event); } } } /** * Method used by event listeners to invoke the show method, taking into account delays and * the `wait` option * @param {Event} event * @memberof Tippy * @private */ function _enter(event) { var _this4 = this; var options = this.options; _clearDelayTimeouts.call(this); if (this.state.visible) return; // Is a delegate, create Tippy instance for the child target if (options.target) { _createDelegateChildTippy.call(this, event); return; } this._(key).isPreparingToShow = true; if (options.wait) { options.wait.call(this.popper, this.show.bind(this), event); return; } // 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.call(this)) { if (!this._(key).followCursorListener) { _setFollowCursorListener.call(this); } var _getInnerElements3 = getInnerElements(this.popper), arrow = _getInnerElements3.arrow; if (arrow) arrow.style.margin = '0'; document.addEventListener('mousemove', this._(key).followCursorListener); } var delay = getValue(options.delay, 0); if (delay) { this._(key).showTimeout = setTimeout(function () { _this4.show(); }, delay); } else { this.show(); } } /** * Method used by event listeners to invoke the hide method, taking into account delays * @memberof Tippy * @private */ function _leave() { var _this5 = this; _clearDelayTimeouts.call(this); if (!this.state.visible) return; this._(key).isPreparingToShow = false; var delay = getValue(this.options.delay, 1); if (delay) { this._(key).hideTimeout = setTimeout(function () { if (_this5.state.visible) { _this5.hide(); } }, delay); } else { this.hide(); } } /** * Returns relevant listeners for the instance * @return {Object} of listeners * @memberof Tippy * @private */ function _getEventListeners() { var _this6 = this; var onTrigger = function onTrigger(event) { if (!_this6.state.enabled) return; var shouldStopEvent = browser.supportsTouch && browser.usingTouch && ['mouseenter', 'mouseover', 'focus'].indexOf(event.type) > -1; if (shouldStopEvent && _this6.options.touchHold) return; _this6._(key).lastTriggerEvent = event; // Toggle show/hide when clicking click-triggered tooltips if (event.type === 'click' && _this6.options.hideOnClick !== 'persistent' && _this6.state.visible) { _leave.call(_this6); } else { _enter.call(_this6, event); } // iOS prevents click events from firing if (shouldStopEvent && browser.iOS && _this6.reference.click) { _this6.reference.click(); } }; var onMouseLeave = function onMouseLeave(event) { if (['mouseleave', 'mouseout'].indexOf(event.type) > -1 && browser.supportsTouch && browser.usingTouch && _this6.options.touchHold) return; if (_this6.options.interactive) { var hide = _leave.bind(_this6); var onMouseMove = function onMouseMove(event) { var referenceCursorIsOver = closest(event.target, selectors.REFERENCE); var cursorIsOverPopper = closest(event.target, selectors.POPPER) === _this6.popper; var cursorIsOverReference = referenceCursorIsOver === _this6.reference; if (cursorIsOverPopper || cursorIsOverReference) return; if (cursorIsOutsideInteractiveBorder(event, _this6.popper, _this6.options)) { document.body.removeEventListener('mouseleave', hide); document.removeEventListener('mousemove', onMouseMove); _leave.call(_this6, onMouseMove); } }; document.body.addEventListener('mouseleave', hide); document.addEventListener('mousemove', onMouseMove); return; } _leave.call(_this6); }; var onBlur = function onBlur(event) { if (event.target !== _this6.reference || browser.usingTouch) return; if (_this6.options.interactive) { if (!event.relatedTarget) return; if (closest(event.relatedTarget, selectors.POPPER)) return; } _leave.call(_this6); }; var onDelegateShow = function onDelegateShow(event) { if (closest(event.target, _this6.options.target)) { _enter.call(_this6, event); } }; var onDelegateHide = function onDelegateHide(event) { if (closest(event.target, _this6.options.target)) { _leave.call(_this6); } }; return { onTrigger: onTrigger, onMouseLeave: onMouseLeave, onBlur: onBlur, onDelegateShow: onDelegateShow, onDelegateHide: onDelegateHide }; } /** * Creates and returns a new popper instance * @return {Popper} * @memberof Tippy * @private */ function _createPopperInstance() { var _this7 = this; var popper = this.popper, reference = this.reference, options = this.options; var _getInnerElements4 = getInnerElements(popper), tooltip = _getInnerElements4.tooltip; var popperOptions = options.popperOptions; var arrowSelector = options.arrowType === 'round' ? selectors.ROUND_ARROW : selectors.ARROW; var arrow = tooltip.querySelector(arrowSelector); var config = _extends({ placement: options.placement }, popperOptions || {}, { modifiers: _extends({}, popperOptions ? popperOptions.modifiers : {}, { arrow: _extends({ element: arrowSelector }, popperOptions && popperOptions.modifiers ? popperOptions.modifiers.arrow : {}), flip: _extends({ enabled: options.flip, padding: options.distance + 5 /* 5px from viewport boundary */ , behavior: options.flipBehavior }, popperOptions && popperOptions.modifiers ? popperOptions.modifiers.flip : {}), offset: _extends({ offset: options.offset }, popperOptions && popperOptions.modifiers ? popperOptions.modifiers.offset : {}) }), onCreate: function onCreate() { tooltip.style[getPopperPlacement(popper)] = getOffsetDistanceInPx(options.distance); if (arrow && options.arrowTransform) { computeArrowTransform(popper, arrow, options.arrowTransform); } }, onUpdate: function onUpdate() { var styles = tooltip.style; styles.top = ''; styles.bottom = ''; styles.left = ''; styles.right = ''; styles[getPopperPlacement(popper)] = getOffsetDistanceInPx(options.distance); if (arrow && options.arrowTransform) { computeArrowTransform(popper, arrow, options.arrowTransform); } } }); _addMutationObserver.call(this, { target: popper, callback: function callback() { _this7.popperInstance.update(); }, options: { childList: true, subtree: true, characterData: true } }); return new Popper(reference, popper, config); } /** * Appends the popper element to the DOM, updating or creating the popper instance * @param {Function} callback * @memberof Tippy * @private */ function _mount(callback) { var options = this.options; if (!this.popperInstance) { this.popperInstance = _createPopperInstance.call(this); if (!options.livePlacement) { this.popperInstance.disableEventListeners(); } } else { this.popperInstance.scheduleUpdate(); if (options.livePlacement && !_hasFollowCursorBehavior.call(this)) { this.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 if (!_hasFollowCursorBehavior.call(this)) { var _getInnerElements5 = getInnerElements(this.popper), arrow = _getInnerElements5.arrow; if (arrow) arrow.style.margin = ''; this.popperInstance.reference = this.reference; } updatePopperPosition(this.popperInstance, callback, true); if (!options.appendTo.contains(this.popper)) { options.appendTo.appendChild(this.popper); } } /** * Clears the show and hide delay timeouts * @memberof Tippy * @private */ function _clearDelayTimeouts() { var _ref = this._(key), showTimeout = _ref.showTimeout, hideTimeout = _ref.hideTimeout; clearTimeout(showTimeout); clearTimeout(hideTimeout); } /** * Sets the mousemove event listener function for `followCursor` option * @memberof Tippy * @private */ function _setFollowCursorListener() { var _this8 = this; this._(key).followCursorListener = function (event) { var _$lastMouseMoveEvent = _this8._(key).lastMouseMoveEvent = event, clientX = _$lastMouseMoveEvent.clientX, clientY = _$lastMouseMoveEvent.clientY; if (!_this8.popperInstance) return; _this8.popperInstance.reference = { getBoundingClientRect: function getBoundingClientRect() { return { width: 0, height: 0, top: clientY, left: clientX, right: clientX, bottom: clientY }; }, clientWidth: 0, clientHeight: 0 }; _this8.popperInstance.scheduleUpdate(); }; } /** * Updates the popper's position on each animation frame * @memberof Tippy * @private */ function _makeSticky() { var _this9 = this; var applyTransitionDuration$$1 = function applyTransitionDuration$$1() { _this9.popper.style[prefix('transitionDuration')] = _this9.options.updateDuration + 'ms'; }; var removeTransitionDuration = function removeTransitionDuration() { _this9.popper.style[prefix('transitionDuration')] = ''; }; var updatePosition = function updatePosition() { if (_this9.popperInstance) { _this9.popperInstance.update(); } applyTransitionDuration$$1(); if (_this9.state.visible) { requestAnimationFrame(updatePosition); } else { removeTransitionDuration(); } }; updatePosition(); } /** * Adds a mutation observer to an element and stores it in the instance * @param {Object} * @memberof Tippy * @private */ function _addMutationObserver(_ref2) { var target = _ref2.target, callback = _ref2.callback, options = _ref2.options; if (!window.MutationObserver) return; var observer = new MutationObserver(callback); observer.observe(target, options); this._(key).mutationObservers.push(observer); } /** * Fires the callback functions once the CSS transition ends for `show` and `hide` methods * @param {Number} duration * @param {Function} callback - callback function to fire once transition completes * @memberof Tippy * @private */ function _onTransitionEnd(duration, callback) { // Make callback synchronous if duration is 0 if (!duration) { return callback(); } var _getInnerElements6 = getInnerElements(this.popper), tooltip = _getInnerElements6.tooltip; var toggleListeners = function toggleListeners(action, listener) { if (!listener) return; tooltip[action + 'EventListener']('ontransitionend' in window ? 'transitionend' : 'webkitTransitionEnd', listener); }; var listener = function listener(e) { if (e.target === tooltip) { toggleListeners('remove', listener); callback(); } }; toggleListeners('remove', this._(key).transitionendListener); toggleListeners('add', listener); this._(key).transitionendListener = listener; } var idCounter = 1; /** * Creates tooltips for each reference element * @param {Element[]} els * @param {Object} config * @return {Tippy[]} Array of Tippy instances */ function createTooltips(els, config) { return els.reduce(function (acc, reference) { var id = idCounter; var options = evaluateOptions(reference, config.performance ? config : getIndividualOptions(reference, config)); var title = reference.getAttribute('title'); // Don't create an instance when: // * the `title` attribute is falsy (null or empty string), and // * it's not a delegate for tooltips, and // * there is no html template specified, and // * `dynamicTitle` option is false if (!title && !options.target && !options.html && !options.dynamicTitle) { return acc; } // Delegates should be highlighted as different reference.setAttribute(options.target ? 'data-tippy-delegate' : 'data-tippy', ''); removeTitle(reference); var popper = createPopperElement(id, title, options); var tippy = new Tippy({ id: id, reference: reference, popper: popper, options: options, title: title, popperInstance: null }); if (options.createPopperInstanceOnInit) { tippy.popperInstance = _createPopperInstance.call(tippy); tippy.popperInstance.disableEventListeners(); } var listeners = _getEventListeners.call(tippy); tippy.listeners = options.trigger.trim().split(' ').reduce(function (acc, eventType) { return acc.concat(createTrigger(eventType, reference, listeners, options)); }, []); // Update tooltip content whenever the title attribute on the reference changes if (options.dynamicTitle) { _addMutationObserver.call(tippy, { target: reference, callback: function callback() { var _getInnerElements = getInnerElements(popper), content = _getInnerElements.content; var title = reference.getAttribute('title'); if (title) { content[options.allowTitleHTML ? 'innerHTML' : 'textContent'] = tippy.title = title; removeTitle(reference); } }, options: { attributes: true } }); } // Shortcuts reference._tippy = tippy; popper._tippy = tippy; popper._reference = reference; acc.push(tippy); idCounter++; return acc; }, []); } /** * Hides all poppers * @param {Tippy} excludeTippy - tippy to exclude if needed */ function hideAllPoppers(excludeTippy) { var poppers = toArray(document.querySelectorAll(selectors.POPPER)); poppers.forEach(function (popper) { var tippy = popper._tippy; if (!tippy) return; var options = tippy.options; if ((options.hideOnClick === true || options.trigger.indexOf('focus') > -1) && (!excludeTippy || popper !== excludeTippy.popper)) { tippy.hide(); } }); } /** * Adds the needed event listeners */ function bindEventListeners() { var onDocumentTouch = function onDocumentTouch() { if (browser.usingTouch) return; browser.usingTouch = true; if (browser.iOS) { document.body.classList.add('tippy-touch'); } if (browser.dynamicInputDetection && window.performance) { document.addEventListener('mousemove', onDocumentMouseMove); } browser.onUserInputChange('touch'); }; var onDocumentMouseMove = function () { var time = void 0; return function () { var now = performance.now(); // Chrome 60+ is 1 mousemove per animation frame, use 20ms time difference if (now - time < 20) { browser.usingTouch = false; document.removeEventListener('mousemove', onDocumentMouseMove); if (!browser.iOS) { document.body.classList.remove('tippy-touch'); } browser.onUserInputChange('mouse'); } time = now; }; }(); var onDocumentClick = function onDocumentClick(event) { // Simulated events dispatched on the document if (!(event.target instanceof Element)) { return hideAllPoppers(); } var reference = closest(event.target, selectors.REFERENCE); var popper = closest(event.target, selectors.POPPER); if (popper && popper._tippy && popper._tippy.options.interactive) { return; } if (reference && reference._tippy) { var options = reference._tippy.options; var isClickTrigger = options.trigger.indexOf('click') > -1; var isMultiple = options.multiple; // Hide all poppers except the one belonging to the element that was clicked if (!isMultiple && browser.usingTouch || !isMultiple && isClickTrigger) { return hideAllPoppers(reference._tippy); } if (options.hideOnClick !== true || isClickTrigger) { return; } } hideAllPoppers(); }; var onWindowBlur = function onWindowBlur() { var _document = document, el = _document.activeElement; if (el && el.blur && matches$1.call(el, selectors.REFERENCE)) { el.blur(); } }; var onWindowResize = function onWindowResize() { toArray(document.querySelectorAll(selectors.POPPER)).forEach(function (popper) { var tippyInstance = popper._tippy; if (tippyInstance && !tippyInstance.options.livePlacement) { tippyInstance.popperInstance.scheduleUpdate(); } }); }; document.addEventListener('click', onDocumentClick); document.addEventListener('touchstart', onDocumentTouch); window.addEventListener('blur', onWindowBlur); window.addEventListener('resize', onWindowResize); if (!browser.supportsTouch && (navigator.maxTouchPoints || navigator.msMaxTouchPoints)) { document.addEventListener('pointerdown', onDocumentTouch); } } var eventListenersBound = false; /** * Exported module * @param {String|Element|Element[]|NodeList|Object} selector * @param {Object} options * @param {Boolean} one - create one tooltip * @return {Object} */ function tippy$1(selector, options, one) { if (browser.supported && !eventListenersBound) { bindEventListeners(); eventListenersBound = true; } if (isObjectLiteral(selector)) { polyfillVirtualReferenceProps(selector); } options = _extends({}, defaults, options); var references = getArrayOfElements(selector); var firstReference = references[0]; return { selector: selector, options: options, tooltips: browser.supported ? createTooltips(one && firstReference ? [firstReference] : references, options) : [], destroyAll: function destroyAll() { this.tooltips.forEach(function (tooltip) { return tooltip.destroy(); }); this.tooltips = []; } }; } tippy$1.version = version; tippy$1.browser = browser; tippy$1.defaults = defaults; tippy$1.one = function (selector, options) { return tippy$1(selector, options, true).tooltips[0]; }; tippy$1.disableAnimations = function () { defaults.updateDuration = defaults.duration = 0; defaults.animateFill = false; }; return tippy$1; })));