/**! * @fileOverview Kickass library to create and place poppers near their reference elements. * @version 1.0.4 * @license * Copyright (c) 2016 Federico Zivolo and contributors * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in all * copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ (function (global, factory) { typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : typeof define === 'function' && define.amd ? define(factory) : (global.Popper = factory()); }(this, (function () { 'use strict'; /** * Returns the offset parent of the given element * @method * @memberof Popper.Utils * @argument {Element} element * @returns {Element} offset parent */ function getOffsetParent(element) { // NOTE: 1 DOM access here const offsetParent = element.offsetParent; const nodeName = offsetParent && offsetParent.nodeName; if (!nodeName || nodeName === 'BODY' || nodeName === 'HTML') { return window.document.documentElement; } return offsetParent; } /** * Get CSS computed property of the given element * @method * @memberof Popper.Utils * @argument {Eement} element * @argument {String} property */ function getStyleComputedProperty(element, property) { if (element.nodeType !== 1) { return []; } // NOTE: 1 DOM access here const css = window.getComputedStyle(element, null); return property ? css[property] : css; } /** * Returns the parentNode or the host of the element * @method * @memberof Popper.Utils * @argument {Element} element * @returns {Element} parent */ function getParentNode(element) { if (element.nodeName === 'HTML') { return element; } return element.parentNode || element.host; } /** * Returns the scrolling parent of the given element * @method * @memberof Popper.Utils * @argument {Element} element * @returns {Element} offset parent */ function getScrollParent(element) { // Return body, `getScroll` will take care to get the correct `scrollTop` from it if (!element || ['HTML', 'BODY', '#document'].indexOf(element.nodeName) !== -1) { return window.document.body; } // Firefox want us to check `-x` and `-y` variations as well const { overflow, overflowX, overflowY } = getStyleComputedProperty(element); if (/(auto|scroll)/.test(overflow + overflowY + overflowX)) { return element; } return getScrollParent(getParentNode(element)); } function getWindowSizes() { const body = window.document.body; const html = window.document.documentElement; return { height: Math.max(body.scrollHeight, body.offsetHeight, html.clientHeight, html.scrollHeight, html.offsetHeight), width: Math.max(body.scrollWidth, body.offsetWidth, html.clientWidth, html.scrollWidth, html.offsetWidth) }; } /** * Get the position of the given element, relative to its offset parent * @method * @memberof Popper.Utils * @param {Element} element * @return {Object} position - Coordinates of the element and its `scrollTop` */ function getOffsetRect(element) { let elementRect; if (element.nodeName === 'HTML') { const { width, height } = getWindowSizes(); elementRect = { width, height, left: 0, top: 0 }; } else { elementRect = { width: element.offsetWidth, height: element.offsetHeight, left: element.offsetLeft, top: element.offsetTop }; } elementRect.right = elementRect.left + elementRect.width; elementRect.bottom = elementRect.top + elementRect.height; // position return elementRect; } /** * Check if the given element is fixed or is inside a fixed parent * @method * @memberof Popper.Utils * @argument {Element} element * @argument {Element} customContainer * @returns {Boolean} answer to "isFixed?" */ function isFixed(element) { const nodeName = element.nodeName; if (nodeName === 'BODY' || nodeName === 'HTML') { return false; } if (getStyleComputedProperty(element, 'position') === 'fixed') { return true; } return isFixed(getParentNode(element)); } /** * Helper used to get the position which will be applied to the popper * @method * @memberof Popper.Utils * @param config {HTMLElement} popper element * @returns {HTMLElement} reference element */ function getPosition(element) { const container = getOffsetParent(element); // Decide if the popper will be fixed // If the reference element is inside a fixed context, the popper will be fixed as well to allow them to scroll together const isParentFixed = isFixed(container); return isParentFixed ? 'fixed' : 'absolute'; } /** * Get bounding client rect of given element * @method * @memberof Popper.Utils * @param {HTMLElement} element * @return {Object} client rect */ function getBoundingClientRect(element) { const isIE10 = navigator.appVersion.indexOf('MSIE 10') !== -1; let rect; // IE10 10 FIX: Please, don't ask, the element isn't // considered in DOM in some circumstances... // This isn't reproducible in IE10 compatibility mode of IE11 if (isIE10) { try { rect = element.getBoundingClientRect(); } catch (err) { rect = {}; } } else { rect = element.getBoundingClientRect(); } const result = { left: rect.left, top: rect.top, right: rect.right, bottom: rect.bottom, width: rect.right - rect.left, height: rect.bottom - rect.top }; // IE10 FIX: `getBoundingClientRect`, when executed on `documentElement` // will not take in account the `scrollTop` and `scrollLeft` if (element.nodeName === 'HTML' && isIE10) { const { scrollTop, scrollLeft } = window.document.documentElement; result.top -= scrollTop; result.bottom -= scrollTop; result.left -= scrollLeft; result.right -= scrollLeft; } // subtract scrollbar size from sizes let horizScrollbar = rect.width - (element.clientWidth || rect.right - rect.left); let vertScrollbar = rect.height - (element.clientHeight || rect.bottom - rect.top); // if an hypothetical scrollbar is detected, we must be sure it's not a `border` // we make this check conditional for performance reasons if (horizScrollbar || vertScrollbar) { const styles = getStyleComputedProperty(element); horizScrollbar -= Number(styles.borderLeftWidth.split('px')[0]) + Number(styles.borderRightWidth.split('px')[0]); vertScrollbar -= Number(styles.borderTopWidth.split('px')[0]) + Number(styles.borderBottomWidth.split('px')[0]); } result.right -= horizScrollbar; result.width -= horizScrollbar; result.bottom -= vertScrollbar; result.height -= vertScrollbar; return result; } function getScroll(element, side = 'top') { const upperSide = side === 'top' ? 'scrollTop' : 'scrollLeft'; const nodeName = element.nodeName; if (nodeName === 'BODY' || nodeName === 'HTML') { const html = window.document.documentElement; const scrollingElement = window.document.scrollingElement || html; return scrollingElement[upperSide]; } return element[upperSide]; } /** * Given an element and one of its parents, return the offset * @method * @memberof Popper.Utils * @param {HTMLElement} element * @param {HTMLElement} parent * @return {Object} rect */ function getOffsetRectRelativeToCustomParent(element, parent, fixed = false, transformed = false) { const scrollParent = getScrollParent(parent); const elementRect = getBoundingClientRect(element); const parentRect = getBoundingClientRect(parent); const rect = { top: elementRect.top - parentRect.top, left: elementRect.left - parentRect.left, bottom: elementRect.top - parentRect.top + elementRect.height, right: elementRect.left - parentRect.left + elementRect.width, width: elementRect.width, height: elementRect.height }; if (fixed && !transformed) { const scrollTop = getScroll(scrollParent, 'top'); const scrollLeft = getScroll(scrollParent, 'left'); rect.top -= scrollTop; rect.bottom -= scrollTop; rect.left -= scrollLeft; rect.right -= scrollLeft; } // When a popper doesn't have any positioned or scrollable parents, `offsetParent.contains(scrollParent)` // will return a "false positive". This is happening because `getOffsetParent` returns `html` node, // and `scrollParent` is the `body` node. Hence the additional check. else if (getOffsetParent(element).contains(scrollParent) && scrollParent.nodeName !== 'BODY') { const scrollTop = getScroll(parent, 'top'); const scrollLeft = getScroll(parent, 'left'); rect.top += scrollTop; rect.bottom += scrollTop; rect.left += scrollLeft; rect.right += scrollLeft; } // subtract borderTopWidth and borderTopWidth from final result const styles = getStyleComputedProperty(parent); const borderTopWidth = Number(styles.borderTopWidth.split('px')[0]); const borderLeftWidth = Number(styles.borderLeftWidth.split('px')[0]); rect.top -= borderTopWidth; rect.bottom -= borderTopWidth; rect.left -= borderLeftWidth; rect.right -= borderLeftWidth; return rect; } function getTotalScroll(element, side = 'top') { const scrollParent = getScrollParent(element); const scroll = getScroll(scrollParent, side); if (['BODY', 'HTML'].indexOf(scrollParent.nodeName) === -1) { return scroll + getTotalScroll(getParentNode(scrollParent), side); } return scroll; } /** * Computed the boundaries limits and return them * @method * @memberof Popper.Utils * @param {Object} data - Object containing the property "offsets" generated by `_getOffsets` * @param {Number} padding - Boundaries padding * @param {Element} boundariesElement - Element used to define the boundaries * @returns {Object} Coordinates of the boundaries */ function getBoundaries(popper, padding, boundariesElement) { // NOTE: 1 DOM access here let boundaries = { top: 0, left: 0 }; const offsetParent = getOffsetParent(popper); // Handle viewport case if (boundariesElement === 'viewport') { const { left, top } = getOffsetRect(offsetParent); const { clientWidth: width, clientHeight: height } = window.document.documentElement; if (getPosition(popper) === 'fixed') { boundaries.right = width; boundaries.bottom = height; } else { const scrollLeft = getTotalScroll(popper, 'left'); const scrollTop = getTotalScroll(popper, 'top'); boundaries = { top: 0 - top, right: width - left + scrollLeft, bottom: height - top + scrollTop, left: 0 - left }; } } // Handle other cases based on DOM element used as boundaries else { let boundariesNode; if (boundariesElement === 'scrollParent') { boundariesNode = getScrollParent(getParentNode(popper)); } else if (boundariesElement === 'window') { boundariesNode = window.document.body; } else { boundariesNode = boundariesElement; } // In case of BODY, we need a different computation if (boundariesNode.nodeName === 'BODY') { const { height, width } = getWindowSizes(); boundaries.right = width; boundaries.bottom = height; } // for all the other DOM elements, this one is good else { boundaries = getOffsetRectRelativeToCustomParent(boundariesNode, offsetParent, isFixed(popper)); } } // Add paddings boundaries.left += padding; boundaries.top += padding; boundaries.right -= padding; boundaries.bottom -= padding; return boundaries; } /** * Utility used to transform the `auto` placement to the placement with more * available space. * @method * @memberof Popper.Utils * @argument {Object} data - The data object generated by update method * @argument {Object} options - Modifiers configuration and options * @returns {Object} The data object, properly modified */ function computeAutoPlacement(placement, refRect, popper) { if (placement.indexOf('auto') === -1) { return placement; } const boundaries = getBoundaries(popper, 0, 'scrollParent'); const sides = { top: refRect.top - boundaries.top, right: boundaries.right - refRect.right, bottom: boundaries.bottom - refRect.bottom, left: refRect.left - boundaries.left }; const computedPlacement = Object.keys(sides).sort((a, b) => sides[b] - sides[a])[0]; const variation = placement.split('-')[1]; return computedPlacement + (variation ? `-${ variation }` : ''); } const nativeHints = ['native code', '[object MutationObserverConstructor]' // for mobile safari iOS 9.0 ]; /** * Determine if a function is implemented natively (as opposed to a polyfill). * @argument {Function | undefined} fn the function to check * @returns {boolean} */ var isNative = (fn => nativeHints.some(hint => (fn || '').toString().indexOf(hint) > -1)); const longerTimeoutBrowsers = ['Edge', 'Trident', 'Firefox']; let timeoutDuration = 0; for (let i = 0; i < longerTimeoutBrowsers.length; i += 1) { if (navigator.userAgent.indexOf(longerTimeoutBrowsers[i]) >= 0) { timeoutDuration = 1; break; } } function microtaskDebounce(fn) { let scheduled = false; let i = 0; let elem = document.createElement('span'); // MutationObserver provides a mechanism for scheduling microtasks, which // are scheduled *before* the next task. This gives us a way to debounce // a function but ensure it's called *before* the next paint. const observer = new MutationObserver(() => { fn(); scheduled = false; }); observer.observe(elem, { attributes: true }); return () => { if (!scheduled) { scheduled = true; elem.setAttribute('x-index', i); i = i + 1; // don't use compund (+=) because it doesn't get optimized in V8 } }; } function taskDebounce(fn) { let scheduled = false; return () => { if (!scheduled) { scheduled = true; setTimeout(() => { scheduled = false; fn(); }, timeoutDuration); } }; } // It's common for MutationObserver polyfills to be seen in the wild, however // these rely on Mutation Events which only occur when an element is connected // to the DOM. The algorithm used in this module does not use a connected element, // and so we must ensure that a *native* MutationObserver is available. const supportsNativeMutationObserver = isNative(window.MutationObserver); /** * Create a debounced version of a method, that's asynchronously deferred * but called in the minimum time possible. * * @method * @memberof Popper.Utils * @argument {Function} fn * @returns {Function} */ var debounce = supportsNativeMutationObserver ? microtaskDebounce : taskDebounce; /** * Mimics the `find` method of Array * @method * @memberof Popper.Utils * @argument {Array} arr * @argument prop * @argument value * @returns index or -1 */ function findIndex$1(arr, check) { // use native find if supported if (Array.prototype.find) { return arr.find(check); } // use `filter` to obtain the same behavior of `find` return arr.filter(check)[0]; } /** * Return the index of the matching object * @method * @memberof Popper.Utils * @argument {Array} arr * @argument prop * @argument value * @returns index or -1 */ function findIndex(arr, prop, value) { // use native findIndex if supported if (Array.prototype.findIndex) { return arr.findIndex(cur => cur[prop] === value); } // use `find` + `indexOf` if `findIndex` isn't supported const match = findIndex$1(arr, obj => obj[prop] === value); return arr.indexOf(match); } 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; }; /** * Given the popper offsets, generate an output similar to getBoundingClientRect * @method * @memberof Popper.Utils * @argument {Object} popperOffsets * @returns {Object} ClientRect like output */ function getClientRect(popperOffsets) { return _extends({}, popperOffsets, { right: popperOffsets.left + popperOffsets.width, bottom: popperOffsets.top + popperOffsets.height }); } /** * Get the outer sizes of the given element (offset size + margins) * @method * @memberof Popper.Utils * @argument {Element} element * @returns {Object} object containing width and height properties */ function getOuterSizes(element) { const styles = window.getComputedStyle(element); const x = parseFloat(styles.marginTop) + parseFloat(styles.marginBottom); const y = parseFloat(styles.marginLeft) + parseFloat(styles.marginRight); const result = { width: element.offsetWidth + y, height: element.offsetHeight + x }; return result; } /** * Get the opposite placement of the given one/ * @method * @memberof Popper.Utils * @argument {String} placement * @returns {String} flipped placement */ function getOppositePlacement(placement) { const hash = { left: 'right', right: 'left', bottom: 'top', top: 'bottom' }; return placement.replace(/left|right|bottom|top/g, matched => hash[matched]); } /** * Get offsets to the popper * @method * @memberof Popper.Utils * @param {Element} popper - the popper element * @param {Element} reference - the reference element (the popper will be relative to this) * @returns {Object} An object containing the offsets which will be applied to the popper */ function getPopperOffsets(state, popper, referenceOffsets, placement) { placement = placement.split('-')[0]; // Get popper node sizes const popperRect = getOuterSizes(popper); // Add position, width and height to our offsets object const popperOffsets = { position: state.position, width: popperRect.width, height: popperRect.height }; // depending by the popper placement we have to compute its offsets slightly differently const isHoriz = ['right', 'left'].indexOf(placement) !== -1; const mainSide = isHoriz ? 'top' : 'left'; const secondarySide = isHoriz ? 'left' : 'top'; const measurement = isHoriz ? 'height' : 'width'; const secondaryMeasurement = !isHoriz ? 'height' : 'width'; popperOffsets[mainSide] = referenceOffsets[mainSide] + referenceOffsets[measurement] / 2 - popperRect[measurement] / 2; if (placement === secondarySide) { popperOffsets[secondarySide] = referenceOffsets[secondarySide] - popperRect[secondaryMeasurement]; } else { popperOffsets[secondarySide] = referenceOffsets[getOppositePlacement(secondarySide)]; } return popperOffsets; } /** * Get offsets to the reference element * @method * @memberof Popper.Utils * @param {Object} state * @param {Element} popper - the popper element * @param {Element} reference - the reference element (the popper will be relative to this) * @returns {Object} An object containing the offsets which will be applied to the popper */ function getReferenceOffsets(state, popper, reference) { const isParentFixed = state.position === 'fixed'; const isParentTransformed = state.isParentTransformed; const offsetParent = getOffsetParent(isParentFixed && isParentTransformed ? reference : popper); return getOffsetRectRelativeToCustomParent(reference, offsetParent, isParentFixed, isParentTransformed); } /** * Get the prefixed supported property name * @method * @memberof Popper.Utils * @argument {String} property (camelCase) * @returns {String} prefixed property (camelCase) */ function getSupportedPropertyName(property) { const prefixes = [false, 'ms', 'webkit', 'moz', 'o']; const upperProp = property.charAt(0).toUpperCase() + property.slice(1); for (let i = 0; i < prefixes.length - 1; i++) { const prefix = prefixes[i]; const toCheck = prefix ? `${ prefix }${ upperProp }` : property; if (typeof window.document.body.style[toCheck] !== 'undefined') { return toCheck; } } return null; } /** * Check if the given variable is a function * @method * @memberof Popper.Utils * @argument {Element} element - Element to check * @returns {Boolean} answer to: is a function? */ function isFunction(functionToCheck) { const getType = {}; return functionToCheck && getType.toString.call(functionToCheck) === '[object Function]'; } /** * Helper used to know if the given modifier is enabled. * @method * @memberof Popper.Utils * @returns {Boolean} */ function isModifierEnabled(modifiers, modifierName) { return modifiers.some(({ name, enabled }) => enabled && name === modifierName); } /** * Helper used to know if the given modifier depends from another one. * It checks if the needed modifier is listed and enabled. * @method * @memberof Popper.Utils * @returns {Boolean} */ function isModifierRequired(modifiers, requestingName, requestedName) { const requesting = findIndex$1(modifiers, ({ name }) => name === requestingName); return !!requesting && modifiers.some(modifier => { return modifier.name === requestedName && modifier.enabled && modifier.order < requesting.order; }); } /** * Tells if a given input is a number * @method * @memberof Popper.Utils * @param {*} input to check * @return {Boolean} */ function isNumeric(n) { return n !== '' && !isNaN(parseFloat(n)) && isFinite(n); } /** * Check if the given element has transforms applied to itself or a parent * @method * @memberof Popper.Utils * @param {Element} element * @return {Boolean} answer to "isTransformed?" */ function isTransformed(element) { if (element.nodeName === 'BODY') { return false; } if (getStyleComputedProperty(element, 'transform') !== 'none') { return true; } return getParentNode(element) ? isTransformed(getParentNode(element)) : element; } /** * Remove event listeners used to update the popper position * @method * @memberof Popper.Utils * @private */ function removeEventListeners(reference, state) { // NOTE: 1 DOM access here window.removeEventListener('resize', state.updateBound); if (state.scrollElement) { state.scrollElement.removeEventListener('scroll', state.updateBound); } state.updateBound = null; state.scrollElement = null; state.eventsEnabled = false; return state; } /** * Loop trough the list of modifiers and run them in order, each of them will then edit the data object * @method * @memberof Popper.Utils * @param {Object} data * @param {Array} modifiers * @param {Function} ends */ function runModifiers(modifiers, data, ends) { const modifiersToRun = ends === undefined ? modifiers : modifiers.slice(0, findIndex(modifiers, 'name', ends)); modifiersToRun.forEach(modifier => { if (modifier.enabled && isFunction(modifier.function)) { data = modifier.function(data, modifier); } }); return data; } /** * Set the attributes to the given popper * @method * @memberof Popper.Utils * @argument {Element} element - Element to apply the attributes to * @argument {Object} styles - Object with a list of properties and values which will be applied to the element */ function setAttributes(element, attributes) { Object.keys(attributes).forEach(function (prop) { const value = attributes[prop]; if (value !== false) { element.setAttribute(prop, attributes[prop]); } else { element.removeAttribute(prop); } }); } /** * Set the style to the given popper * @method * @memberof Popper.Utils * @argument {Element} element - Element to apply the style to * @argument {Object} styles - Object with a list of properties and values which will be applied to the element */ function setStyles(element, styles) { Object.keys(styles).forEach(prop => { let unit = ''; // add unit if the value is numeric and is one of the following if (['width', 'height', 'top', 'right', 'bottom', 'left'].indexOf(prop) !== -1 && isNumeric(styles[prop])) { unit = 'px'; } element.style[prop] = styles[prop] + unit; }); } /** * Setup needed event listeners used to update the popper position * @method * @memberof Popper.Utils * @private */ function setupEventListeners(reference, options, state, updateBound) { // NOTE: 1 DOM access here state.updateBound = updateBound; window.addEventListener('resize', state.updateBound, { passive: true }); let target = getScrollParent(reference); if (target.nodeName === 'BODY') { target = window; } target.addEventListener('scroll', state.updateBound, { passive: true }); state.scrollElement = target; state.eventsEnabled = true; return state; } /** @namespace Popper.Utils */ var Utils = { computeAutoPlacement, debounce, findIndex, getBoundaries, getBoundingClientRect, getClientRect, getOffsetParent, getOffsetRect, getOffsetRectRelativeToCustomParent, getOuterSizes, getParentNode, getPopperOffsets, getPosition, getReferenceOffsets, getScroll, getScrollParent, getStyleComputedProperty, getSupportedPropertyName, getTotalScroll, getWindowSizes, isFixed, isFunction, isModifierEnabled, isModifierRequired, isNative, isNumeric, isTransformed, removeEventListeners, runModifiers, setAttributes, setStyles, setupEventListeners }; /** * Apply the computed styles to the popper element * @method * @memberof Modifiers * @argument {Object} data - The data object generated by `update` method * @argument {Object} data.styles - List of style properties - values to apply to popper element * @argument {Object} data.attributes - List of attribute properties - values to apply to popper element * @argument {Object} options - Modifiers configuration and options * @returns {Object} The same data object */ function applyStyle(data, options) { // apply the final offsets to the popper // NOTE: 1 DOM access here const styles = { position: data.offsets.popper.position }; const attributes = { 'x-placement': data.placement }; // round top and left to avoid blurry text const left = Math.round(data.offsets.popper.left); const top = Math.round(data.offsets.popper.top); // if gpuAcceleration is set to true and transform is supported, // we use `translate3d` to apply the position to the popper we // automatically use the supported prefixed version if needed const prefixedProperty = getSupportedPropertyName('transform'); if (options.gpuAcceleration && prefixedProperty) { styles[prefixedProperty] = 'translate3d(' + left + 'px, ' + top + 'px, 0)'; styles.top = 0; styles.left = 0; styles.willChange = 'transform'; } // othwerise, we use the standard `left` and `top` properties else { styles.left = left; styles.top = top; styles.willChange = 'top, left'; } // any property present in `data.styles` will be applied to the popper, // in this way we can make the 3rd party modifiers add custom styles to it // Be aware, modifiers could override the properties defined in the previous // lines of this modifier! setStyles(data.instance.popper, _extends({}, styles, data.styles)); // any property present in `data.attributes` will be applied to the popper, // they will be set as HTML attributes of the element setAttributes(data.instance.popper, _extends({}, attributes, data.attributes)); // if the arrow style has been computed, apply the arrow style if (data.offsets.arrow) { setStyles(data.arrowElement, data.offsets.arrow); } return data; } /** * Set the x-placement attribute before everything else because it could be used to add margins to the popper * margins needs to be calculated to get the correct popper offsets * @method * @memberof Popper.modifiers * @param {HTMLElement} reference - The reference element used to position the popper * @param {HTMLElement} popper - The HTML element used as popper. * @param {Object} options - Popper.js options */ function applyStyleOnLoad(reference, popper, options, modifierOptions, state) { // compute reference element offsets const referenceOffsets = getReferenceOffsets(state, popper, reference); // compute auto placement, store placement inside the data object, // modifiers will be able to edit `placement` if needed // and refer to originalPlacement to know the original value options.placement = computeAutoPlacement(options.placement, referenceOffsets, popper); popper.setAttribute('x-placement', options.placement); return options; } /** * Modifier used to move the arrowElements on the edge of the popper to make sure them are always between the popper and the reference element * It will use the CSS outer size of the arrowElement element to know how many pixels of conjuction are needed * @method * @memberof Modifiers * @argument {Object} data - The data object generated by update method * @argument {Object} options - Modifiers configuration and options * @returns {Object} The data object, properly modified */ function arrow(data, options) { // arrow depends on keepTogether in order to work if (!isModifierRequired(data.instance.modifiers, 'arrow', 'keepTogether')) { console.warn('WARNING: `keepTogether` modifier is required by arrow modifier in order to work, be sure to include it before `arrow`!'); return data; } let arrowElement = options.element; // if arrowElement is a string, suppose it's a CSS selector if (typeof arrowElement === 'string') { arrowElement = data.instance.popper.querySelector(arrowElement); // if arrowElement is not found, don't run the modifier if (!arrowElement) { return data; } } else { // if the arrowElement isn't a query selector we must check that the // provided DOM node is child of its popper node if (!data.instance.popper.contains(arrowElement)) { console.warn('WARNING: `arrow.element` must be child of its popper element!'); return data; } } const placement = data.placement.split('-')[0]; const popper = getClientRect(data.offsets.popper); const reference = data.offsets.reference; const isVertical = ['left', 'right'].indexOf(placement) !== -1; const len = isVertical ? 'height' : 'width'; const side = isVertical ? 'top' : 'left'; const altSide = isVertical ? 'left' : 'top'; const opSide = isVertical ? 'bottom' : 'right'; const arrowElementSize = getOuterSizes(arrowElement)[len]; // // extends keepTogether behavior making sure the popper and its reference have enough pixels in conjuction // // top/left side if (reference[opSide] - arrowElementSize < popper[side]) { data.offsets.popper[side] -= popper[side] - (reference[opSide] - arrowElementSize); } // bottom/right side if (reference[side] + arrowElementSize > popper[opSide]) { data.offsets.popper[side] += reference[side] + arrowElementSize - popper[opSide]; } // compute center of the popper const center = reference[side] + reference[len] / 2 - arrowElementSize / 2; // Compute the sideValue using the updated popper offsets let sideValue = center - getClientRect(data.offsets.popper)[side]; // prevent arrowElement from being placed not contiguously to its popper sideValue = Math.max(Math.min(popper[len] - arrowElementSize, sideValue), 0); data.arrowElement = arrowElement; data.offsets.arrow = {}; data.offsets.arrow[side] = sideValue; data.offsets.arrow[altSide] = ''; // make sure to unset any eventual altSide value from the DOM node return data; } /** * Get the opposite placement variation of the given one/ * @method * @memberof Popper.Utils * @argument {String} placement variation * @returns {String} flipped placement variation */ function getOppositeVariation(variation) { if (variation === 'end') { return 'start'; } else if (variation === 'start') { return 'end'; } return variation; } /** * Modifier used to flip the placement of the popper when the latter is starting overlapping its reference element. * Requires the `preventOverflow` modifier before it in order to work. * **NOTE:** data.instance modifier will run all its previous modifiers everytime it tries to flip the popper! * @method * @memberof Modifiers * @argument {Object} data - The data object generated by update method * @argument {Object} options - Modifiers configuration and options * @returns {Object} The data object, properly modified */ function flip(data, options) { // if `inner` modifier is enabled, we can't use the `flip` modifier if (isModifierEnabled(data.instance.modifiers, 'inner')) { return data; } if (data.flipped && data.placement === data.originalPlacement) { // seems like flip is trying to loop, probably there's not enough space on any of the flippable sides return data; } const boundaries = getBoundaries(data.instance.popper, options.padding, options.boundariesElement); let placement = data.placement.split('-')[0]; let placementOpposite = getOppositePlacement(placement); let variation = data.placement.split('-')[1] || ''; let flipOrder = []; if (options.behavior === 'flip') { flipOrder = [placement, placementOpposite]; } else { flipOrder = options.behavior; } flipOrder.forEach((step, index) => { if (placement !== step || flipOrder.length === index + 1) { return data; } placement = data.placement.split('-')[0]; placementOpposite = getOppositePlacement(placement); const popperOffsets = getClientRect(data.offsets.popper); const refOffsets = data.offsets.reference; // using Math.floor because the reference offsets may contain decimals we are not going to consider here const overlapsRef = placement === 'left' && Math.floor(popperOffsets.right) > Math.floor(refOffsets.left) || placement === 'right' && Math.floor(popperOffsets.left) < Math.floor(refOffsets.right) || placement === 'top' && Math.floor(popperOffsets.bottom) > Math.floor(refOffsets.top) || placement === 'bottom' && Math.floor(popperOffsets.top) < Math.floor(refOffsets.bottom); const overflowsBoundaries = placement === 'left' && Math.floor(popperOffsets.left) < Math.floor(boundaries.left) || placement === 'right' && Math.floor(popperOffsets.right) > Math.floor(boundaries.right) || placement === 'top' && Math.floor(popperOffsets.top) < Math.floor(boundaries.top) || placement === 'bottom' && Math.floor(popperOffsets.bottom) > Math.floor(boundaries.bottom); // flip the variation if required const isVertical = ['top', 'bottom'].indexOf(placement) !== -1; const flippedVariation = !!options.flipVariations && (isVertical && variation === 'start' && Math.floor(popperOffsets.left) < Math.floor(boundaries.left) || isVertical && variation === 'end' && Math.floor(popperOffsets.right) > Math.floor(boundaries.right) || !isVertical && variation === 'start' && Math.floor(popperOffsets.top) < Math.floor(boundaries.top) || !isVertical && variation === 'end' && Math.floor(popperOffsets.bottom) > Math.floor(boundaries.bottom)); if (overlapsRef || overflowsBoundaries || flippedVariation) { // this boolean to detect any flip loop data.flipped = true; if (overlapsRef || overflowsBoundaries) { placement = flipOrder[index + 1]; } if (flippedVariation) { variation = getOppositeVariation(variation); } data.placement = placement + (variation ? '-' + variation : ''); data.offsets.popper = getPopperOffsets(data.instance.state, data.instance.popper, data.offsets.reference, data.placement); data = runModifiers(data.instance.modifiers, data, 'flip'); } }); return data; } /** * Modifier used to make sure the popper is always near its reference element * It cares only about the first axis, you can still have poppers with margin * between the popper and its reference element. * @method * @memberof Modifiers * @argument {Object} data - The data object generated by update method * @argument {Object} options - Modifiers configuration and options * @returns {Object} The data object, properly modified */ function keepTogether(data) { const popper = getClientRect(data.offsets.popper); const reference = data.offsets.reference; const placement = data.placement.split('-')[0]; const floor = Math.floor; if (['top', 'bottom'].indexOf(placement) !== -1) { if (popper.right < floor(reference.left)) { data.offsets.popper.left = floor(reference.left) - popper.width; } if (popper.left > floor(reference.right)) { data.offsets.popper.left = floor(reference.right); } } else { if (popper.bottom < floor(reference.top)) { data.offsets.popper.top = floor(reference.top) - popper.height; } if (popper.top > floor(reference.bottom)) { data.offsets.popper.top = floor(reference.bottom); } } return data; } /** * Modifier used to add an offset to the popper, useful if you more granularity positioning your popper. * The offsets will shift the popper on the side of its reference element. * @method * @memberof Modifiers * @argument {Object} data - The data object generated by update method * @argument {Object} options - Modifiers configuration and options * @argument {Number|String} options.offset=0 * Basic usage allows a number used to nudge the popper by the given amount of pixels. * You can pass a percentage value as string (eg. `20%`) to nudge by the given percentage (relative to reference element size) * Other supported units are `vh` and `vw` (relative to viewport) * Additionally, you can pass a pair of values (eg. `10 20` or `2vh 20%`) to nudge the popper * on both axis. * A note about percentage values, if you want to refer a percentage to the popper size instead of the reference element size, * use `%p` instead of `%` (eg: `20%p`). To make it clearer, you can replace `%` with `%r` and use eg.`10%p 25%r`. * > **Heads up!** The order of the axis is relative to the popper placement: `bottom` or `top` are `X,Y`, the other are `Y,X` * @returns {Object} The data object, properly modified */ function offset(data, options) { const placement = data.placement; const popper = data.offsets.popper; let offsets; if (isNumeric(options.offset)) { offsets = [options.offset, 0]; } else { // split the offset in case we are providing a pair of offsets separated // by a blank space offsets = options.offset.split(' '); // itherate through each offset to compute them in case they are percentages offsets = offsets.map((offset, index) => { // separate value from unit const split = offset.match(/(\d*\.?\d*)(.*)/); const value = +split[1]; const unit = split[2]; // use height if placement is left or right and index is 0 otherwise use width // in this way the first offset will use an axis and the second one // will use the other one let useHeight = placement.indexOf('right') !== -1 || placement.indexOf('left') !== -1; if (index === 1) { useHeight = !useHeight; } const measurement = useHeight ? 'height' : 'width'; // if is a percentage, we calculate the value of it using as base the // sizes of the reference element if (unit === '%' || unit === '%r') { const referenceRect = getClientRect(data.offsets.reference); let len = referenceRect[measurement]; return len / 100 * value; } // if is a percentage relative to the popper, we calculate the value of it using // as base the sizes of the popper else if (unit === '%p') { const popperRect = getClientRect(data.offsets.popper); let len = popperRect[measurement]; return len / 100 * value; } // if is a vh or vw, we calculate the size based on the viewport else if (unit === 'vh' || unit === 'vw') { let size; if (unit === 'vh') { size = Math.max(document.documentElement.clientHeight, window.innerHeight || 0); } else { size = Math.max(document.documentElement.clientWidth, window.innerWidth || 0); } return size / 100 * value; } // if is an explicit pixel unit, we get rid of the unit and keep the value else if (unit === 'px') { return +value; } // if is an implicit unit, it's px, and we return just the value else { return +offset; } }); } if (data.placement.indexOf('left') !== -1) { popper.top += offsets[0]; popper.left -= offsets[1] || 0; } else if (data.placement.indexOf('right') !== -1) { popper.top += offsets[0]; popper.left += offsets[1] || 0; } else if (data.placement.indexOf('top') !== -1) { popper.left += offsets[0]; popper.top -= offsets[1] || 0; } else if (data.placement.indexOf('bottom') !== -1) { popper.left += offsets[0]; popper.top += offsets[1] || 0; } return data; } /** * Modifier used to prevent the popper from being positioned outside the boundary. * * An scenario exists where the reference itself is not within the boundaries. We can * say it has "escaped the boundaries" — or just "escaped". In this case we need to * decide whether the popper should either: * * - detach from the reference and remain "trapped" in the boundaries, or * - if it should be ignore the boundary and "escape with the reference" * * When `escapeWithReference` is `true`, and reference is completely outside the * boundaries, the popper will overflow (or completely leave) the boundaries in order * to remain attached to the edge of the reference. * * @method * @memberof Modifiers * @argument {Object} data - The data object generated by `update` method * @argument {Object} options - Modifiers configuration and options * @returns {Object} The data object, properly modified */ function preventOverflow(data, options) { const boundariesElement = options.boundariesElement || getOffsetParent(data.instance.popper); const boundaries = getBoundaries(data.instance.popper, options.padding, boundariesElement); options.boundaries = boundaries; const order = options.priority; let popper = getClientRect(data.offsets.popper); const check = { left() { let left = popper.left; if (popper.left < boundaries.left && !shouldOverflowBoundary(data, options, 'left')) { left = Math.max(popper.left, boundaries.left); } return { left }; }, right() { let left = popper.left; if (popper.right > boundaries.right && !shouldOverflowBoundary(data, options, 'right')) { left = Math.min(popper.left, boundaries.right - popper.width); } return { left }; }, top() { let top = popper.top; if (popper.top < boundaries.top && !shouldOverflowBoundary(data, options, 'top')) { top = Math.max(popper.top, boundaries.top); } return { top }; }, bottom() { let top = popper.top; if (popper.bottom > boundaries.bottom && !shouldOverflowBoundary(data, options, 'bottom')) { top = Math.min(popper.top, boundaries.bottom - popper.height); } return { top }; } }; order.forEach(direction => { popper = _extends({}, popper, check[direction]()); }); data.offsets.popper = popper; return data; } /** * Determine if the popper should overflow a boundary edge to stay together with the reference. */ function shouldOverflowBoundary(data, options, overflowDirection) { if (!options.escapeWithReference) { return false; } if (data.flipped && isSameAxis(data.originalPlacement, overflowDirection)) { return true; } if (!isSameAxis(data.originalPlacement, overflowDirection)) { return true; } return true; } /** * Determine if two placement values are on the same axis. */ function isSameAxis(a, b) { // placement syntax: // // ( "top" | "right" | "bottom" | "left" ) ( "-start" | "" | "-end" ) // |------------- Direction -------------| // const aDirection = a.split('-')[0]; const bDirection = b.split('-')[0]; return aDirection === bDirection || aDirection === getOppositePlacement(b); } /** * Modifier used to shift the popper on the start or end of its reference element side * @method * @memberof Modifiers * @argument {Object} data - The data object generated by `update` method * @argument {Object} options - Modifiers configuration and options * @returns {Object} The data object, properly modified */ function shift(data) { const placement = data.placement; const basePlacement = placement.split('-')[0]; const shiftvariation = placement.split('-')[1]; // if shift shiftvariation is specified, run the modifier if (shiftvariation) { const reference = data.offsets.reference; const popper = getClientRect(data.offsets.popper); const shiftOffsets = { y: { start: { top: reference.top }, end: { top: reference.top + reference.height - popper.height } }, x: { start: { left: reference.left }, end: { left: reference.left + reference.width - popper.width } } }; const axis = ['bottom', 'top'].indexOf(basePlacement) !== -1 ? 'x' : 'y'; data.offsets.popper = _extends({}, popper, shiftOffsets[axis][shiftvariation]); } return data; } /** * Modifier used to hide the popper when its reference element is outside of the * popper boundaries. It will set an x-hidden attribute which can be used to hide * the popper when its reference is out of boundaries. * @method * @memberof Modifiers * @argument {Object} data - The data object generated by update method * @argument {Object} options - Modifiers configuration and options * @returns {Object} The data object, properly modified */ function hide(data) { if (!isModifierRequired(data.instance.modifiers, 'hide', 'preventOverflow')) { console.warn('WARNING: preventOverflow modifier is required by hide modifier in order to work, be sure to include it before hide!'); return data; } const refRect = data.offsets.reference; const bound = findIndex$1(data.instance.modifiers, modifier => modifier.name === 'preventOverflow').boundaries; if (refRect.bottom < bound.top || refRect.left > bound.right || refRect.top > bound.bottom || refRect.right < bound.left) { // Avoid unnecessary DOM access if visibility hasn't changed if (data.hide === true) { return data; } data.hide = true; data.attributes['x-out-of-boundaries'] = ''; } else { // Avoid unnecessary DOM access if visibility hasn't changed if (data.hide === false) { return data; } data.hide = false; data.attributes['x-out-of-boundaries'] = false; } return data; } /** * Modifier used to make the popper flow toward the inner of the reference element. * By default, when this modifier is disabled, the popper will be placed outside * the reference element. * @method * @memberof Modifiers * @argument {Object} data - The data object generated by `update` method * @argument {Object} options - Modifiers configuration and options * @returns {Object} The data object, properly modified */ function inner(data) { const placement = data.placement; const basePlacement = placement.split('-')[0]; const popper = getClientRect(data.offsets.popper); const reference = getClientRect(data.offsets.reference); const isHoriz = ['left', 'right'].indexOf(basePlacement) !== -1; const subtractLength = ['top', 'left'].indexOf(basePlacement) === -1; popper[isHoriz ? 'left' : 'top'] = reference[placement] - (subtractLength ? popper[isHoriz ? 'width' : 'height'] : 0); data.placement = getOppositePlacement(placement); data.offsets.popper = getClientRect(popper); return data; } /** * Modifiers are plugins used to alter the behavior of your poppers. * Popper.js uses a set of 7 modifiers to provide all the basic functionalities * needed by the library. * * Each modifier is an object containing several properties listed below. * @namespace Modifiers * @param {Object} modifier - Modifier descriptor * @param {Integer} modifier.order * The `order` property defines the execution order of the modifiers. * The built-in modifiers have orders with a gap of 100 units in between, * this allows you to inject additional modifiers between the existing ones * without having to redefine the order of all of them. * The modifiers are executed starting from the one with the lowest order. * @param {Boolean} modifier.enabled - When `true`, the modifier will be used. * @param {Modifiers~modifier} modifier.function - Modifier function. * @param {Modifiers~onLoad} modifier.onLoad - Function executed on popper initalization * @return {Object} data - Each modifier must return the modified `data` object. */ var modifiers = { shift: { order: 100, enabled: true, function: shift }, offset: { order: 200, enabled: true, function: offset, // nudges popper from its origin by the given amount of pixels (can be negative) offset: 0 }, preventOverflow: { order: 300, enabled: true, function: preventOverflow, // popper will try to prevent overflow following these priorities // by default, then, it could overflow on the left and on top of the boundariesElement priority: ['left', 'right', 'top', 'bottom'], // amount of pixel used to define a minimum distance between the boundaries and the popper // this makes sure the popper has always a little padding between the edges of its container padding: 5, boundariesElement: 'scrollParent' }, keepTogether: { order: 400, enabled: true, function: keepTogether }, arrow: { order: 500, enabled: true, function: arrow, // selector or node used as arrow element: '[x-arrow]' }, flip: { order: 600, enabled: true, function: flip, // the behavior used to change the popper's placement behavior: 'flip', // the popper will flip if it hits the edges of the boundariesElement - padding padding: 5, boundariesElement: 'viewport' }, inner: { order: 700, enabled: false, function: inner }, hide: { order: 800, enabled: true, function: hide }, applyStyle: { order: 900, enabled: true, // if true, it uses the CSS 3d transformation to position the popper gpuAcceleration: true, function: applyStyle, onLoad: applyStyleOnLoad } }; /** * Modifiers can edit the `data` object to change the beheavior of the popper. * This object contains all the informations used by Popper.js to compute the * popper position. * The modifier can edit the data as needed, and then `return` it as result. * * @callback Modifiers~modifier * @param {dataObject} data * @return {dataObject} modified data */ /** * The `dataObject` is an object containing all the informations used by Popper.js * this object get passed to modifiers and to the `onCreate` and `onUpdate` callbacks. * @name dataObject * @property {Object} data.instance The Popper.js instance * @property {String} data.placement Placement applied to popper * @property {String} data.originalPlacement Placement originally defined on init * @property {Boolean} data.flipped True if popper has been flipped by flip modifier * @property {Boolean} data.hide True if the reference element is out of boundaries, useful to know when to hide the popper. * @property {HTMLElement} data.arrowElement Node used as arrow by arrow modifier * @property {Object} data.styles Any CSS property defined here will be applied to the popper, it expects the JavaScript nomenclature (eg. `marginBottom`) * @property {Object} data.boundaries Offsets of the popper boundaries * @property {Object} data.offsets The measurements of popper, reference and arrow elements. * @property {Object} data.offsets.popper `top`, `left`, `width`, `height` values * @property {Object} data.offsets.reference `top`, `left`, `width`, `height` values * @property {Object} data.offsets.arro] `top` and `left` offsets, only one of them will be different from 0 */ // Utils // Modifiers // default options const DEFAULTS = { // placement of the popper placement: 'bottom', // whether events (resize, scroll) are initially enabled eventsEnabled: true, /** * Callback called when the popper is created. * By default, is set to no-op. * Access Popper.js instance with `data.instance`. * @callback createCallback * @static * @param {dataObject} data */ onCreate: () => {}, /** * Callback called when the popper is updated, this callback is not called * on the initialization/creation of the popper, but only on subsequent * updates. * By default, is set to no-op. * Access Popper.js instance with `data.instance`. * @callback updateCallback * @static * @param {dataObject} data */ onUpdate: () => {}, // list of functions used to modify the offsets before they are applied to the popper modifiers }; /** * Create a new Popper.js instance * @class Popper * @param {HTMLElement} reference - The reference element used to position the popper * @param {HTMLElement} popper - The HTML element used as popper. * @param {Object} options * @param {String} options.placement=bottom * Placement of the popper accepted values: `top(-start, -end), right(-start, -end), bottom(-start, -end), * left(-start, -end)` * * @param {Boolean} options.eventsEnabled=true * Whether events (resize, scroll) are initially enabled * @param {Boolean} options.gpuAcceleration=true * When this property is set to true, the popper position will be applied using CSS3 translate3d, allowing the * browser to use the GPU to accelerate the rendering. * If set to false, the popper will be placed using `top` and `left` properties, not using the GPU. * * @param {Boolean} options.removeOnDestroy=false * Set to true if you want to automatically remove the popper when you call the `destroy` method. * * @param {Object} options.modifiers * List of functions used to modify the data before they are applied to the popper (see source code for default values) * * @param {Object} options.modifiers.arrow - Arrow modifier configuration * @param {String|HTMLElement} options.modifiers.arrow.element='[x-arrow]' * The DOM Node used as arrow for the popper, or a CSS selector used to get the DOM node. It must be child of * its parent Popper. Popper.js will apply to the given element the style required to align the arrow with its * reference element. * By default, it will look for a child node of the popper with the `x-arrow` attribute. * * @param {Object} options.modifiers.offset - Offset modifier configuration * @param {Number} options.modifiers.offset.offset=0 * Amount of pixels the popper will be shifted (can be negative). * * @param {Object} options.modifiers.preventOverflow - PreventOverflow modifier configuration * @param {Array} [options.modifiers.preventOverflow.priority=['left', 'right', 'top', 'bottom']] * Priority used when Popper.js tries to avoid overflows from the boundaries, they will be checked in order, * this means that the last one will never overflow * @param {String|HTMLElement} options.modifiers.preventOverflow.boundariesElement='scrollParent' * Boundaries used by the modifier, can be `scrollParent`, `window`, `viewport` or any DOM element. * @param {Number} options.modifiers.preventOverflow.padding=5 * Amount of pixel used to define a minimum distance between the boundaries and the popper * this makes sure the popper has always a little padding between the edges of its container. * * @param {Object} options.modifiers.flip - Flip modifier configuration * @param {String|Array} options.modifiers.flip.behavior='flip' * The behavior used by the `flip` modifier to change the placement of the popper when the latter is trying to * overlap its reference element. Defining `flip` as value, the placement will be flipped on * its axis (`right - left`, `top - bottom`). * You can even pass an array of placements (eg: `['right', 'left', 'top']` ) to manually specify * how alter the placement when a flip is needed. (eg. in the above example, it would first flip from right to left, * then, if even in its new placement, the popper is overlapping its reference element, it will be moved to top) * @param {String|HTMLElement} options.modifiers.flip.boundariesElement='viewport' * The element which will define the boundaries of the popper position, the popper will never be placed outside * of the defined boundaries (except if `keepTogether` is enabled) * * @param {Object} options.modifiers.inner - Inner modifier configuration * @param {Number} options.modifiers.innner.enabled=false * Set to `true` to make the popper flow toward the inner of the reference element. * * @param {Number} options.modifiers.flip.padding=5 * Amount of pixel used to define a minimum distance between the boundaries and the popper * this makes sure the popper has always a little padding between the edges of its container. * * @param {createCallback} options.onCreate - onCreate callback * Function called after the Popper has been instantiated. * * @param {updateCallback} options.onUpdate - onUpdate callback * Function called on subsequent updates of Popper. * * @return {Object} instance - The generated Popper.js instance */ class Popper { constructor(reference, popper, options = {}) { this.scheduleUpdate = () => requestAnimationFrame(this.update); // make update() debounced, so that it only runs at most once-per-tick this.update = debounce(this.update.bind(this)); // with {} we create a new object with the options inside it this.options = _extends({}, Popper.Defaults, options); // init state this.state = { isDestroyed: false, isCreated: false }; // get reference and popper elements (allow jQuery wrappers) this.reference = reference.jquery ? reference[0] : reference; this.popper = popper.jquery ? popper[0] : popper; // refactoring modifiers' list (Object => Array) this.modifiers = Object.keys(Popper.Defaults.modifiers).map(name => _extends({ name }, Popper.Defaults.modifiers[name])); // assign default values to modifiers, making sure to override them with // the ones defined by user this.modifiers = this.modifiers.map(defaultConfig => { const userConfig = options.modifiers && options.modifiers[defaultConfig.name] || {}; return _extends({}, defaultConfig, userConfig); }); // add custom modifiers to the modifiers list if (options.modifiers) { this.options.modifiers = _extends({}, Popper.Defaults.modifiers, options.modifiers); Object.keys(options.modifiers).forEach(name => { // take in account only custom modifiers if (Popper.Defaults.modifiers[name] === undefined) { const modifier = options.modifiers[name]; modifier.name = name; this.modifiers.push(modifier); } }); } // get the popper position type this.state.position = getPosition(this.reference); // sort the modifiers by order this.modifiers = this.modifiers.sort((a, b) => a.order - b.order); // modifiers have the ability to execute arbitrary code when Popper.js get inited // such code is executed in the same order of its modifier // they could add new properties to their options configuration // BE AWARE: don't add options to `options.modifiers.name` but to `modifierOptions`! this.modifiers.forEach(modifierOptions => { if (modifierOptions.enabled && isFunction(modifierOptions.onLoad)) { modifierOptions.onLoad(this.reference, this.popper, this.options, modifierOptions, this.state); } }); // determine how we should set the origin of offsets this.state.isParentTransformed = isTransformed(this.popper.parentNode); // fire the first update to position the popper in the right place this.update(); const eventsEnabled = this.options.eventsEnabled; if (eventsEnabled) { // setup event listeners, they will take care of update the position in specific situations this.enableEventListeners(); } this.state.eventsEnabled = eventsEnabled; } // // Methods // /** * Updates the position of the popper, computing the new offsets and applying the new style * Prefer `scheduleUpdate` over `update` because of performance reasons * @method * @memberof Popper */ update() { // if popper is destroyed, don't perform any further update if (this.state.isDestroyed) { return; } let data = { instance: this, styles: {}, attributes: {}, flipped: false, offsets: {} }; // make sure to apply the popper position before any computation this.state.position = getPosition(this.reference); setStyles(this.popper, { position: this.state.position }); // compute reference element offsets data.offsets.reference = getReferenceOffsets(this.state, this.popper, this.reference); // compute auto placement, store placement inside the data object, // modifiers will be able to edit `placement` if needed // and refer to originalPlacement to know the original value data.placement = computeAutoPlacement(this.options.placement, data.offsets.reference, this.popper); // store the computed placement inside `originalPlacement` data.originalPlacement = this.options.placement; // compute the popper offsets data.offsets.popper = getPopperOffsets(this.state, this.popper, data.offsets.reference, data.placement); // run the modifiers data = runModifiers(this.modifiers, data); // the first `update` will call `onCreate` callback // the other ones will call `onUpdate` callback if (!this.state.isCreated) { this.state.isCreated = true; this.options.onCreate(data); } else { this.options.onUpdate(data); } } /** * Schedule an update, it will run on the next UI update available * @method * @memberof Popper */ /** * Destroy the popper * @method * @memberof Popper */ destroy() { this.state.isDestroyed = true; // touch DOM only if `applyStyle` modifier is enabled if (isModifierEnabled(this.modifiers, 'applyStyle')) { this.popper.removeAttribute('x-placement'); this.popper.style.left = ''; this.popper.style.position = ''; this.popper.style.top = ''; this.popper.style[getSupportedPropertyName('transform')] = ''; } this.disableEventListeners(); // remove the popper if user explicity asked for the deletion on destroy // do not use `remove` because IE11 doesn't support it if (this.options.removeOnDestroy) { this.popper.parentNode.removeChild(this.popper); } return this; } /** * it will add resize/scroll events and start recalculating * position of the popper element when they are triggered * @method * @memberof Popper */ enableEventListeners() { if (!this.state.eventsEnabled) { this.state = setupEventListeners(this.reference, this.options, this.state, this.scheduleUpdate); } } /** * it will remove resize/scroll events and won't recalculate * popper position when they are triggered. It also won't trigger onUpdate callback anymore, * unless you call 'update' method manually. * @method * @memberof Popper */ disableEventListeners() { if (this.state.eventsEnabled) { window.cancelAnimationFrame(this.scheduleUpdate); this.state = removeEventListeners(this.reference, this.state); } } /** * Collection of utilities useful when writing custom modifiers * @memberof Popper */ /** * List of accepted placements to use as values of the `placement` option * @memberof Popper */ /** * Default Popper.js options * @memberof Popper */ } Popper.Utils = Utils; Popper.placements = ['auto', 'auto-start', 'auto-end', 'top', 'top-start', 'top-end', 'right', 'right-start', 'right-end', 'bottom', 'bottom-start', 'bottom-end', 'left', 'left-start', 'left-end']; Popper.Defaults = DEFAULTS; return Popper; }))); //# sourceMappingURL=popper.js.map