UNPKG

50.1 kBJavaScriptView Raw
1/**
2 * @fileOverview Kickass library to create and place poppers near their reference elements.
3 * @version {{version}}
4 * @license
5 * Copyright (c) 2016 Federico Zivolo and contributors
6 *
7 * Permission is hereby granted, free of charge, to any person obtaining a copy
8 * of this software and associated documentation files (the "Software"), to deal
9 * in the Software without restriction, including without limitation the rights
10 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11 * copies of the Software, and to permit persons to whom the Software is
12 * furnished to do so, subject to the following conditions:
13 *
14 * The above copyright notice and this permission notice shall be included in all
15 * copies or substantial portions of the Software.
16 *
17 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
23 * SOFTWARE.
24 */
25
26//
27// Cross module loader
28// Supported: Node, AMD, Browser globals
29//
30;(function (root, factory) {
31 if (typeof define === 'function' && define.amd) {
32 // AMD. Register as an anonymous module.
33 define(factory);
34 } else if (typeof module === 'object' && module.exports) {
35 // Node. Does not work with strict CommonJS, but
36 // only CommonJS-like environments that support module.exports,
37 // like Node.
38 module.exports = factory();
39 } else {
40 // Browser globals (root is window)
41 root.Popper = factory();
42 }
43}(this, function () {
44
45 'use strict';
46
47 var root = window;
48
49 // default options
50 var DEFAULTS = {
51 // placement of the popper
52 placement: 'bottom',
53
54 gpuAcceleration: true,
55
56 // shift popper from its origin by the given amount of pixels (can be negative)
57 offset: 0,
58
59 // the element which will act as boundary of the popper
60 boundariesElement: 'viewport',
61
62 // amount of pixel used to define a minimum distance between the boundaries and the popper
63 boundariesPadding: 5,
64
65 // popper will try to prevent overflow following this order,
66 // by default, then, it could overflow on the left and on top of the boundariesElement
67 preventOverflowOrder: ['left', 'right', 'top', 'bottom'],
68
69 // the behavior used by flip to change the placement of the popper
70 flipBehavior: 'flip',
71
72 arrowElement: '[x-arrow]',
73
74 arrowOffset: 0,
75
76 // list of functions used to modify the offsets before they are applied to the popper
77 modifiers: [ 'shift', 'offset', 'preventOverflow', 'keepTogether', 'arrow', 'flip', 'applyStyle'],
78
79 modifiersIgnored: [],
80
81 forceAbsolute: false
82 };
83
84 /**
85 * Create a new Popper.js instance
86 * @constructor Popper
87 * @param {HTMLElement} reference - The reference element used to position the popper
88 * @param {HTMLElement|Object} popper
89 * The HTML element used as popper, or a configuration used to generate the popper.
90 * @param {String} [popper.tagName='div'] The tag name of the generated popper.
91 * @param {Array} [popper.classNames=['popper']] Array of classes to apply to the generated popper.
92 * @param {Array} [popper.attributes] Array of attributes to apply, specify `attr:value` to assign a value to it.
93 * @param {HTMLElement|String} [popper.parent=window.document.body] The parent element, given as HTMLElement or as query string.
94 * @param {String} [popper.content=''] The content of the popper, it can be text, html, or node; if it is not text, set `contentType` to `html` or `node`.
95 * @param {String} [popper.contentType='text'] If `html`, the `content` will be parsed as HTML. If `node`, it will be appended as-is.
96 * @param {String} [popper.arrowTagName='div'] Same as `popper.tagName` but for the arrow element.
97 * @param {Array} [popper.arrowClassNames='popper__arrow'] Same as `popper.classNames` but for the arrow element.
98 * @param {String} [popper.arrowAttributes=['x-arrow']] Same as `popper.attributes` but for the arrow element.
99 * @param {Object} options
100 * @param {String} [options.placement=bottom]
101 * Placement of the popper accepted values: `top(-start, -end), right(-start, -end), bottom(-start, -right),
102 * left(-start, -end)`
103 *
104 * @param {HTMLElement|String} [options.arrowElement='[x-arrow]']
105 * The DOM Node used as arrow for the popper, or a CSS selector used to get the DOM node. It must be child of
106 * its parent Popper. Popper.js will apply to the given element the style required to align the arrow with its
107 * reference element.
108 * By default, it will look for a child node of the popper with the `x-arrow` attribute.
109 *
110 * @param {Boolean} [options.gpuAcceleration=true]
111 * When this property is set to true, the popper position will be applied using CSS3 translate3d, allowing the
112 * browser to use the GPU to accelerate the rendering.
113 * If set to false, the popper will be placed using `top` and `left` properties, not using the GPU.
114 *
115 * @param {Number} [options.offset=0]
116 * Amount of pixels the popper will be shifted (can be negative).
117 *
118 * @param {String|Element} [options.boundariesElement='viewport']
119 * The element which will define the boundaries of the popper position, the popper will never be placed outside
120 * of the defined boundaries (except if `keepTogether` is enabled)
121 *
122 * @param {Number} [options.boundariesPadding=5]
123 * Additional padding for the boundaries
124 *
125 * @param {Array} [options.preventOverflowOrder=['left', 'right', 'top', 'bottom']]
126 * Order used when Popper.js tries to avoid overflows from the boundaries, they will be checked in order,
127 * this means that the last ones will never overflow
128 *
129 * @param {String|Array} [options.flipBehavior='flip']
130 * The behavior used by the `flip` modifier to change the placement of the popper when the latter is trying to
131 * overlap its reference element. Defining `flip` as value, the placement will be flipped on
132 * its axis (`right - left`, `top - bottom`).
133 * You can even pass an array of placements (eg: `['right', 'left', 'top']` ) to manually specify
134 * how alter the placement when a flip is needed. (eg. in the above example, it would first flip from right to left,
135 * then, if even in its new placement, the popper is overlapping its reference element, it will be moved to top)
136 *
137 * @param {Array} [options.modifiers=[ 'shift', 'offset', 'preventOverflow', 'keepTogether', 'arrow', 'flip', 'applyStyle']]
138 * List of functions used to modify the data before they are applied to the popper, add your custom functions
139 * to this array to edit the offsets and placement.
140 * The function should reflect the @params and @returns of preventOverflow
141 *
142 * @param {Array} [options.modifiersIgnored=[]]
143 * Put here any built-in modifier name you want to exclude from the modifiers list
144 * The function should reflect the @params and @returns of preventOverflow
145 *
146 * @param {Boolean} [options.removeOnDestroy=false]
147 * Set to true if you want to automatically remove the popper when you call the `destroy` method.
148 */
149 function Popper(reference, popper, options) {
150 this._reference = reference.jquery ? reference[0] : reference;
151 this.state = {};
152
153 // if the popper variable is a configuration object, parse it to generate an HTMLElement
154 // generate a default popper if is not defined
155 var isNotDefined = typeof popper === 'undefined' || popper === null;
156 var isConfig = popper && Object.prototype.toString.call(popper) === '[object Object]';
157 if (isNotDefined || isConfig) {
158 this._popper = this.parse(isConfig ? popper : {});
159 }
160 // otherwise, use the given HTMLElement as popper
161 else {
162 this._popper = popper.jquery ? popper[0] : popper;
163 }
164
165 // with {} we create a new object with the options inside it
166 this._options = Object.assign({}, DEFAULTS, options);
167
168 // refactoring modifiers' list
169 this._options.modifiers = this._options.modifiers.map(function(modifier){
170 // remove ignored modifiers
171 if (this._options.modifiersIgnored.indexOf(modifier) !== -1) return;
172
173 // set the x-placement attribute before everything else because it could be used to add margins to the popper
174 // margins needs to be calculated to get the correct popper offsets
175 if (modifier === 'applyStyle') {
176 this._popper.setAttribute('x-placement', this._options.placement);
177 }
178
179 // return predefined modifier identified by string or keep the custom one
180 return this.modifiers[modifier] || modifier;
181 }.bind(this));
182
183 // make sure to apply the popper position before any computation
184 this.state.position = this._getPosition(this._popper, this._reference);
185 setStyle(this._popper, { position: this.state.position, top: 0 });
186
187 // fire the first update to position the popper in the right place
188 this.update();
189
190 // setup event listeners, they will take care of update the position in specific situations
191 this._setupEventListeners();
192 return this;
193 }
194
195
196 //
197 // Methods
198 //
199 /**
200 * Destroy the popper
201 * @method
202 * @memberof Popper
203 */
204 Popper.prototype.destroy = function() {
205 this._popper.removeAttribute('x-placement');
206 this._popper.style.left = '';
207 this._popper.style.position = '';
208 this._popper.style.top = '';
209 this._popper.style[getSupportedPropertyName('transform')] = '';
210 this._removeEventListeners();
211
212 // remove the popper if user explicity asked for the deletion on destroy
213 if (this._options.removeOnDestroy) {
214 this._popper.remove();
215 }
216 return this;
217 };
218
219 /**
220 * Updates the position of the popper, computing the new offsets and applying the new style
221 * @method
222 * @memberof Popper
223 */
224 Popper.prototype.update = function() {
225 var data = { instance: this, styles: {} };
226
227 // store placement inside the data object, modifiers will be able to edit `placement` if needed
228 // and refer to _originalPlacement to know the original value
229 data.placement = this._options.placement;
230 data._originalPlacement = this._options.placement;
231
232 // compute the popper and reference offsets and put them inside data.offsets
233 data.offsets = this._getOffsets(this._popper, this._reference, data.placement);
234
235 // get boundaries
236 data.boundaries = this._getBoundaries(data, this._options.boundariesPadding, this._options.boundariesElement);
237
238 data = this.runModifiers(data, this._options.modifiers);
239
240 if (typeof this.state.updateCallback === 'function') {
241 this.state.updateCallback(data);
242 }
243 };
244
245 /**
246 * If a function is passed, it will be executed after the initialization of popper with as first argument the Popper instance.
247 * @method
248 * @memberof Popper
249 * @param {Function} callback
250 */
251 Popper.prototype.onCreate = function(callback) {
252 // the createCallbacks return as first argument the popper instance
253 callback(this);
254 return this;
255 };
256
257 /**
258 * If a function is passed, it will be executed after each update of popper with as first argument the set of coordinates and informations
259 * used to style popper and its arrow.
260 * NOTE: it doesn't get fired on the first call of the `Popper.update()` method inside the `Popper` constructor!
261 * @method
262 * @memberof Popper
263 * @param {Function} callback
264 */
265 Popper.prototype.onUpdate = function(callback) {
266 this.state.updateCallback = callback;
267 return this;
268 };
269
270 /**
271 * Helper used to generate poppers from a configuration file
272 * @method
273 * @memberof Popper
274 * @param config {Object} configuration
275 * @returns {HTMLElement} popper
276 */
277 Popper.prototype.parse = function(config) {
278 var defaultConfig = {
279 tagName: 'div',
280 classNames: [ 'popper' ],
281 attributes: [],
282 parent: root.document.body,
283 content: '',
284 contentType: 'text',
285 arrowTagName: 'div',
286 arrowClassNames: [ 'popper__arrow' ],
287 arrowAttributes: [ 'x-arrow']
288 };
289 config = Object.assign({}, defaultConfig, config);
290
291 var d = root.document;
292
293 var popper = d.createElement(config.tagName);
294 addClassNames(popper, config.classNames);
295 addAttributes(popper, config.attributes);
296 if (config.contentType === 'node') {
297 popper.appendChild(config.content.jquery ? config.content[0] : config.content);
298 }else if (config.contentType === 'html') {
299 popper.innerHTML = config.content;
300 } else {
301 popper.textContent = config.content;
302 }
303
304 if (config.arrowTagName) {
305 var arrow = d.createElement(config.arrowTagName);
306 addClassNames(arrow, config.arrowClassNames);
307 addAttributes(arrow, config.arrowAttributes);
308 popper.appendChild(arrow);
309 }
310
311 var parent = config.parent.jquery ? config.parent[0] : config.parent;
312
313 // if the given parent is a string, use it to match an element
314 // if more than one element is matched, the first one will be used as parent
315 // if no elements are matched, the script will throw an error
316 if (typeof parent === 'string') {
317 parent = d.querySelectorAll(config.parent);
318 if (parent.length > 1) {
319 console.warn('WARNING: the given `parent` query(' + config.parent + ') matched more than one element, the first one will be used');
320 }
321 if (parent.length === 0) {
322 throw 'ERROR: the given `parent` doesn\'t exists!';
323 }
324 parent = parent[0];
325 }
326 // if the given parent is a DOM nodes list or an array of nodes with more than one element,
327 // the first one will be used as parent
328 if (parent.length > 1 && parent instanceof Element === false) {
329 console.warn('WARNING: you have passed as parent a list of elements, the first one will be used');
330 parent = parent[0];
331 }
332
333 // append the generated popper to its parent
334 parent.appendChild(popper);
335
336 return popper;
337
338 /**
339 * Adds class names to the given element
340 * @function
341 * @ignore
342 * @param {HTMLElement} target
343 * @param {Array} classes
344 */
345 function addClassNames(element, classNames) {
346 classNames.forEach(function(className) {
347 element.classList.add(className);
348 });
349 }
350
351 /**
352 * Adds attributes to the given element
353 * @function
354 * @ignore
355 * @param {HTMLElement} target
356 * @param {Array} attributes
357 * @example
358 * addAttributes(element, [ 'data-info:foobar' ]);
359 */
360 function addAttributes(element, attributes) {
361 attributes.forEach(function(attribute) {
362 element.setAttribute(attribute.split(':')[0], attribute.split(':')[1] || '');
363 });
364 }
365
366 };
367
368 /**
369 * Helper used to get the position which will be applied to the popper
370 * @method
371 * @memberof Popper
372 * @param config {HTMLElement} popper element
373 * @param reference {HTMLElement} reference element
374 * @returns {String} position
375 */
376 Popper.prototype._getPosition = function(popper, reference) {
377 var container = getOffsetParent(reference);
378
379 if (this._options.forceAbsolute) {
380 return 'absolute';
381 }
382
383 // Decide if the popper will be fixed
384 // If the reference element is inside a fixed context, the popper will be fixed as well to allow them to scroll together
385 var isParentFixed = isFixed(reference, container);
386 return isParentFixed ? 'fixed' : 'absolute';
387 };
388
389 /**
390 * Get offsets to the popper
391 * @method
392 * @memberof Popper
393 * @access private
394 * @param {Element} popper - the popper element
395 * @param {Element} reference - the reference element (the popper will be relative to this)
396 * @returns {Object} An object containing the offsets which will be applied to the popper
397 */
398 Popper.prototype._getOffsets = function(popper, reference, placement) {
399 placement = placement.split('-')[0];
400 var popperOffsets = {};
401
402 popperOffsets.position = this.state.position;
403 var isParentFixed = popperOffsets.position === 'fixed';
404
405 //
406 // Get reference element position
407 //
408 var referenceOffsets = getOffsetRectRelativeToCustomParent(reference, getOffsetParent(popper), isParentFixed);
409
410 //
411 // Get popper sizes
412 //
413 var popperRect = getOuterSizes(popper);
414
415 //
416 // Compute offsets of popper
417 //
418
419 // depending by the popper placement we have to compute its offsets slightly differently
420 if (['right', 'left'].indexOf(placement) !== -1) {
421 popperOffsets.top = referenceOffsets.top + referenceOffsets.height / 2 - popperRect.height / 2;
422 if (placement === 'left') {
423 popperOffsets.left = referenceOffsets.left - popperRect.width;
424 } else {
425 popperOffsets.left = referenceOffsets.right;
426 }
427 } else {
428 popperOffsets.left = referenceOffsets.left + referenceOffsets.width / 2 - popperRect.width / 2;
429 if (placement === 'top') {
430 popperOffsets.top = referenceOffsets.top - popperRect.height;
431 } else {
432 popperOffsets.top = referenceOffsets.bottom;
433 }
434 }
435
436 // Add width and height to our offsets object
437 popperOffsets.width = popperRect.width;
438 popperOffsets.height = popperRect.height;
439
440 return {
441 popper: popperOffsets,
442 reference: referenceOffsets
443 };
444 };
445
446
447 /**
448 * Setup needed event listeners used to update the popper position
449 * @method
450 * @memberof Popper
451 * @access private
452 */
453 Popper.prototype._setupEventListeners = function() {
454 // NOTE: 1 DOM access here
455 this.state.updateBound = this.update.bind(this);
456 root.addEventListener('resize', this.state.updateBound);
457 // if the boundariesElement is window we don't need to listen for the scroll event
458 if (this._options.boundariesElement !== 'window') {
459 var target = getScrollParent(this._reference);
460 // here it could be both `body` or `documentElement` thanks to Firefox, we then check both
461 if (target === root.document.body || target === root.document.documentElement) {
462 target = root;
463 }
464 target.addEventListener('scroll', this.state.updateBound);
465 this.state.scrollTarget = target;
466 }
467 };
468
469 /**
470 * Remove event listeners used to update the popper position
471 * @method
472 * @memberof Popper
473 * @access private
474 */
475 Popper.prototype._removeEventListeners = function() {
476 // NOTE: 1 DOM access here
477 root.removeEventListener('resize', this.state.updateBound);
478 if (this._options.boundariesElement !== 'window' && this.state.scrollTarget) {
479 this.state.scrollTarget.removeEventListener('scroll', this.state.updateBound);
480 this.state.scrollTarget = null;
481 }
482 this.state.updateBound = null;
483 };
484
485 /**
486 * Computed the boundaries limits and return them
487 * @method
488 * @memberof Popper
489 * @access private
490 * @param {Object} data - Object containing the property "offsets" generated by `_getOffsets`
491 * @param {Number} padding - Boundaries padding
492 * @param {Element} boundariesElement - Element used to define the boundaries
493 * @returns {Object} Coordinates of the boundaries
494 */
495 Popper.prototype._getBoundaries = function(data, padding, boundariesElement) {
496 // NOTE: 1 DOM access here
497 var boundaries = {};
498 var width, height;
499 if (boundariesElement === 'window') {
500 var body = root.document.body,
501 html = root.document.documentElement;
502
503 height = Math.max( body.scrollHeight, body.offsetHeight, html.clientHeight, html.scrollHeight, html.offsetHeight );
504 width = Math.max( body.scrollWidth, body.offsetWidth, html.clientWidth, html.scrollWidth, html.offsetWidth );
505
506 boundaries = {
507 top: 0,
508 right: width,
509 bottom: height,
510 left: 0
511 };
512 } else if (boundariesElement === 'viewport') {
513 var offsetParent = getOffsetParent(this._popper);
514 var scrollParent = getScrollParent(this._popper);
515 var offsetParentRect = getOffsetRect(offsetParent);
516
517 // Thanks the fucking native API, `document.body.scrollTop` & `document.documentElement.scrollTop`
518 var getScrollTopValue = function (element) {
519 return element == document.body ? Math.max(document.documentElement.scrollTop, document.body.scrollTop) : element.scrollTop;
520 }
521 var getScrollLeftValue = function (element) {
522 return element == document.body ? Math.max(document.documentElement.scrollLeft, document.body.scrollLeft) : element.scrollLeft;
523 }
524
525 // if the popper is fixed we don't have to substract scrolling from the boundaries
526 var scrollTop = data.offsets.popper.position === 'fixed' ? 0 : getScrollTopValue(scrollParent);
527 var scrollLeft = data.offsets.popper.position === 'fixed' ? 0 : getScrollLeftValue(scrollParent);
528
529 boundaries = {
530 top: 0 - (offsetParentRect.top - scrollTop),
531 right: root.document.documentElement.clientWidth - (offsetParentRect.left - scrollLeft),
532 bottom: root.document.documentElement.clientHeight - (offsetParentRect.top - scrollTop),
533 left: 0 - (offsetParentRect.left - scrollLeft)
534 };
535 } else {
536 if (getOffsetParent(this._popper) === boundariesElement) {
537 boundaries = {
538 top: 0,
539 left: 0,
540 right: boundariesElement.clientWidth,
541 bottom: boundariesElement.clientHeight
542 };
543 } else {
544 boundaries = getOffsetRect(boundariesElement);
545 }
546 }
547 boundaries.left += padding;
548 boundaries.right -= padding;
549 boundaries.top = boundaries.top + padding;
550 boundaries.bottom = boundaries.bottom - padding;
551 return boundaries;
552 };
553
554
555 /**
556 * Loop trough the list of modifiers and run them in order, each of them will then edit the data object
557 * @method
558 * @memberof Popper
559 * @access public
560 * @param {Object} data
561 * @param {Array} modifiers
562 * @param {Function} ends
563 */
564 Popper.prototype.runModifiers = function(data, modifiers, ends) {
565 var modifiersToRun = modifiers.slice();
566 if (ends !== undefined) {
567 modifiersToRun = this._options.modifiers.slice(0, getArrayKeyIndex(this._options.modifiers, ends));
568 }
569
570 modifiersToRun.forEach(function(modifier) {
571 if (isFunction(modifier)) {
572 data = modifier.call(this, data);
573 }
574 }.bind(this));
575
576 return data;
577 };
578
579 /**
580 * Helper used to know if the given modifier depends from another one.
581 * @method
582 * @memberof Popper
583 * @param {String} requesting - name of requesting modifier
584 * @param {String} requested - name of requested modifier
585 * @returns {Boolean}
586 */
587 Popper.prototype.isModifierRequired = function(requesting, requested) {
588 var index = getArrayKeyIndex(this._options.modifiers, requesting);
589 return !!this._options.modifiers.slice(0, index).filter(function(modifier) {
590 return modifier === requested;
591 }).length;
592 };
593
594 //
595 // Modifiers
596 //
597
598 /**
599 * Modifiers list
600 * @namespace Popper.modifiers
601 * @memberof Popper
602 * @type {Object}
603 */
604 Popper.prototype.modifiers = {};
605
606 /**
607 * Apply the computed styles to the popper element
608 * @method
609 * @memberof Popper.modifiers
610 * @argument {Object} data - The data object generated by `update` method
611 * @returns {Object} The same data object
612 */
613 Popper.prototype.modifiers.applyStyle = function(data) {
614 // apply the final offsets to the popper
615 // NOTE: 1 DOM access here
616 var styles = {
617 position: data.offsets.popper.position
618 };
619
620 // round top and left to avoid blurry text
621 var left = Math.round(data.offsets.popper.left);
622 var top = Math.round(data.offsets.popper.top);
623
624 // if gpuAcceleration is set to true and transform is supported, we use `translate3d` to apply the position to the popper
625 // we automatically use the supported prefixed version if needed
626 var prefixedProperty;
627 if (this._options.gpuAcceleration && (prefixedProperty = getSupportedPropertyName('transform'))) {
628 styles[prefixedProperty] = 'translate3d(' + left + 'px, ' + top + 'px, 0)';
629 styles.top = 0;
630 styles.left = 0;
631 }
632 // othwerise, we use the standard `left` and `top` properties
633 else {
634 styles.left =left;
635 styles.top = top;
636 }
637
638 // any property present in `data.styles` will be applied to the popper,
639 // in this way we can make the 3rd party modifiers add custom styles to it
640 // Be aware, modifiers could override the properties defined in the previous
641 // lines of this modifier!
642 Object.assign(styles, data.styles);
643
644 setStyle(this._popper, styles);
645
646 // set an attribute which will be useful to style the tooltip (use it to properly position its arrow)
647 // NOTE: 1 DOM access here
648 this._popper.setAttribute('x-placement', data.placement);
649
650 // if the arrow modifier is required and the arrow style has been computed, apply the arrow style
651 if (this.isModifierRequired(this.modifiers.applyStyle, this.modifiers.arrow) && data.offsets.arrow) {
652 setStyle(data.arrowElement, data.offsets.arrow);
653 }
654
655 return data;
656 };
657
658 /**
659 * Modifier used to shift the popper on the start or end of its reference element side
660 * @method
661 * @memberof Popper.modifiers
662 * @argument {Object} data - The data object generated by `update` method
663 * @returns {Object} The data object, properly modified
664 */
665 Popper.prototype.modifiers.shift = function(data) {
666 var placement = data.placement;
667 var basePlacement = placement.split('-')[0];
668 var shiftVariation = placement.split('-')[1];
669
670 // if shift shiftVariation is specified, run the modifier
671 if (shiftVariation) {
672 var reference = data.offsets.reference;
673 var popper = getPopperClientRect(data.offsets.popper);
674
675 var shiftOffsets = {
676 y: {
677 start: { top: reference.top },
678 end: { top: reference.top + reference.height - popper.height }
679 },
680 x: {
681 start: { left: reference.left },
682 end: { left: reference.left + reference.width - popper.width }
683 }
684 };
685
686 var axis = ['bottom', 'top'].indexOf(basePlacement) !== -1 ? 'x' : 'y';
687
688 data.offsets.popper = Object.assign(popper, shiftOffsets[axis][shiftVariation]);
689 }
690
691 return data;
692 };
693
694
695 /**
696 * Modifier used to make sure the popper does not overflows from it's boundaries
697 * @method
698 * @memberof Popper.modifiers
699 * @argument {Object} data - The data object generated by `update` method
700 * @returns {Object} The data object, properly modified
701 */
702 Popper.prototype.modifiers.preventOverflow = function(data) {
703 var order = this._options.preventOverflowOrder;
704 var popper = getPopperClientRect(data.offsets.popper);
705
706 var check = {
707 left: function() {
708 var left = popper.left;
709 if (popper.left < data.boundaries.left) {
710 left = Math.max(popper.left, data.boundaries.left);
711 }
712 return { left: left };
713 },
714 right: function() {
715 var left = popper.left;
716 if (popper.right > data.boundaries.right) {
717 left = Math.min(popper.left, data.boundaries.right - popper.width);
718 }
719 return { left: left };
720 },
721 top: function() {
722 var top = popper.top;
723 if (popper.top < data.boundaries.top) {
724 top = Math.max(popper.top, data.boundaries.top);
725 }
726 return { top: top };
727 },
728 bottom: function() {
729 var top = popper.top;
730 if (popper.bottom > data.boundaries.bottom) {
731 top = Math.min(popper.top, data.boundaries.bottom - popper.height);
732 }
733 return { top: top };
734 }
735 };
736
737 order.forEach(function(direction) {
738 data.offsets.popper = Object.assign(popper, check[direction]());
739 });
740
741 return data;
742 };
743
744 /**
745 * Modifier used to make sure the popper is always near its reference
746 * @method
747 * @memberof Popper.modifiers
748 * @argument {Object} data - The data object generated by _update method
749 * @returns {Object} The data object, properly modified
750 */
751 Popper.prototype.modifiers.keepTogether = function(data) {
752 var popper = getPopperClientRect(data.offsets.popper);
753 var reference = data.offsets.reference;
754 var f = Math.floor;
755
756 if (popper.right < f(reference.left)) {
757 data.offsets.popper.left = f(reference.left) - popper.width;
758 }
759 if (popper.left > f(reference.right)) {
760 data.offsets.popper.left = f(reference.right);
761 }
762 if (popper.bottom < f(reference.top)) {
763 data.offsets.popper.top = f(reference.top) - popper.height;
764 }
765 if (popper.top > f(reference.bottom)) {
766 data.offsets.popper.top = f(reference.bottom);
767 }
768
769 return data;
770 };
771
772 /**
773 * Modifier used to flip the placement of the popper when the latter is starting overlapping its reference element.
774 * Requires the `preventOverflow` modifier before it in order to work.
775 * **NOTE:** This modifier will run all its previous modifiers everytime it tries to flip the popper!
776 * @method
777 * @memberof Popper.modifiers
778 * @argument {Object} data - The data object generated by _update method
779 * @returns {Object} The data object, properly modified
780 */
781 Popper.prototype.modifiers.flip = function(data) {
782 // check if preventOverflow is in the list of modifiers before the flip modifier.
783 // otherwise flip would not work as expected.
784 if (!this.isModifierRequired(this.modifiers.flip, this.modifiers.preventOverflow)) {
785 console.warn('WARNING: preventOverflow modifier is required by flip modifier in order to work, be sure to include it before flip!');
786 return data;
787 }
788
789 if (data.flipped && data.placement === data._originalPlacement) {
790 // seems like flip is trying to loop, probably there's not enough space on any of the flippable sides
791 return data;
792 }
793
794 var placement = data.placement.split('-')[0];
795 var placementOpposite = getOppositePlacement(placement);
796 var variation = data.placement.split('-')[1] || '';
797
798 var flipOrder = [];
799 if(this._options.flipBehavior === 'flip') {
800 flipOrder = [
801 placement,
802 placementOpposite
803 ];
804 } else {
805 flipOrder = this._options.flipBehavior;
806 }
807
808 flipOrder.forEach(function(step, index) {
809 if (placement !== step || flipOrder.length === index + 1) {
810 return;
811 }
812
813 placement = data.placement.split('-')[0];
814 placementOpposite = getOppositePlacement(placement);
815
816 var popperOffsets = getPopperClientRect(data.offsets.popper);
817
818 // this boolean is used to distinguish right and bottom from top and left
819 // they need different computations to get flipped
820 var a = ['right', 'bottom'].indexOf(placement) !== -1;
821
822 // using Math.floor because the reference offsets may contain decimals we are not going to consider here
823 if (
824 a && Math.floor(data.offsets.reference[placement]) > Math.floor(popperOffsets[placementOpposite]) ||
825 !a && Math.floor(data.offsets.reference[placement]) < Math.floor(popperOffsets[placementOpposite])
826 ) {
827 // we'll use this boolean to detect any flip loop
828 data.flipped = true;
829 data.placement = flipOrder[index + 1];
830 if (variation) {
831 data.placement += '-' + variation;
832 }
833 data.offsets.popper = this._getOffsets(this._popper, this._reference, data.placement).popper;
834
835 data = this.runModifiers(data, this._options.modifiers, this._flip);
836 }
837 }.bind(this));
838 return data;
839 };
840
841 /**
842 * Modifier used to add an offset to the popper, useful if you more granularity positioning your popper.
843 * The offsets will shift the popper on the side of its reference element.
844 * @method
845 * @memberof Popper.modifiers
846 * @argument {Object} data - The data object generated by _update method
847 * @returns {Object} The data object, properly modified
848 */
849 Popper.prototype.modifiers.offset = function(data) {
850 var offset = this._options.offset;
851 var popper = data.offsets.popper;
852
853 if (data.placement.indexOf('left') !== -1) {
854 popper.top -= offset;
855 }
856 else if (data.placement.indexOf('right') !== -1) {
857 popper.top += offset;
858 }
859 else if (data.placement.indexOf('top') !== -1) {
860 popper.left -= offset;
861 }
862 else if (data.placement.indexOf('bottom') !== -1) {
863 popper.left += offset;
864 }
865 return data;
866 };
867
868 /**
869 * Modifier used to move the arrows on the edge of the popper to make sure them are always between the popper and the reference element
870 * It will use the CSS outer size of the arrow element to know how many pixels of conjuction are needed
871 * @method
872 * @memberof Popper.modifiers
873 * @argument {Object} data - The data object generated by _update method
874 * @returns {Object} The data object, properly modified
875 */
876 Popper.prototype.modifiers.arrow = function(data) {
877 var arrow = this._options.arrowElement;
878 var arrowOffset = this._options.arrowOffset;
879
880 // if the arrowElement is a string, suppose it's a CSS selector
881 if (typeof arrow === 'string') {
882 arrow = this._popper.querySelector(arrow);
883 }
884
885 // if arrow element is not found, don't run the modifier
886 if (!arrow) {
887 return data;
888 }
889
890 // the arrow element must be child of its popper
891 if (!this._popper.contains(arrow)) {
892 console.warn('WARNING: `arrowElement` must be child of its popper element!');
893 return data;
894 }
895
896 // arrow depends on keepTogether in order to work
897 if (!this.isModifierRequired(this.modifiers.arrow, this.modifiers.keepTogether)) {
898 console.warn('WARNING: keepTogether modifier is required by arrow modifier in order to work, be sure to include it before arrow!');
899 return data;
900 }
901
902 var arrowStyle = {};
903 var placement = data.placement.split('-')[0];
904 var popper = getPopperClientRect(data.offsets.popper);
905 var reference = data.offsets.reference;
906 var isVertical = ['left', 'right'].indexOf(placement) !== -1;
907
908 var len = isVertical ? 'height' : 'width';
909 var side = isVertical ? 'top' : 'left';
910 var translate = isVertical ? 'translateY' : 'translateX';
911 var altSide = isVertical ? 'left' : 'top';
912 var opSide = isVertical ? 'bottom' : 'right';
913 var arrowSize = getOuterSizes(arrow)[len];
914
915 //
916 // extends keepTogether behavior making sure the popper and its reference have enough pixels in conjuction
917 //
918
919 // top/left side
920 if (reference[opSide] - arrowSize < popper[side]) {
921 data.offsets.popper[side] -= popper[side] - (reference[opSide] - arrowSize);
922 }
923 // bottom/right side
924 if (reference[side] + arrowSize > popper[opSide]) {
925 data.offsets.popper[side] += (reference[side] + arrowSize) - popper[opSide];
926 }
927
928 // compute center of the popper
929 var center = reference[side] + (arrowOffset || (reference[len] / 2) - (arrowSize / 2));
930
931 var sideValue = center - popper[side];
932
933 // prevent arrow from being placed not contiguously to its popper
934 sideValue = Math.max(Math.min(popper[len] - arrowSize - 8, sideValue), 8);
935 arrowStyle[side] = sideValue;
936 arrowStyle[altSide] = ''; // make sure to remove any old style from the arrow
937
938 data.offsets.arrow = arrowStyle;
939 data.arrowElement = arrow;
940
941 return data;
942 };
943
944
945 //
946 // Helpers
947 //
948
949 /**
950 * Get the outer sizes of the given element (offset size + margins)
951 * @function
952 * @ignore
953 * @argument {Element} element
954 * @returns {Object} object containing width and height properties
955 */
956 function getOuterSizes(element) {
957 // NOTE: 1 DOM access here
958 var _display = element.style.display, _visibility = element.style.visibility;
959 element.style.display = 'block'; element.style.visibility = 'hidden';
960 var calcWidthToForceRepaint = element.offsetWidth;
961
962 // original method
963 var styles = root.getComputedStyle(element);
964 var x = parseFloat(styles.marginTop) + parseFloat(styles.marginBottom);
965 var y = parseFloat(styles.marginLeft) + parseFloat(styles.marginRight);
966 var result = { width: element.offsetWidth + y, height: element.offsetHeight + x };
967
968 // reset element styles
969 element.style.display = _display; element.style.visibility = _visibility;
970 return result;
971 }
972
973 /**
974 * Get the opposite placement of the given one/
975 * @function
976 * @ignore
977 * @argument {String} placement
978 * @returns {String} flipped placement
979 */
980 function getOppositePlacement(placement) {
981 var hash = {left: 'right', right: 'left', bottom: 'top', top: 'bottom' };
982 return placement.replace(/left|right|bottom|top/g, function(matched){
983 return hash[matched];
984 });
985 }
986
987 /**
988 * Given the popper offsets, generate an output similar to getBoundingClientRect
989 * @function
990 * @ignore
991 * @argument {Object} popperOffsets
992 * @returns {Object} ClientRect like output
993 */
994 function getPopperClientRect(popperOffsets) {
995 var offsets = Object.assign({}, popperOffsets);
996 offsets.right = offsets.left + offsets.width;
997 offsets.bottom = offsets.top + offsets.height;
998 return offsets;
999 }
1000
1001 /**
1002 * Given an array and the key to find, returns its index
1003 * @function
1004 * @ignore
1005 * @argument {Array} arr
1006 * @argument keyToFind
1007 * @returns index or null
1008 */
1009 function getArrayKeyIndex(arr, keyToFind) {
1010 var i = 0, key;
1011 for (key in arr) {
1012 if (arr[key] === keyToFind) {
1013 return i;
1014 }
1015 i++;
1016 }
1017 return null;
1018 }
1019
1020 /**
1021 * Get CSS computed property of the given element
1022 * @function
1023 * @ignore
1024 * @argument {Eement} element
1025 * @argument {String} property
1026 */
1027 function getStyleComputedProperty(element, property) {
1028 // NOTE: 1 DOM access here
1029 var css = root.getComputedStyle(element, null);
1030 return css[property];
1031 }
1032
1033 /**
1034 * Returns the offset parent of the given element
1035 * @function
1036 * @ignore
1037 * @argument {Element} element
1038 * @returns {Element} offset parent
1039 */
1040 function getOffsetParent(element) {
1041 // NOTE: 1 DOM access here
1042 var offsetParent = element.offsetParent;
1043 return offsetParent === root.document.body || !offsetParent ? root.document.documentElement : offsetParent;
1044 }
1045
1046 /**
1047 * Returns the scrolling parent of the given element
1048 * @function
1049 * @ignore
1050 * @argument {Element} element
1051 * @returns {Element} offset parent
1052 */
1053 function getScrollParent(element) {
1054 var parent = element.parentNode;
1055
1056 if (!parent) {
1057 return element;
1058 }
1059
1060 if (parent === root.document) {
1061 // Firefox puts the scrollTOp value on `documentElement` instead of `body`, we then check which of them is
1062 // greater than 0 and return the proper element
1063 if (root.document.body.scrollTop || root.document.body.scrollLeft) {
1064 return root.document.body;
1065 } else {
1066 return root.document.documentElement;
1067 }
1068 }
1069
1070 // Firefox want us to check `-x` and `-y` variations as well
1071 if (
1072 ['scroll', 'auto'].indexOf(getStyleComputedProperty(parent, 'overflow')) !== -1 ||
1073 ['scroll', 'auto'].indexOf(getStyleComputedProperty(parent, 'overflow-x')) !== -1 ||
1074 ['scroll', 'auto'].indexOf(getStyleComputedProperty(parent, 'overflow-y')) !== -1
1075 ) {
1076 // If the detected scrollParent is body, we perform an additional check on its parentNode
1077 // in this way we'll get body if the browser is Chrome-ish, or documentElement otherwise
1078 // fixes issue #65
1079 return parent;
1080 }
1081 return getScrollParent(element.parentNode);
1082 }
1083
1084 /**
1085 * Check if the given element is fixed or is inside a fixed parent
1086 * @function
1087 * @ignore
1088 * @argument {Element} element
1089 * @argument {Element} customContainer
1090 * @returns {Boolean} answer to "isFixed?"
1091 */
1092 function isFixed(element) {
1093 if (element === root.document.body) {
1094 return false;
1095 }
1096 if (getStyleComputedProperty(element, 'position') === 'fixed') {
1097 return true;
1098 }
1099 return element.parentNode ? isFixed(element.parentNode) : element;
1100 }
1101
1102 /**
1103 * Set the style to the given popper
1104 * @function
1105 * @ignore
1106 * @argument {Element} element - Element to apply the style to
1107 * @argument {Object} styles - Object with a list of properties and values which will be applied to the element
1108 */
1109 function setStyle(element, styles) {
1110 function is_numeric(n) {
1111 return (n !== '' && !isNaN(parseFloat(n)) && isFinite(n));
1112 }
1113 Object.keys(styles).forEach(function(prop) {
1114 var unit = '';
1115 // add unit if the value is numeric and is one of the following
1116 if (['width', 'height', 'top', 'right', 'bottom', 'left'].indexOf(prop) !== -1 && is_numeric(styles[prop])) {
1117 unit = 'px';
1118 }
1119 element.style[prop] = styles[prop] + unit;
1120 });
1121 }
1122
1123 /**
1124 * Check if the given variable is a function
1125 * @function
1126 * @ignore
1127 * @argument {*} functionToCheck - variable to check
1128 * @returns {Boolean} answer to: is a function?
1129 */
1130 function isFunction(functionToCheck) {
1131 var getType = {};
1132 return functionToCheck && getType.toString.call(functionToCheck) === '[object Function]';
1133 }
1134
1135 /**
1136 * Get the position of the given element, relative to its offset parent
1137 * @function
1138 * @ignore
1139 * @param {Element} element
1140 * @return {Object} position - Coordinates of the element and its `scrollTop`
1141 */
1142 function getOffsetRect(element) {
1143 var elementRect = {
1144 width: element.offsetWidth,
1145 height: element.offsetHeight,
1146 left: element.offsetLeft,
1147 top: element.offsetTop
1148 };
1149
1150 elementRect.right = elementRect.left + elementRect.width;
1151 elementRect.bottom = elementRect.top + elementRect.height;
1152
1153 // position
1154 return elementRect;
1155 }
1156
1157 /**
1158 * Get bounding client rect of given element
1159 * @function
1160 * @ignore
1161 * @param {HTMLElement} element
1162 * @return {Object} client rect
1163 */
1164 function getBoundingClientRect(element) {
1165 var rect = element.getBoundingClientRect();
1166
1167 // whether the IE version is lower than 11
1168 var isIE = navigator.userAgent.indexOf("MSIE") != -1;
1169
1170 // fix ie document bounding top always 0 bug
1171 var rectTop = isIE && element.tagName === 'HTML'
1172 ? -element.scrollTop
1173 : rect.top;
1174
1175 return {
1176 left: rect.left,
1177 top: rectTop,
1178 right: rect.right,
1179 bottom: rect.bottom,
1180 width: rect.right - rect.left,
1181 height: rect.bottom - rectTop
1182 };
1183 }
1184
1185 /**
1186 * Given an element and one of its parents, return the offset
1187 * @function
1188 * @ignore
1189 * @param {HTMLElement} element
1190 * @param {HTMLElement} parent
1191 * @return {Object} rect
1192 */
1193 function getOffsetRectRelativeToCustomParent(element, parent, fixed) {
1194 var elementRect = getBoundingClientRect(element);
1195 var parentRect = getBoundingClientRect(parent);
1196
1197 if (fixed) {
1198 var scrollParent = getScrollParent(parent);
1199 parentRect.top += scrollParent.scrollTop;
1200 parentRect.bottom += scrollParent.scrollTop;
1201 parentRect.left += scrollParent.scrollLeft;
1202 parentRect.right += scrollParent.scrollLeft;
1203 }
1204
1205 var rect = {
1206 top: elementRect.top - parentRect.top ,
1207 left: elementRect.left - parentRect.left ,
1208 bottom: (elementRect.top - parentRect.top) + elementRect.height,
1209 right: (elementRect.left - parentRect.left) + elementRect.width,
1210 width: elementRect.width,
1211 height: elementRect.height
1212 };
1213 return rect;
1214 }
1215
1216 /**
1217 * Get the prefixed supported property name
1218 * @function
1219 * @ignore
1220 * @argument {String} property (camelCase)
1221 * @returns {String} prefixed property (camelCase)
1222 */
1223 function getSupportedPropertyName(property) {
1224 var prefixes = ['', 'ms', 'webkit', 'moz', 'o'];
1225
1226 for (var i = 0; i < prefixes.length; i++) {
1227 var toCheck = prefixes[i] ? prefixes[i] + property.charAt(0).toUpperCase() + property.slice(1) : property;
1228 if (typeof root.document.body.style[toCheck] !== 'undefined') {
1229 return toCheck;
1230 }
1231 }
1232 return null;
1233 }
1234
1235 /**
1236 * The Object.assign() method is used to copy the values of all enumerable own properties from one or more source
1237 * objects to a target object. It will return the target object.
1238 * This polyfill doesn't support symbol properties, since ES5 doesn't have symbols anyway
1239 * Source: https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Object/assign
1240 * @function
1241 * @ignore
1242 */
1243 if (!Object.assign) {
1244 Object.defineProperty(Object, 'assign', {
1245 enumerable: false,
1246 configurable: true,
1247 writable: true,
1248 value: function(target) {
1249 if (target === undefined || target === null) {
1250 throw new TypeError('Cannot convert first argument to object');
1251 }
1252
1253 var to = Object(target);
1254 for (var i = 1; i < arguments.length; i++) {
1255 var nextSource = arguments[i];
1256 if (nextSource === undefined || nextSource === null) {
1257 continue;
1258 }
1259 nextSource = Object(nextSource);
1260
1261 var keysArray = Object.keys(nextSource);
1262 for (var nextIndex = 0, len = keysArray.length; nextIndex < len; nextIndex++) {
1263 var nextKey = keysArray[nextIndex];
1264 var desc = Object.getOwnPropertyDescriptor(nextSource, nextKey);
1265 if (desc !== undefined && desc.enumerable) {
1266 to[nextKey] = nextSource[nextKey];
1267 }
1268 }
1269 }
1270 return to;
1271 }
1272 });
1273 }
1274
1275 return Popper;
1276}));