tippy.js
Version:
Vanilla JS Tooltip Library
1,751 lines (1,442 loc) • 48 kB
JavaScript
/*!
* 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;
})));