aboutsummaryrefslogtreecommitdiff
path: root/js/src
diff options
context:
space:
mode:
Diffstat (limited to 'js/src')
-rw-r--r--js/src/alert.js37
-rw-r--r--js/src/base-component.js14
-rw-r--r--js/src/button.js23
-rw-r--r--js/src/carousel.js171
-rw-r--r--js/src/collapse.js40
-rw-r--r--js/src/dom/data.js82
-rw-r--r--js/src/dom/event-handler.js41
-rw-r--r--js/src/dom/manipulator.js2
-rw-r--r--js/src/dom/selector-engine.js15
-rw-r--r--js/src/dropdown.js359
-rw-r--r--js/src/modal.js286
-rw-r--r--js/src/offcanvas.js285
-rw-r--r--js/src/popover.js23
-rw-r--r--js/src/scrollspy.js44
-rw-r--r--js/src/tab.js58
-rw-r--r--js/src/toast.js28
-rw-r--r--js/src/tooltip.js328
-rw-r--r--js/src/util/backdrop.js133
-rw-r--r--js/src/util/index.js76
-rw-r--r--js/src/util/sanitizer.js8
-rw-r--r--js/src/util/scrollbar.js81
21 files changed, 1220 insertions, 914 deletions
diff --git a/js/src/alert.js b/js/src/alert.js
index f1f612232..a25c44ec3 100644
--- a/js/src/alert.js
+++ b/js/src/alert.js
@@ -1,14 +1,12 @@
/**
* --------------------------------------------------------------------------
- * Bootstrap (v5.0.0-alpha3): alert.js
+ * Bootstrap (v5.0.0-beta3): alert.js
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
* --------------------------------------------------------------------------
*/
import {
- getjQuery,
- onDOMContentLoaded,
- TRANSITION_END,
+ defineJQueryPlugin,
emulateTransitionEnd,
getElementFromSelector,
getTransitionDurationFromElement
@@ -34,9 +32,9 @@ const EVENT_CLOSE = `close${EVENT_KEY}`
const EVENT_CLOSED = `closed${EVENT_KEY}`
const EVENT_CLICK_DATA_API = `click${EVENT_KEY}${DATA_API_KEY}`
-const CLASSNAME_ALERT = 'alert'
-const CLASSNAME_FADE = 'fade'
-const CLASSNAME_SHOW = 'show'
+const CLASS_NAME_ALERT = 'alert'
+const CLASS_NAME_FADE = 'fade'
+const CLASS_NAME_SHOW = 'show'
/**
* ------------------------------------------------------------------------
@@ -67,7 +65,7 @@ class Alert extends BaseComponent {
// Private
_getRootElement(element) {
- return getElementFromSelector(element) || element.closest(`.${CLASSNAME_ALERT}`)
+ return getElementFromSelector(element) || element.closest(`.${CLASS_NAME_ALERT}`)
}
_triggerCloseEvent(element) {
@@ -75,16 +73,16 @@ class Alert extends BaseComponent {
}
_removeElement(element) {
- element.classList.remove(CLASSNAME_SHOW)
+ element.classList.remove(CLASS_NAME_SHOW)
- if (!element.classList.contains(CLASSNAME_FADE)) {
+ if (!element.classList.contains(CLASS_NAME_FADE)) {
this._destroyElement(element)
return
}
const transitionDuration = getTransitionDurationFromElement(element)
- EventHandler.one(element, TRANSITION_END, () => this._destroyElement(element))
+ EventHandler.one(element, 'transitionend', () => this._destroyElement(element))
emulateTransitionEnd(element, transitionDuration)
}
@@ -100,7 +98,7 @@ class Alert extends BaseComponent {
static jQueryInterface(config) {
return this.each(function () {
- let data = Data.getData(this, DATA_KEY)
+ let data = Data.get(this, DATA_KEY)
if (!data) {
data = new Alert(this)
@@ -128,6 +126,7 @@ class Alert extends BaseComponent {
* Data Api implementation
* ------------------------------------------------------------------------
*/
+
EventHandler.on(document, EVENT_CLICK_DATA_API, SELECTOR_DISMISS, Alert.handleDismiss(new Alert()))
/**
@@ -137,18 +136,6 @@ EventHandler.on(document, EVENT_CLICK_DATA_API, SELECTOR_DISMISS, Alert.handleDi
* add .Alert to jQuery only if jQuery is present
*/
-onDOMContentLoaded(() => {
- const $ = getjQuery()
- /* istanbul ignore if */
- if ($) {
- const JQUERY_NO_CONFLICT = $.fn[NAME]
- $.fn[NAME] = Alert.jQueryInterface
- $.fn[NAME].Constructor = Alert
- $.fn[NAME].noConflict = () => {
- $.fn[NAME] = JQUERY_NO_CONFLICT
- return Alert.jQueryInterface
- }
- }
-})
+defineJQueryPlugin(NAME, Alert)
export default Alert
diff --git a/js/src/base-component.js b/js/src/base-component.js
index 776a0052b..77d54faad 100644
--- a/js/src/base-component.js
+++ b/js/src/base-component.js
@@ -1,11 +1,12 @@
/**
* --------------------------------------------------------------------------
- * Bootstrap (v5.0.0-alpha3): base-component.js
+ * Bootstrap (v5.0.0-beta3): base-component.js
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
* --------------------------------------------------------------------------
*/
import Data from './dom/data'
+import EventHandler from './dom/event-handler'
/**
* ------------------------------------------------------------------------
@@ -13,27 +14,30 @@ import Data from './dom/data'
* ------------------------------------------------------------------------
*/
-const VERSION = '5.0.0-alpha3'
+const VERSION = '5.0.0-beta3'
class BaseComponent {
constructor(element) {
+ element = typeof element === 'string' ? document.querySelector(element) : element
+
if (!element) {
return
}
this._element = element
- Data.setData(element, this.constructor.DATA_KEY, this)
+ Data.set(this._element, this.constructor.DATA_KEY, this)
}
dispose() {
- Data.removeData(this._element, this.constructor.DATA_KEY)
+ Data.remove(this._element, this.constructor.DATA_KEY)
+ EventHandler.off(this._element, `.${this.constructor.DATA_KEY}`)
this._element = null
}
/** Static */
static getInstance(element) {
- return Data.getData(element, this.DATA_KEY)
+ return Data.get(element, this.DATA_KEY)
}
static get VERSION() {
diff --git a/js/src/button.js b/js/src/button.js
index 240995564..093679e90 100644
--- a/js/src/button.js
+++ b/js/src/button.js
@@ -1,11 +1,11 @@
/**
* --------------------------------------------------------------------------
- * Bootstrap (v5.0.0-alpha3): button.js
+ * Bootstrap (v5.0.0-beta3): button.js
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
* --------------------------------------------------------------------------
*/
-import { getjQuery, onDOMContentLoaded } from './util/index'
+import { defineJQueryPlugin } from './util/index'
import Data from './dom/data'
import EventHandler from './dom/event-handler'
import BaseComponent from './base-component'
@@ -51,7 +51,7 @@ class Button extends BaseComponent {
static jQueryInterface(config) {
return this.each(function () {
- let data = Data.getData(this, DATA_KEY)
+ let data = Data.get(this, DATA_KEY)
if (!data) {
data = new Button(this)
@@ -75,7 +75,7 @@ EventHandler.on(document, EVENT_CLICK_DATA_API, SELECTOR_DATA_TOGGLE, event => {
const button = event.target.closest(SELECTOR_DATA_TOGGLE)
- let data = Data.getData(button, DATA_KEY)
+ let data = Data.get(button, DATA_KEY)
if (!data) {
data = new Button(button)
}
@@ -90,19 +90,6 @@ EventHandler.on(document, EVENT_CLICK_DATA_API, SELECTOR_DATA_TOGGLE, event => {
* add .Button to jQuery only if jQuery is present
*/
-onDOMContentLoaded(() => {
- const $ = getjQuery()
- /* istanbul ignore if */
- if ($) {
- const JQUERY_NO_CONFLICT = $.fn[NAME]
- $.fn[NAME] = Button.jQueryInterface
- $.fn[NAME].Constructor = Button
-
- $.fn[NAME].noConflict = () => {
- $.fn[NAME] = JQUERY_NO_CONFLICT
- return Button.jQueryInterface
- }
- }
-})
+defineJQueryPlugin(NAME, Button)
export default Button
diff --git a/js/src/carousel.js b/js/src/carousel.js
index d8ad3a135..ebb0b7b20 100644
--- a/js/src/carousel.js
+++ b/js/src/carousel.js
@@ -1,17 +1,16 @@
/**
* --------------------------------------------------------------------------
- * Bootstrap (v5.0.0-alpha3): carousel.js
+ * Bootstrap (v5.0.0-beta3): carousel.js
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
* --------------------------------------------------------------------------
*/
import {
- getjQuery,
- onDOMContentLoaded,
- TRANSITION_END,
+ defineJQueryPlugin,
emulateTransitionEnd,
getElementFromSelector,
getTransitionDurationFromElement,
+ isRTL,
isVisible,
reflow,
triggerTransitionEnd,
@@ -57,8 +56,8 @@ const DefaultType = {
touch: 'boolean'
}
-const DIRECTION_NEXT = 'next'
-const DIRECTION_PREV = 'prev'
+const ORDER_NEXT = 'next'
+const ORDER_PREV = 'prev'
const DIRECTION_LEFT = 'left'
const DIRECTION_RIGHT = 'right'
@@ -91,13 +90,12 @@ const SELECTOR_ITEM = '.carousel-item'
const SELECTOR_ITEM_IMG = '.carousel-item img'
const SELECTOR_NEXT_PREV = '.carousel-item-next, .carousel-item-prev'
const SELECTOR_INDICATORS = '.carousel-indicators'
+const SELECTOR_INDICATOR = '[data-bs-target]'
const SELECTOR_DATA_SLIDE = '[data-bs-slide], [data-bs-slide-to]'
const SELECTOR_DATA_RIDE = '[data-bs-ride="carousel"]'
-const PointerType = {
- TOUCH: 'touch',
- PEN: 'pen'
-}
+const POINTER_TYPE_TOUCH = 'touch'
+const POINTER_TYPE_PEN = 'pen'
/**
* ------------------------------------------------------------------------
@@ -139,7 +137,7 @@ class Carousel extends BaseComponent {
next() {
if (!this._isSliding) {
- this._slide(DIRECTION_NEXT)
+ this._slide(ORDER_NEXT)
}
}
@@ -153,7 +151,7 @@ class Carousel extends BaseComponent {
prev() {
if (!this._isSliding) {
- this._slide(DIRECTION_PREV)
+ this._slide(ORDER_PREV)
}
}
@@ -210,17 +208,14 @@ class Carousel extends BaseComponent {
return
}
- const direction = index > activeIndex ?
- DIRECTION_NEXT :
- DIRECTION_PREV
+ const order = index > activeIndex ?
+ ORDER_NEXT :
+ ORDER_PREV
- this._slide(direction, this._items[index])
+ this._slide(order, this._items[index])
}
dispose() {
- super.dispose()
- EventHandler.off(this._element, EVENT_KEY)
-
this._items = null
this._config = null
this._interval = null
@@ -228,6 +223,8 @@ class Carousel extends BaseComponent {
this._isSliding = null
this._activeElement = null
this._indicatorsElement = null
+
+ super.dispose()
}
// Private
@@ -252,15 +249,11 @@ class Carousel extends BaseComponent {
this.touchDeltaX = 0
- // swipe left
- if (direction > 0) {
- this.prev()
+ if (!direction) {
+ return
}
- // swipe right
- if (direction < 0) {
- this.next()
- }
+ this._slide(direction > 0 ? DIRECTION_RIGHT : DIRECTION_LEFT)
}
_addEventListeners() {
@@ -280,7 +273,7 @@ class Carousel extends BaseComponent {
_addTouchEventListeners() {
const start = event => {
- if (this._pointerEvent && PointerType[event.pointerType.toUpperCase()]) {
+ if (this._pointerEvent && (event.pointerType === POINTER_TYPE_PEN || event.pointerType === POINTER_TYPE_TOUCH)) {
this.touchStartX = event.clientX
} else if (!this._pointerEvent) {
this.touchStartX = event.touches[0].clientX
@@ -289,15 +282,13 @@ class Carousel extends BaseComponent {
const move = event => {
// ensure swiping with one touch and not pinching
- if (event.touches && event.touches.length > 1) {
- this.touchDeltaX = 0
- } else {
- this.touchDeltaX = event.touches[0].clientX - this.touchStartX
- }
+ this.touchDeltaX = event.touches && event.touches.length > 1 ?
+ 0 :
+ event.touches[0].clientX - this.touchStartX
}
const end = event => {
- if (this._pointerEvent && PointerType[event.pointerType.toUpperCase()]) {
+ if (this._pointerEvent && (event.pointerType === POINTER_TYPE_PEN || event.pointerType === POINTER_TYPE_TOUCH)) {
this.touchDeltaX = event.clientX - this.touchStartX
}
@@ -341,16 +332,12 @@ class Carousel extends BaseComponent {
return
}
- switch (event.key) {
- case ARROW_LEFT_KEY:
- event.preventDefault()
- this.prev()
- break
- case ARROW_RIGHT_KEY:
- event.preventDefault()
- this.next()
- break
- default:
+ if (event.key === ARROW_LEFT_KEY) {
+ event.preventDefault()
+ this._slide(DIRECTION_RIGHT)
+ } else if (event.key === ARROW_RIGHT_KEY) {
+ event.preventDefault()
+ this._slide(DIRECTION_LEFT)
}
}
@@ -362,19 +349,18 @@ class Carousel extends BaseComponent {
return this._items.indexOf(element)
}
- _getItemByDirection(direction, activeElement) {
- const isNextDirection = direction === DIRECTION_NEXT
- const isPrevDirection = direction === DIRECTION_PREV
+ _getItemByOrder(order, activeElement) {
+ const isNext = order === ORDER_NEXT
+ const isPrev = order === ORDER_PREV
const activeIndex = this._getItemIndex(activeElement)
const lastItemIndex = this._items.length - 1
- const isGoingToWrap = (isPrevDirection && activeIndex === 0) ||
- (isNextDirection && activeIndex === lastItemIndex)
+ const isGoingToWrap = (isPrev && activeIndex === 0) || (isNext && activeIndex === lastItemIndex)
if (isGoingToWrap && !this._config.wrap) {
return activeElement
}
- const delta = direction === DIRECTION_PREV ? -1 : 1
+ const delta = isPrev ? -1 : 1
const itemIndex = (activeIndex + delta) % this._items.length
return itemIndex === -1 ?
@@ -396,18 +382,19 @@ class Carousel extends BaseComponent {
_setActiveIndicatorElement(element) {
if (this._indicatorsElement) {
- const indicators = SelectorEngine.find(SELECTOR_ACTIVE, this._indicatorsElement)
+ const activeIndicator = SelectorEngine.findOne(SELECTOR_ACTIVE, this._indicatorsElement)
- for (let i = 0; i < indicators.length; i++) {
- indicators[i].classList.remove(CLASS_NAME_ACTIVE)
- }
+ activeIndicator.classList.remove(CLASS_NAME_ACTIVE)
+ activeIndicator.removeAttribute('aria-current')
- const nextIndicator = this._indicatorsElement.children[
- this._getItemIndex(element)
- ]
+ const indicators = SelectorEngine.find(SELECTOR_INDICATOR, this._indicatorsElement)
- if (nextIndicator) {
- nextIndicator.classList.add(CLASS_NAME_ACTIVE)
+ for (let i = 0; i < indicators.length; i++) {
+ if (Number.parseInt(indicators[i].getAttribute('data-bs-slide-to'), 10) === this._getItemIndex(element)) {
+ indicators[i].classList.add(CLASS_NAME_ACTIVE)
+ indicators[i].setAttribute('aria-current', 'true')
+ break
+ }
}
}
}
@@ -429,27 +416,19 @@ class Carousel extends BaseComponent {
}
}
- _slide(direction, element) {
+ _slide(directionOrOrder, element) {
+ const order = this._directionToOrder(directionOrOrder)
const activeElement = SelectorEngine.findOne(SELECTOR_ACTIVE_ITEM, this._element)
const activeElementIndex = this._getItemIndex(activeElement)
- const nextElement = element || (activeElement && this._getItemByDirection(direction, activeElement))
+ const nextElement = element || this._getItemByOrder(order, activeElement)
const nextElementIndex = this._getItemIndex(nextElement)
const isCycling = Boolean(this._interval)
- let directionalClassName
- let orderClassName
- let eventDirectionName
-
- if (direction === DIRECTION_NEXT) {
- directionalClassName = CLASS_NAME_START
- orderClassName = CLASS_NAME_NEXT
- eventDirectionName = DIRECTION_LEFT
- } else {
- directionalClassName = CLASS_NAME_END
- orderClassName = CLASS_NAME_PREV
- eventDirectionName = DIRECTION_RIGHT
- }
+ const isNext = order === ORDER_NEXT
+ const directionalClassName = isNext ? CLASS_NAME_START : CLASS_NAME_END
+ const orderClassName = isNext ? CLASS_NAME_NEXT : CLASS_NAME_PREV
+ const eventDirectionName = this._orderToDirection(order)
if (nextElement && nextElement.classList.contains(CLASS_NAME_ACTIVE)) {
this._isSliding = false
@@ -485,7 +464,7 @@ class Carousel extends BaseComponent {
const transitionDuration = getTransitionDurationFromElement(activeElement)
- EventHandler.one(activeElement, TRANSITION_END, () => {
+ EventHandler.one(activeElement, 'transitionend', () => {
nextElement.classList.remove(directionalClassName, orderClassName)
nextElement.classList.add(CLASS_NAME_ACTIVE)
@@ -522,10 +501,34 @@ class Carousel extends BaseComponent {
}
}
+ _directionToOrder(direction) {
+ if (![DIRECTION_RIGHT, DIRECTION_LEFT].includes(direction)) {
+ return direction
+ }
+
+ if (isRTL()) {
+ return direction === DIRECTION_LEFT ? ORDER_PREV : ORDER_NEXT
+ }
+
+ return direction === DIRECTION_LEFT ? ORDER_NEXT : ORDER_PREV
+ }
+
+ _orderToDirection(order) {
+ if (![ORDER_NEXT, ORDER_PREV].includes(order)) {
+ return order
+ }
+
+ if (isRTL()) {
+ return order === ORDER_PREV ? DIRECTION_LEFT : DIRECTION_RIGHT
+ }
+
+ return order === ORDER_PREV ? DIRECTION_RIGHT : DIRECTION_LEFT
+ }
+
// Static
static carouselInterface(element, config) {
- let data = Data.getData(element, DATA_KEY)
+ let data = Data.get(element, DATA_KEY)
let _config = {
...Default,
...Manipulator.getDataAttributes(element)
@@ -584,7 +587,7 @@ class Carousel extends BaseComponent {
Carousel.carouselInterface(target, config)
if (slideIndex) {
- Data.getData(target, DATA_KEY).to(slideIndex)
+ Data.get(target, DATA_KEY).to(slideIndex)
}
event.preventDefault()
@@ -603,7 +606,7 @@ EventHandler.on(window, EVENT_LOAD_DATA_API, () => {
const carousels = SelectorEngine.find(SELECTOR_DATA_RIDE)
for (let i = 0, len = carousels.length; i < len; i++) {
- Carousel.carouselInterface(carousels[i], Data.getData(carousels[i], DATA_KEY))
+ Carousel.carouselInterface(carousels[i], Data.get(carousels[i], DATA_KEY))
}
})
@@ -614,18 +617,6 @@ EventHandler.on(window, EVENT_LOAD_DATA_API, () => {
* add .Carousel to jQuery only if jQuery is present
*/
-onDOMContentLoaded(() => {
- const $ = getjQuery()
- /* istanbul ignore if */
- if ($) {
- const JQUERY_NO_CONFLICT = $.fn[NAME]
- $.fn[NAME] = Carousel.jQueryInterface
- $.fn[NAME].Constructor = Carousel
- $.fn[NAME].noConflict = () => {
- $.fn[NAME] = JQUERY_NO_CONFLICT
- return Carousel.jQueryInterface
- }
- }
-})
+defineJQueryPlugin(NAME, Carousel)
export default Carousel
diff --git a/js/src/collapse.js b/js/src/collapse.js
index 0ad2b198e..6cb14cdd2 100644
--- a/js/src/collapse.js
+++ b/js/src/collapse.js
@@ -1,14 +1,12 @@
/**
* --------------------------------------------------------------------------
- * Bootstrap (v5.0.0-alpha3): collapse.js
+ * Bootstrap (v5.0.0-beta3): collapse.js
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
* --------------------------------------------------------------------------
*/
import {
- getjQuery,
- onDOMContentLoaded,
- TRANSITION_END,
+ defineJQueryPlugin,
emulateTransitionEnd,
getSelectorFromElement,
getElementFromSelector,
@@ -74,8 +72,8 @@ class Collapse extends BaseComponent {
this._isTransitioning = false
this._config = this._getConfig(config)
this._triggerArray = SelectorEngine.find(
- `${SELECTOR_DATA_TOGGLE}[href="#${element.id}"],` +
- `${SELECTOR_DATA_TOGGLE}[data-bs-target="#${element.id}"]`
+ `${SELECTOR_DATA_TOGGLE}[href="#${this._element.id}"],` +
+ `${SELECTOR_DATA_TOGGLE}[data-bs-target="#${this._element.id}"]`
)
const toggleList = SelectorEngine.find(SELECTOR_DATA_TOGGLE)
@@ -84,7 +82,7 @@ class Collapse extends BaseComponent {
const elem = toggleList[i]
const selector = getSelectorFromElement(elem)
const filterElement = SelectorEngine.find(selector)
- .filter(foundElem => foundElem === element)
+ .filter(foundElem => foundElem === this._element)
if (selector !== null && filterElement.length) {
this._selector = selector
@@ -149,7 +147,7 @@ class Collapse extends BaseComponent {
const container = SelectorEngine.findOne(this._selector)
if (actives) {
const tempActiveData = actives.find(elem => container !== elem)
- activesData = tempActiveData ? Data.getData(tempActiveData, DATA_KEY) : null
+ activesData = tempActiveData ? Data.get(tempActiveData, DATA_KEY) : null
if (activesData && activesData._isTransitioning) {
return
@@ -168,7 +166,7 @@ class Collapse extends BaseComponent {
}
if (!activesData) {
- Data.setData(elemActive, DATA_KEY, null)
+ Data.set(elemActive, DATA_KEY, null)
}
})
}
@@ -204,7 +202,7 @@ class Collapse extends BaseComponent {
const scrollSize = `scroll${capitalizedDimension}`
const transitionDuration = getTransitionDurationFromElement(this._element)
- EventHandler.one(this._element, TRANSITION_END, complete)
+ EventHandler.one(this._element, 'transitionend', complete)
emulateTransitionEnd(this._element, transitionDuration)
this._element.style[dimension] = `${this._element[scrollSize]}px`
@@ -254,7 +252,7 @@ class Collapse extends BaseComponent {
this._element.style[dimension] = ''
const transitionDuration = getTransitionDurationFromElement(this._element)
- EventHandler.one(this._element, TRANSITION_END, complete)
+ EventHandler.one(this._element, 'transitionend', complete)
emulateTransitionEnd(this._element, transitionDuration)
}
@@ -334,7 +332,7 @@ class Collapse extends BaseComponent {
// Static
static collapseInterface(element, config) {
- let data = Data.getData(element, DATA_KEY)
+ let data = Data.get(element, DATA_KEY)
const _config = {
...Default,
...Manipulator.getDataAttributes(element),
@@ -373,7 +371,7 @@ class Collapse extends BaseComponent {
EventHandler.on(document, EVENT_CLICK_DATA_API, SELECTOR_DATA_TOGGLE, function (event) {
// preventDefault only for <a> elements (which change the URL) not inside the collapsible element
- if (event.target.tagName === 'A') {
+ if (event.target.tagName === 'A' || (event.delegateTarget && event.delegateTarget.tagName === 'A')) {
event.preventDefault()
}
@@ -382,7 +380,7 @@ EventHandler.on(document, EVENT_CLICK_DATA_API, SELECTOR_DATA_TOGGLE, function (
const selectorElements = SelectorEngine.find(selector)
selectorElements.forEach(element => {
- const data = Data.getData(element, DATA_KEY)
+ const data = Data.get(element, DATA_KEY)
let config
if (data) {
// update parent attribute
@@ -407,18 +405,6 @@ EventHandler.on(document, EVENT_CLICK_DATA_API, SELECTOR_DATA_TOGGLE, function (
* add .Collapse to jQuery only if jQuery is present
*/
-onDOMContentLoaded(() => {
- const $ = getjQuery()
- /* istanbul ignore if */
- if ($) {
- const JQUERY_NO_CONFLICT = $.fn[NAME]
- $.fn[NAME] = Collapse.jQueryInterface
- $.fn[NAME].Constructor = Collapse
- $.fn[NAME].noConflict = () => {
- $.fn[NAME] = JQUERY_NO_CONFLICT
- return Collapse.jQueryInterface
- }
- }
-})
+defineJQueryPlugin(NAME, Collapse)
export default Collapse
diff --git a/js/src/dom/data.js b/js/src/dom/data.js
index 0a47a58d7..41ad08ab3 100644
--- a/js/src/dom/data.js
+++ b/js/src/dom/data.js
@@ -1,6 +1,6 @@
/**
* --------------------------------------------------------------------------
- * Bootstrap (v5.0.0-alpha3): dom/data.js
+ * Bootstrap (v5.0.0-beta3): dom/data.js
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
* --------------------------------------------------------------------------
*/
@@ -11,57 +11,47 @@
* ------------------------------------------------------------------------
*/
-const mapData = (() => {
- const storeData = {}
- let id = 1
- return {
- set(element, key, data) {
- if (typeof element.bsKey === 'undefined') {
- element.bsKey = {
- key,
- id
- }
- id++
- }
+const elementMap = new Map()
- storeData[element.bsKey.id] = data
- },
- get(element, key) {
- if (!element || typeof element.bsKey === 'undefined') {
- return null
- }
-
- const keyProperties = element.bsKey
- if (keyProperties.key === key) {
- return storeData[keyProperties.id]
- }
+export default {
+ set(element, key, instance) {
+ if (!elementMap.has(element)) {
+ elementMap.set(element, new Map())
+ }
- return null
- },
- delete(element, key) {
- if (typeof element.bsKey === 'undefined') {
- return
- }
+ const instanceMap = elementMap.get(element)
- const keyProperties = element.bsKey
- if (keyProperties.key === key) {
- delete storeData[keyProperties.id]
- delete element.bsKey
- }
+ // make it clear we only want one instance per element
+ // can be removed later when multiple key/instances are fine to be used
+ if (!instanceMap.has(key) && instanceMap.size !== 0) {
+ // eslint-disable-next-line no-console
+ console.error(`Bootstrap doesn't allow more than one instance per element. Bound instance: ${Array.from(instanceMap.keys())[0]}.`)
+ return
}
- }
-})()
-const Data = {
- setData(instance, key, data) {
- mapData.set(instance, key, data)
+ instanceMap.set(key, instance)
},
- getData(instance, key) {
- return mapData.get(instance, key)
+
+ get(element, key) {
+ if (elementMap.has(element)) {
+ return elementMap.get(element).get(key) || null
+ }
+
+ return null
},
- removeData(instance, key) {
- mapData.delete(instance, key)
+
+ remove(element, key) {
+ if (!elementMap.has(element)) {
+ return
+ }
+
+ const instanceMap = elementMap.get(element)
+
+ instanceMap.delete(key)
+
+ // free up element references if there are no instances left for an element
+ if (instanceMap.size === 0) {
+ elementMap.delete(element)
+ }
}
}
-
-export default Data
diff --git a/js/src/dom/event-handler.js b/js/src/dom/event-handler.js
index ceb6a6e6e..3293f397d 100644
--- a/js/src/dom/event-handler.js
+++ b/js/src/dom/event-handler.js
@@ -1,6 +1,6 @@
/**
* --------------------------------------------------------------------------
- * Bootstrap (v5.0.0-alpha3): dom/event-handler.js
+ * Bootstrap (v5.0.0-beta3): dom/event-handler.js
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
* --------------------------------------------------------------------------
*/
@@ -22,6 +22,7 @@ const customEvents = {
mouseenter: 'mouseover',
mouseleave: 'mouseout'
}
+const customEventsRegex = /^(mouseenter|mouseleave)/i
const nativeEvents = new Set([
'click',
'dblclick',
@@ -112,7 +113,8 @@ function bootstrapDelegationHandler(element, selector, fn) {
event.delegateTarget = target
if (handler.oneOff) {
- EventHandler.off(element, event.type, fn)
+ // eslint-disable-next-line unicorn/consistent-destructuring
+ EventHandler.off(element, event.type, selector, fn)
}
return fn.apply(target, [event])
@@ -143,14 +145,7 @@ function normalizeParams(originalTypeEvent, handler, delegationFn) {
const delegation = typeof handler === 'string'
const originalHandler = delegation ? delegationFn : handler
- // allow to get the native events from namespaced events ('click.bs.button' --> 'click')
- let typeEvent = originalTypeEvent.replace(stripNameRegex, '')
- const custom = customEvents[typeEvent]
-
- if (custom) {
- typeEvent = custom
- }
-
+ let typeEvent = getTypeEvent(originalTypeEvent)
const isNative = nativeEvents.has(typeEvent)
if (!isNative) {
@@ -170,6 +165,24 @@ function addHandler(element, originalTypeEvent, handler, delegationFn, oneOff) {
delegationFn = null
}
+ // in case of mouseenter or mouseleave wrap the handler within a function that checks for its DOM position
+ // this prevents the handler from being dispatched the same way as mouseover or mouseout does
+ if (customEventsRegex.test(originalTypeEvent)) {
+ const wrapFn = fn => {
+ return function (event) {
+ if (!event.relatedTarget || (event.relatedTarget !== event.delegateTarget && !event.delegateTarget.contains(event.relatedTarget))) {
+ return fn.call(this, event)
+ }
+ }
+ }
+
+ if (delegationFn) {
+ delegationFn = wrapFn(delegationFn)
+ } else {
+ handler = wrapFn(handler)
+ }
+ }
+
const [delegation, originalHandler, typeEvent] = normalizeParams(originalTypeEvent, handler, delegationFn)
const events = getEvent(element)
const handlers = events[typeEvent] || (events[typeEvent] = {})
@@ -218,6 +231,12 @@ function removeNamespacedHandlers(element, events, typeEvent, namespace) {
})
}
+function getTypeEvent(event) {
+ // allow to get the native events from namespaced events ('click.bs.button' --> 'click')
+ event = event.replace(stripNameRegex, '')
+ return customEvents[event] || event
+}
+
const EventHandler = {
on(element, event, handler, delegationFn) {
addHandler(element, event, handler, delegationFn, false)
@@ -271,7 +290,7 @@ const EventHandler = {
}
const $ = getjQuery()
- const typeEvent = event.replace(stripNameRegex, '')
+ const typeEvent = getTypeEvent(event)
const inNamespace = event !== typeEvent
const isNative = nativeEvents.has(typeEvent)
diff --git a/js/src/dom/manipulator.js b/js/src/dom/manipulator.js
index ed74e0ce2..73b409f7e 100644
--- a/js/src/dom/manipulator.js
+++ b/js/src/dom/manipulator.js
@@ -1,6 +1,6 @@
/**
* --------------------------------------------------------------------------
- * Bootstrap (v5.0.0-alpha3): dom/manipulator.js
+ * Bootstrap (v5.0.0-beta3): dom/manipulator.js
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
* --------------------------------------------------------------------------
*/
diff --git a/js/src/dom/selector-engine.js b/js/src/dom/selector-engine.js
index b42c30c3f..116b02741 100644
--- a/js/src/dom/selector-engine.js
+++ b/js/src/dom/selector-engine.js
@@ -1,6 +1,6 @@
/**
* --------------------------------------------------------------------------
- * Bootstrap (v5.0.0-alpha3): dom/selector-engine.js
+ * Bootstrap (v5.0.0-beta3): dom/selector-engine.js
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
* --------------------------------------------------------------------------
*/
@@ -14,10 +14,6 @@
const NODE_TEXT = 3
const SelectorEngine = {
- matches(element, selector) {
- return element.matches(selector)
- },
-
find(selector, element = document.documentElement) {
return [].concat(...Element.prototype.querySelectorAll.call(element, selector))
},
@@ -27,9 +23,8 @@ const SelectorEngine = {
},
children(element, selector) {
- const children = [].concat(...element.children)
-
- return children.filter(child => child.matches(selector))
+ return [].concat(...element.children)
+ .filter(child => child.matches(selector))
},
parents(element, selector) {
@@ -38,7 +33,7 @@ const SelectorEngine = {
let ancestor = element.parentNode
while (ancestor && ancestor.nodeType === Node.ELEMENT_NODE && ancestor.nodeType !== NODE_TEXT) {
- if (this.matches(ancestor, selector)) {
+ if (ancestor.matches(selector)) {
parents.push(ancestor)
}
@@ -66,7 +61,7 @@ const SelectorEngine = {
let next = element.nextElementSibling
while (next) {
- if (this.matches(next, selector)) {
+ if (next.matches(selector)) {
return [next]
}
diff --git a/js/src/dropdown.js b/js/src/dropdown.js
index 0ac108ab8..d26fa96ca 100644
--- a/js/src/dropdown.js
+++ b/js/src/dropdown.js
@@ -1,14 +1,16 @@
/**
* --------------------------------------------------------------------------
- * Bootstrap (v5.0.0-alpha3): dropdown.js
+ * Bootstrap (v5.0.0-beta3): dropdown.js
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
* --------------------------------------------------------------------------
*/
+import * as Popper from '@popperjs/core'
+
import {
- getjQuery,
- onDOMContentLoaded,
+ defineJQueryPlugin,
getElementFromSelector,
+ isDisabled,
isElement,
isVisible,
isRTL,
@@ -18,7 +20,6 @@ import {
import Data from './dom/data'
import EventHandler from './dom/event-handler'
import Manipulator from './dom/manipulator'
-import Popper from 'popper.js'
import SelectorEngine from './dom/selector-engine'
import BaseComponent from './base-component'
@@ -51,44 +52,40 @@ const EVENT_CLICK_DATA_API = `click${EVENT_KEY}${DATA_API_KEY}`
const EVENT_KEYDOWN_DATA_API = `keydown${EVENT_KEY}${DATA_API_KEY}`
const EVENT_KEYUP_DATA_API = `keyup${EVENT_KEY}${DATA_API_KEY}`
-const CLASS_NAME_DISABLED = 'disabled'
const CLASS_NAME_SHOW = 'show'
const CLASS_NAME_DROPUP = 'dropup'
const CLASS_NAME_DROPEND = 'dropend'
const CLASS_NAME_DROPSTART = 'dropstart'
-const CLASS_NAME_MENUEND = 'dropdown-menu-end'
const CLASS_NAME_NAVBAR = 'navbar'
-const CLASS_NAME_POSITION_STATIC = 'position-static'
const SELECTOR_DATA_TOGGLE = '[data-bs-toggle="dropdown"]'
-const SELECTOR_FORM_CHILD = '.dropdown form'
const SELECTOR_MENU = '.dropdown-menu'
const SELECTOR_NAVBAR_NAV = '.navbar-nav'
const SELECTOR_VISIBLE_ITEMS = '.dropdown-menu .dropdown-item:not(.disabled):not(:disabled)'
-const PLACEMENT_TOP = isRTL ? 'top-end' : 'top-start'
-const PLACEMENT_TOPEND = isRTL ? 'top-start' : 'top-end'
-const PLACEMENT_BOTTOM = isRTL ? 'bottom-end' : 'bottom-start'
-const PLACEMENT_BOTTOMEND = isRTL ? 'bottom-start' : 'bottom-end'
-const PLACEMENT_RIGHT = isRTL ? 'left-start' : 'right-start'
-const PLACEMENT_LEFT = isRTL ? 'right-start' : 'left-start'
+const PLACEMENT_TOP = isRTL() ? 'top-end' : 'top-start'
+const PLACEMENT_TOPEND = isRTL() ? 'top-start' : 'top-end'
+const PLACEMENT_BOTTOM = isRTL() ? 'bottom-end' : 'bottom-start'
+const PLACEMENT_BOTTOMEND = isRTL() ? 'bottom-start' : 'bottom-end'
+const PLACEMENT_RIGHT = isRTL() ? 'left-start' : 'right-start'
+const PLACEMENT_LEFT = isRTL() ? 'right-start' : 'left-start'
const Default = {
- offset: 0,
- flip: true,
- boundary: 'scrollParent',
+ offset: [0, 2],
+ boundary: 'clippingParents',
reference: 'toggle',
display: 'dynamic',
- popperConfig: null
+ popperConfig: null,
+ autoClose: true
}
const DefaultType = {
- offset: '(number|string|function)',
- flip: 'boolean',
+ offset: '(array|string|function)',
boundary: '(string|element)',
- reference: '(string|element)',
+ reference: '(string|element|object)',
display: 'string',
- popperConfig: '(null|object)'
+ popperConfig: '(null|object|function)',
+ autoClose: '(boolean|string)'
}
/**
@@ -126,15 +123,14 @@ class Dropdown extends BaseComponent {
// Public
toggle() {
- if (this._element.disabled || this._element.classList.contains(CLASS_NAME_DISABLED)) {
+ if (isDisabled(this._element)) {
return
}
const isActive = this._element.classList.contains(CLASS_NAME_SHOW)
- Dropdown.clearMenus()
-
if (isActive) {
+ this.hide()
return
}
@@ -142,7 +138,7 @@ class Dropdown extends BaseComponent {
}
show() {
- if (this._element.disabled || this._element.classList.contains(CLASS_NAME_DISABLED) || this._menu.classList.contains(CLASS_NAME_SHOW)) {
+ if (isDisabled(this._element) || this._menu.classList.contains(CLASS_NAME_SHOW)) {
return
}
@@ -158,7 +154,9 @@ class Dropdown extends BaseComponent {
}
// Totally disable Popper for Dropdowns in Navbar
- if (!this._inNavbar) {
+ if (this._inNavbar) {
+ Manipulator.setDataAttribute(this._menu, 'popper', 'none')
+ } else {
if (typeof Popper === 'undefined') {
throw new TypeError('Bootstrap\'s dropdowns require Popper (https://popper.js.org)')
}
@@ -174,16 +172,18 @@ class Dropdown extends BaseComponent {
if (typeof this._config.reference.jquery !== 'undefined') {
referenceElement = this._config.reference[0]
}
+ } else if (typeof this._config.reference === 'object') {
+ referenceElement = this._config.reference
}
- // If boundary is not `scrollParent`, then set position to `static`
- // to allow the menu to "escape" the scroll parent's boundaries
- // https://github.com/twbs/bootstrap/issues/24251
- if (this._config.boundary !== 'scrollParent') {
- parent.classList.add(CLASS_NAME_POSITION_STATIC)
- }
+ const popperConfig = this._getPopperConfig()
+ const isDisplayStatic = popperConfig.modifiers.find(modifier => modifier.name === 'applyStyles' && modifier.enabled === false)
+
+ this._popper = Popper.createPopper(referenceElement, this._menu, popperConfig)
- this._popper = new Popper(referenceElement, this._menu, this._getPopperConfig())
+ if (isDisplayStatic) {
+ Manipulator.setDataAttribute(this._menu, 'popper', 'static')
+ }
}
// If this is a touch-enabled device we add extra
@@ -193,7 +193,7 @@ class Dropdown extends BaseComponent {
if ('ontouchstart' in document.documentElement &&
!parent.closest(SELECTOR_NAVBAR_NAV)) {
[].concat(...document.body.children)
- .forEach(elem => EventHandler.on(elem, 'mouseover', null, noop()))
+ .forEach(elem => EventHandler.on(elem, 'mouseover', noop))
}
this._element.focus()
@@ -201,48 +201,36 @@ class Dropdown extends BaseComponent {
this._menu.classList.toggle(CLASS_NAME_SHOW)
this._element.classList.toggle(CLASS_NAME_SHOW)
- EventHandler.trigger(parent, EVENT_SHOWN, relatedTarget)
+ EventHandler.trigger(this._element, EVENT_SHOWN, relatedTarget)
}
hide() {
- if (this._element.disabled || this._element.classList.contains(CLASS_NAME_DISABLED) || !this._menu.classList.contains(CLASS_NAME_SHOW)) {
+ if (isDisabled(this._element) || !this._menu.classList.contains(CLASS_NAME_SHOW)) {
return
}
- const parent = Dropdown.getParentFromElement(this._element)
const relatedTarget = {
relatedTarget: this._element
}
- const hideEvent = EventHandler.trigger(parent, EVENT_HIDE, relatedTarget)
-
- if (hideEvent.defaultPrevented) {
- return
- }
-
- if (this._popper) {
- this._popper.destroy()
- }
-
- this._menu.classList.toggle(CLASS_NAME_SHOW)
- this._element.classList.toggle(CLASS_NAME_SHOW)
- EventHandler.trigger(parent, EVENT_HIDDEN, relatedTarget)
+ this._completeHide(relatedTarget)
}
dispose() {
- super.dispose()
- EventHandler.off(this._element, EVENT_KEY)
this._menu = null
+
if (this._popper) {
this._popper.destroy()
this._popper = null
}
+
+ super.dispose()
}
update() {
this._inNavbar = this._detectNavbar()
if (this._popper) {
- this._popper.scheduleUpdate()
+ this._popper.update()
}
}
@@ -251,11 +239,34 @@ class Dropdown extends BaseComponent {
_addEventListeners() {
EventHandler.on(this._element, EVENT_CLICK, event => {
event.preventDefault()
- event.stopPropagation()
this.toggle()
})
}
+ _completeHide(relatedTarget) {
+ const hideEvent = EventHandler.trigger(this._element, EVENT_HIDE, relatedTarget)
+ if (hideEvent.defaultPrevented) {
+ return
+ }
+
+ // If this is a touch-enabled device we remove the extra
+ // empty mouseover listeners we added for iOS support
+ if ('ontouchstart' in document.documentElement) {
+ [].concat(...document.body.children)
+ .forEach(elem => EventHandler.off(elem, 'mouseover', noop))
+ }
+
+ if (this._popper) {
+ this._popper.destroy()
+ }
+
+ this._menu.classList.remove(CLASS_NAME_SHOW)
+ this._element.classList.remove(CLASS_NAME_SHOW)
+ this._element.setAttribute('aria-expanded', 'false')
+ Manipulator.removeDataAttribute(this._menu, 'popper')
+ EventHandler.trigger(this._element, EVENT_HIDDEN, relatedTarget)
+ }
+
_getConfig(config) {
config = {
...this.constructor.Default,
@@ -265,6 +276,13 @@ class Dropdown extends BaseComponent {
typeCheckConfig(NAME, config, this.constructor.DefaultType)
+ if (typeof config.reference === 'object' && !isElement(config.reference) &&
+ typeof config.reference.getBoundingClientRect !== 'function'
+ ) {
+ // Popper virtual elements require a getBoundingClientRect method
+ throw new TypeError(`${NAME.toUpperCase()}: Option "reference" provided type "object" without a required "getBoundingClientRect" method.`)
+ }
+
return config
}
@@ -274,78 +292,103 @@ class Dropdown extends BaseComponent {
_getPlacement() {
const parentDropdown = this._element.parentNode
- let placement = PLACEMENT_BOTTOM
- // Handle dropup
+ if (parentDropdown.classList.contains(CLASS_NAME_DROPEND)) {
+ return PLACEMENT_RIGHT
+ }
+
+ if (parentDropdown.classList.contains(CLASS_NAME_DROPSTART)) {
+ return PLACEMENT_LEFT
+ }
+
+ // We need to trim the value because custom properties can also include spaces
+ const isEnd = getComputedStyle(this._menu).getPropertyValue('--bs-position').trim() === 'end'
+
if (parentDropdown.classList.contains(CLASS_NAME_DROPUP)) {
- placement = this._menu.classList.contains(CLASS_NAME_MENUEND) ?
- PLACEMENT_TOPEND :
- PLACEMENT_TOP
- } else if (parentDropdown.classList.contains(CLASS_NAME_DROPEND)) {
- placement = PLACEMENT_RIGHT
- } else if (parentDropdown.classList.contains(CLASS_NAME_DROPSTART)) {
- placement = PLACEMENT_LEFT
- } else if (this._menu.classList.contains(CLASS_NAME_MENUEND)) {
- placement = PLACEMENT_BOTTOMEND
- }
-
- return placement
+ return isEnd ? PLACEMENT_TOPEND : PLACEMENT_TOP
+ }
+
+ return isEnd ? PLACEMENT_BOTTOMEND : PLACEMENT_BOTTOM
}
_detectNavbar() {
- return Boolean(this._element.closest(`.${CLASS_NAME_NAVBAR}`))
+ return this._element.closest(`.${CLASS_NAME_NAVBAR}`) !== null
}
_getOffset() {
- const offset = {}
+ const { offset } = this._config
- if (typeof this._config.offset === 'function') {
- offset.fn = data => {
- data.offsets = {
- ...data.offsets,
- ...(this._config.offset(data.offsets, this._element) || {})
- }
+ if (typeof offset === 'string') {
+ return offset.split(',').map(val => Number.parseInt(val, 10))
+ }
- return data
- }
- } else {
- offset.offset = this._config.offset
+ if (typeof offset === 'function') {
+ return popperData => offset(popperData, this._element)
}
return offset
}
_getPopperConfig() {
- const popperConfig = {
+ const defaultBsPopperConfig = {
placement: this._getPlacement(),
- modifiers: {
- offset: this._getOffset(),
- flip: {
- enabled: this._config.flip
- },
- preventOverflow: {
- boundariesElement: this._config.boundary
+ modifiers: [{
+ name: 'preventOverflow',
+ options: {
+ boundary: this._config.boundary
}
- }
+ },
+ {
+ name: 'offset',
+ options: {
+ offset: this._getOffset()
+ }
+ }]
}
// Disable Popper if we have a static display
if (this._config.display === 'static') {
- popperConfig.modifiers.applyStyle = {
+ defaultBsPopperConfig.modifiers = [{
+ name: 'applyStyles',
enabled: false
- }
+ }]
}
return {
- ...popperConfig,
- ...this._config.popperConfig
+ ...defaultBsPopperConfig,
+ ...(typeof this._config.popperConfig === 'function' ? this._config.popperConfig(defaultBsPopperConfig) : this._config.popperConfig)
+ }
+ }
+
+ _selectMenuItem(event) {
+ const items = SelectorEngine.find(SELECTOR_VISIBLE_ITEMS, this._menu).filter(isVisible)
+
+ if (!items.length) {
+ return
+ }
+
+ let index = items.indexOf(event.target)
+
+ // Up
+ if (event.key === ARROW_UP_KEY && index > 0) {
+ index--
+ }
+
+ // Down
+ if (event.key === ARROW_DOWN_KEY && index < items.length - 1) {
+ index++
}
+
+ // index is -1 if the first keydown is an ArrowUp
+ index = index === -1 ? 0 : index
+
+ items[index].focus()
}
// Static
static dropdownInterface(element, config) {
- let data = Data.getData(element, DATA_KEY)
+ let data = Data.get(element, DATA_KEY)
const _config = typeof config === 'object' ? config : null
if (!data) {
@@ -368,60 +411,54 @@ class Dropdown extends BaseComponent {
}
static clearMenus(event) {
- if (event && (event.button === RIGHT_MOUSE_BUTTON || (event.type === 'keyup' && event.key !== TAB_KEY))) {
- return
- }
-
- const toggles = SelectorEngine.find(SELECTOR_DATA_TOGGLE)
-
- for (let i = 0, len = toggles.length; i < len; i++) {
- const parent = Dropdown.getParentFromElement(toggles[i])
- const context = Data.getData(toggles[i], DATA_KEY)
- const relatedTarget = {
- relatedTarget: toggles[i]
+ if (event) {
+ if (event.button === RIGHT_MOUSE_BUTTON || (event.type === 'keyup' && event.key !== TAB_KEY)) {
+ return
}
- if (event && event.type === 'click') {
- relatedTarget.clickEvent = event
+ if (/input|select|option|textarea|form/i.test(event.target.tagName)) {
+ return
}
+ }
- if (!context) {
- continue
- }
+ const toggles = SelectorEngine.find(SELECTOR_DATA_TOGGLE)
- const dropdownMenu = context._menu
- if (!toggles[i].classList.contains(CLASS_NAME_SHOW)) {
+ for (let i = 0, len = toggles.length; i < len; i++) {
+ const context = Data.get(toggles[i], DATA_KEY)
+ if (!context || context._config.autoClose === false) {
continue
}
- if (event && ((event.type === 'click' &&
- /input|textarea/i.test(event.target.tagName)) ||
- (event.type === 'keyup' && event.key === TAB_KEY)) &&
- dropdownMenu.contains(event.target)) {
+ if (!context._element.classList.contains(CLASS_NAME_SHOW)) {
continue
}
- const hideEvent = EventHandler.trigger(parent, EVENT_HIDE, relatedTarget)
- if (hideEvent.defaultPrevented) {
- continue
+ const relatedTarget = {
+ relatedTarget: context._element
}
- // If this is a touch-enabled device we remove the extra
- // empty mouseover listeners we added for iOS support
- if ('ontouchstart' in document.documentElement) {
- [].concat(...document.body.children)
- .forEach(elem => EventHandler.off(elem, 'mouseover', null, noop()))
- }
+ if (event) {
+ const composedPath = event.composedPath()
+ const isMenuTarget = composedPath.includes(context._menu)
+ if (
+ composedPath.includes(context._element) ||
+ (context._config.autoClose === 'inside' && !isMenuTarget) ||
+ (context._config.autoClose === 'outside' && isMenuTarget)
+ ) {
+ continue
+ }
- toggles[i].setAttribute('aria-expanded', 'false')
+ // Tab navigation through the dropdown menu shouldn't close the menu
+ if (event.type === 'keyup' && event.key === TAB_KEY && context._menu.contains(event.target)) {
+ continue
+ }
- if (context._popper) {
- context._popper.destroy()
+ if (event.type === 'click') {
+ relatedTarget.clickEvent = event
+ }
}
- dropdownMenu.classList.remove(CLASS_NAME_SHOW)
- toggles[i].classList.remove(CLASS_NAME_SHOW)
- EventHandler.trigger(parent, EVENT_HIDDEN, relatedTarget)
+ context._completeHide(relatedTarget)
}
}
@@ -445,50 +482,38 @@ class Dropdown extends BaseComponent {
return
}
+ const isActive = this.classList.contains(CLASS_NAME_SHOW)
+
+ if (!isActive && event.key === ESCAPE_KEY) {
+ return
+ }
+
event.preventDefault()
event.stopPropagation()
- if (this.disabled || this.classList.contains(CLASS_NAME_DISABLED)) {
+ if (isDisabled(this)) {
return
}
- const parent = Dropdown.getParentFromElement(this)
- const isActive = this.classList.contains(CLASS_NAME_SHOW)
+ const getToggleButton = () => this.matches(SELECTOR_DATA_TOGGLE) ? this : SelectorEngine.prev(this, SELECTOR_DATA_TOGGLE)[0]
if (event.key === ESCAPE_KEY) {
- const button = this.matches(SELECTOR_DATA_TOGGLE) ? this : SelectorEngine.prev(this, SELECTOR_DATA_TOGGLE)[0]
- button.focus()
+ getToggleButton().focus()
Dropdown.clearMenus()
return
}
- if (!isActive || event.key === SPACE_KEY) {
- Dropdown.clearMenus()
+ if (!isActive && (event.key === ARROW_UP_KEY || event.key === ARROW_DOWN_KEY)) {
+ getToggleButton().click()
return
}
- const items = SelectorEngine.find(SELECTOR_VISIBLE_ITEMS, parent).filter(isVisible)
-
- if (!items.length) {
+ if (!isActive || event.key === SPACE_KEY) {
+ Dropdown.clearMenus()
return
}
- let index = items.indexOf(event.target)
-
- // Up
- if (event.key === ARROW_UP_KEY && index > 0) {
- index--
- }
-
- // Down
- if (event.key === ARROW_DOWN_KEY && index < items.length - 1) {
- index++
- }
-
- // index is -1 if the first keydown is an ArrowUp
- index = index === -1 ? 0 : index
-
- items[index].focus()
+ Dropdown.getInstance(getToggleButton())._selectMenuItem(event)
}
}
@@ -504,10 +529,8 @@ EventHandler.on(document, EVENT_CLICK_DATA_API, Dropdown.clearMenus)
EventHandler.on(document, EVENT_KEYUP_DATA_API, Dropdown.clearMenus)
EventHandler.on(document, EVENT_CLICK_DATA_API, SELECTOR_DATA_TOGGLE, function (event) {
event.preventDefault()
- event.stopPropagation()
- Dropdown.dropdownInterface(this, 'toggle')
+ Dropdown.dropdownInterface(this)
})
-EventHandler.on(document, EVENT_CLICK_DATA_API, SELECTOR_FORM_CHILD, e => e.stopPropagation())
/**
* ------------------------------------------------------------------------
@@ -516,18 +539,6 @@ EventHandler.on(document, EVENT_CLICK_DATA_API, SELECTOR_FORM_CHILD, e => e.stop
* add .Dropdown to jQuery only if jQuery is present
*/
-onDOMContentLoaded(() => {
- const $ = getjQuery()
- /* istanbul ignore if */
- if ($) {
- const JQUERY_NO_CONFLICT = $.fn[NAME]
- $.fn[NAME] = Dropdown.jQueryInterface
- $.fn[NAME].Constructor = Dropdown
- $.fn[NAME].noConflict = () => {
- $.fn[NAME] = JQUERY_NO_CONFLICT
- return Dropdown.jQueryInterface
- }
- }
-})
+defineJQueryPlugin(NAME, Dropdown)
export default Dropdown
diff --git a/js/src/modal.js b/js/src/modal.js
index 94bf95f8a..fabb151cb 100644
--- a/js/src/modal.js
+++ b/js/src/modal.js
@@ -1,27 +1,26 @@
/**
* --------------------------------------------------------------------------
- * Bootstrap (v5.0.0-alpha3): modal.js
+ * Bootstrap (v5.0.0-beta3): modal.js
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
* --------------------------------------------------------------------------
*/
import {
- getjQuery,
- onDOMContentLoaded,
- TRANSITION_END,
+ defineJQueryPlugin,
emulateTransitionEnd,
getElementFromSelector,
getTransitionDurationFromElement,
- isVisible,
isRTL,
+ isVisible,
reflow,
typeCheckConfig
} from './util/index'
-import Data from './dom/data'
import EventHandler from './dom/event-handler'
import Manipulator from './dom/manipulator'
import SelectorEngine from './dom/selector-engine'
+import { getWidth as getScrollBarWidth, hide as scrollBarHide, reset as scrollBarReset } from './util/scrollbar'
import BaseComponent from './base-component'
+import Backdrop from './util/backdrop'
/**
* ------------------------------------------------------------------------
@@ -60,8 +59,6 @@ const EVENT_MOUSEUP_DISMISS = `mouseup.dismiss${EVENT_KEY}`
const EVENT_MOUSEDOWN_DISMISS = `mousedown.dismiss${EVENT_KEY}`
const EVENT_CLICK_DATA_API = `click${EVENT_KEY}${DATA_API_KEY}`
-const CLASS_NAME_SCROLLBAR_MEASURER = 'modal-scrollbar-measure'
-const CLASS_NAME_BACKDROP = 'modal-backdrop'
const CLASS_NAME_OPEN = 'modal-open'
const CLASS_NAME_FADE = 'fade'
const CLASS_NAME_SHOW = 'show'
@@ -71,8 +68,6 @@ const SELECTOR_DIALOG = '.modal-dialog'
const SELECTOR_MODAL_BODY = '.modal-body'
const SELECTOR_DATA_TOGGLE = '[data-bs-toggle="modal"]'
const SELECTOR_DATA_DISMISS = '[data-bs-dismiss="modal"]'
-const SELECTOR_FIXED_CONTENT = '.fixed-top, .fixed-bottom, .is-fixed, .sticky-top'
-const SELECTOR_STICKY_CONTENT = '.sticky-top'
/**
* ------------------------------------------------------------------------
@@ -85,13 +80,11 @@ class Modal extends BaseComponent {
super(element)
this._config = this._getConfig(config)
- this._dialog = SelectorEngine.findOne(SELECTOR_DIALOG, element)
- this._backdrop = null
+ this._dialog = SelectorEngine.findOne(SELECTOR_DIALOG, this._element)
+ this._backdrop = this._initializeBackDrop()
this._isShown = false
- this._isBodyOverflowing = false
this._ignoreBackdropClick = false
this._isTransitioning = false
- this._scrollbarWidth = 0
}
// Getters
@@ -115,7 +108,7 @@ class Modal extends BaseComponent {
return
}
- if (this._element.classList.contains(CLASS_NAME_FADE)) {
+ if (this._isAnimated()) {
this._isTransitioning = true
}
@@ -129,8 +122,9 @@ class Modal extends BaseComponent {
this._isShown = true
- this._checkScrollbar()
- this._setScrollbar()
+ scrollBarHide()
+
+ document.body.classList.add(CLASS_NAME_OPEN)
this._adjustDialog()
@@ -166,9 +160,9 @@ class Modal extends BaseComponent {
}
this._isShown = false
- const transition = this._element.classList.contains(CLASS_NAME_FADE)
+ const isAnimated = this._isAnimated()
- if (transition) {
+ if (isAnimated) {
this._isTransitioning = true
}
@@ -182,10 +176,10 @@ class Modal extends BaseComponent {
EventHandler.off(this._element, EVENT_CLICK_DISMISS)
EventHandler.off(this._dialog, EVENT_MOUSEDOWN_DISMISS)
- if (transition) {
+ if (isAnimated) {
const transitionDuration = getTransitionDurationFromElement(this._element)
- EventHandler.one(this._element, TRANSITION_END, event => this._hideModal(event))
+ EventHandler.one(this._element, 'transitionend', event => this._hideModal(event))
emulateTransitionEnd(this._element, transitionDuration)
} else {
this._hideModal()
@@ -193,7 +187,7 @@ class Modal extends BaseComponent {
}
dispose() {
- [window, this._element, this._dialog]
+ [window, this._dialog]
.forEach(htmlElement => EventHandler.off(htmlElement, EVENT_KEY))
super.dispose()
@@ -207,12 +201,11 @@ class Modal extends BaseComponent {
this._config = null
this._dialog = null
+ this._backdrop.dispose()
this._backdrop = null
this._isShown = null
- this._isBodyOverflowing = null
this._ignoreBackdropClick = null
this._isTransitioning = null
- this._scrollbarWidth = null
}
handleUpdate() {
@@ -221,9 +214,17 @@ class Modal extends BaseComponent {
// Private
+ _initializeBackDrop() {
+ return new Backdrop({
+ isVisible: Boolean(this._config.backdrop), // 'static' option will be translated to true, and booleans will keep their value
+ isAnimated: this._isAnimated()
+ })
+ }
+
_getConfig(config) {
config = {
...Default,
+ ...Manipulator.getDataAttributes(this._element),
...config
}
typeCheckConfig(NAME, config, DefaultType)
@@ -231,7 +232,7 @@ class Modal extends BaseComponent {
}
_showElement(relatedTarget) {
- const transition = this._element.classList.contains(CLASS_NAME_FADE)
+ const isAnimated = this._isAnimated()
const modalBody = SelectorEngine.findOne(SELECTOR_MODAL_BODY, this._dialog)
if (!this._element.parentNode || this._element.parentNode.nodeType !== Node.ELEMENT_NODE) {
@@ -249,7 +250,7 @@ class Modal extends BaseComponent {
modalBody.scrollTop = 0
}
- if (transition) {
+ if (isAnimated) {
reflow(this._element)
}
@@ -270,10 +271,10 @@ class Modal extends BaseComponent {
})
}
- if (transition) {
+ if (isAnimated) {
const transitionDuration = getTransitionDurationFromElement(this._dialog)
- EventHandler.one(this._dialog, TRANSITION_END, transitionComplete)
+ EventHandler.one(this._dialog, 'transitionend', transitionComplete)
emulateTransitionEnd(this._dialog, transitionDuration)
} else {
transitionComplete()
@@ -320,84 +321,37 @@ class Modal extends BaseComponent {
this._element.removeAttribute('aria-modal')
this._element.removeAttribute('role')
this._isTransitioning = false
- this._showBackdrop(() => {
+ this._backdrop.hide(() => {
document.body.classList.remove(CLASS_NAME_OPEN)
this._resetAdjustments()
- this._resetScrollbar()
+ scrollBarReset()
EventHandler.trigger(this._element, EVENT_HIDDEN)
})
}
- _removeBackdrop() {
- this._backdrop.parentNode.removeChild(this._backdrop)
- this._backdrop = null
- }
-
_showBackdrop(callback) {
- const animate = this._element.classList.contains(CLASS_NAME_FADE) ?
- CLASS_NAME_FADE :
- ''
-
- if (this._isShown && this._config.backdrop) {
- this._backdrop = document.createElement('div')
- this._backdrop.className = CLASS_NAME_BACKDROP
-
- if (animate) {
- this._backdrop.classList.add(animate)
- }
-
- document.body.appendChild(this._backdrop)
-
- EventHandler.on(this._element, EVENT_CLICK_DISMISS, event => {
- if (this._ignoreBackdropClick) {
- this._ignoreBackdropClick = false
- return
- }
-
- if (event.target !== event.currentTarget) {
- return
- }
-
- if (this._config.backdrop === 'static') {
- this._triggerBackdropTransition()
- } else {
- this.hide()
- }
- })
-
- if (animate) {
- reflow(this._backdrop)
+ EventHandler.on(this._element, EVENT_CLICK_DISMISS, event => {
+ if (this._ignoreBackdropClick) {
+ this._ignoreBackdropClick = false
+ return
}
- this._backdrop.classList.add(CLASS_NAME_SHOW)
-
- if (!animate) {
- callback()
+ if (event.target !== event.currentTarget) {
return
}
- const backdropTransitionDuration = getTransitionDurationFromElement(this._backdrop)
-
- EventHandler.one(this._backdrop, TRANSITION_END, callback)
- emulateTransitionEnd(this._backdrop, backdropTransitionDuration)
- } else if (!this._isShown && this._backdrop) {
- this._backdrop.classList.remove(CLASS_NAME_SHOW)
-
- const callbackRemove = () => {
- this._removeBackdrop()
- callback()
+ if (this._config.backdrop === true) {
+ this.hide()
+ } else if (this._config.backdrop === 'static') {
+ this._triggerBackdropTransition()
}
+ })
- if (this._element.classList.contains(CLASS_NAME_FADE)) {
- const backdropTransitionDuration = getTransitionDurationFromElement(this._backdrop)
- EventHandler.one(this._backdrop, TRANSITION_END, callbackRemove)
- emulateTransitionEnd(this._backdrop, backdropTransitionDuration)
- } else {
- callbackRemove()
- }
- } else {
- callback()
- }
+ this._backdrop.show(callback)
+ }
+
+ _isAnimated() {
+ return this._element.classList.contains(CLASS_NAME_FADE)
}
_triggerBackdropTransition() {
@@ -414,11 +368,11 @@ class Modal extends BaseComponent {
this._element.classList.add(CLASS_NAME_STATIC)
const modalTransitionDuration = getTransitionDurationFromElement(this._dialog)
- EventHandler.off(this._element, TRANSITION_END)
- EventHandler.one(this._element, TRANSITION_END, () => {
+ EventHandler.off(this._element, 'transitionend')
+ EventHandler.one(this._element, 'transitionend', () => {
this._element.classList.remove(CLASS_NAME_STATIC)
if (!isModalOverflowing) {
- EventHandler.one(this._element, TRANSITION_END, () => {
+ EventHandler.one(this._element, 'transitionend', () => {
this._element.style.overflowY = ''
})
emulateTransitionEnd(this._element, modalTransitionDuration)
@@ -433,15 +387,16 @@ class Modal extends BaseComponent {
// ----------------------------------------------------------------------
_adjustDialog() {
- const isModalOverflowing =
- this._element.scrollHeight > document.documentElement.clientHeight
+ const isModalOverflowing = this._element.scrollHeight > document.documentElement.clientHeight
+ const scrollbarWidth = getScrollBarWidth()
+ const isBodyOverflowing = scrollbarWidth > 0
- if ((!this._isBodyOverflowing && isModalOverflowing && !isRTL) || (this._isBodyOverflowing && !isModalOverflowing && isRTL)) {
- this._element.style.paddingLeft = `${this._scrollbarWidth}px`
+ if ((!isBodyOverflowing && isModalOverflowing && !isRTL()) || (isBodyOverflowing && !isModalOverflowing && isRTL())) {
+ this._element.style.paddingLeft = `${scrollbarWidth}px`
}
- if ((this._isBodyOverflowing && !isModalOverflowing && !isRTL) || (!this._isBodyOverflowing && isModalOverflowing && isRTL)) {
- this._element.style.paddingRight = `${this._scrollbarWidth}px`
+ if ((isBodyOverflowing && !isModalOverflowing && !isRTL()) || (!isBodyOverflowing && isModalOverflowing && isRTL())) {
+ this._element.style.paddingRight = `${scrollbarWidth}px`
}
}
@@ -450,108 +405,21 @@ class Modal extends BaseComponent {
this._element.style.paddingRight = ''
}
- _checkScrollbar() {
- const rect = document.body.getBoundingClientRect()
- this._isBodyOverflowing = Math.round(rect.left + rect.right) < window.innerWidth
- this._scrollbarWidth = this._getScrollbarWidth()
- }
-
- _setScrollbar() {
- if (this._isBodyOverflowing) {
- // Note: DOMNode.style.paddingRight returns the actual value or '' if not set
- // while $(DOMNode).css('padding-right') returns the calculated value or 0 if not set
-
- // Adjust fixed content padding
- SelectorEngine.find(SELECTOR_FIXED_CONTENT)
- .forEach(element => {
- const actualPadding = element.style.paddingRight
- const calculatedPadding = window.getComputedStyle(element)['padding-right']
- Manipulator.setDataAttribute(element, 'padding-right', actualPadding)
- element.style.paddingRight = `${Number.parseFloat(calculatedPadding) + this._scrollbarWidth}px`
- })
-
- // Adjust sticky content margin
- SelectorEngine.find(SELECTOR_STICKY_CONTENT)
- .forEach(element => {
- const actualMargin = element.style.marginRight
- const calculatedMargin = window.getComputedStyle(element)['margin-right']
- Manipulator.setDataAttribute(element, 'margin-right', actualMargin)
- element.style.marginRight = `${Number.parseFloat(calculatedMargin) - this._scrollbarWidth}px`
- })
-
- // Adjust body padding
- const actualPadding = document.body.style.paddingRight
- const calculatedPadding = window.getComputedStyle(document.body)['padding-right']
-
- Manipulator.setDataAttribute(document.body, 'padding-right', actualPadding)
- document.body.style.paddingRight = `${Number.parseFloat(calculatedPadding) + this._scrollbarWidth}px`
- }
-
- document.body.classList.add(CLASS_NAME_OPEN)
- }
-
- _resetScrollbar() {
- // Restore fixed content padding
- SelectorEngine.find(SELECTOR_FIXED_CONTENT)
- .forEach(element => {
- const padding = Manipulator.getDataAttribute(element, 'padding-right')
- if (typeof padding !== 'undefined') {
- Manipulator.removeDataAttribute(element, 'padding-right')
- element.style.paddingRight = padding
- }
- })
-
- // Restore sticky content and navbar-toggler margin
- SelectorEngine.find(`${SELECTOR_STICKY_CONTENT}`)
- .forEach(element => {
- const margin = Manipulator.getDataAttribute(element, 'margin-right')
- if (typeof margin !== 'undefined') {
- Manipulator.removeDataAttribute(element, 'margin-right')
- element.style.marginRight = margin
- }
- })
-
- // Restore body padding
- const padding = Manipulator.getDataAttribute(document.body, 'padding-right')
- if (typeof padding === 'undefined') {
- document.body.style.paddingRight = ''
- } else {
- Manipulator.removeDataAttribute(document.body, 'padding-right')
- document.body.style.paddingRight = padding
- }
- }
-
- _getScrollbarWidth() { // thx d.walsh
- const scrollDiv = document.createElement('div')
- scrollDiv.className = CLASS_NAME_SCROLLBAR_MEASURER
- document.body.appendChild(scrollDiv)
- const scrollbarWidth = scrollDiv.getBoundingClientRect().width - scrollDiv.clientWidth
- document.body.removeChild(scrollDiv)
- return scrollbarWidth
- }
-
// Static
static jQueryInterface(config, relatedTarget) {
return this.each(function () {
- let data = Data.getData(this, DATA_KEY)
- const _config = {
- ...Default,
- ...Manipulator.getDataAttributes(this),
- ...(typeof config === 'object' && config ? config : {})
- }
+ const data = Modal.getInstance(this) || new Modal(this, typeof config === 'object' ? config : {})
- if (!data) {
- data = new Modal(this, _config)
+ if (typeof config !== 'string') {
+ return
}
- if (typeof config === 'string') {
- if (typeof data[config] === 'undefined') {
- throw new TypeError(`No method named "${config}"`)
- }
-
- data[config](relatedTarget)
+ if (typeof data[config] === 'undefined') {
+ throw new TypeError(`No method named "${config}"`)
}
+
+ data[config](relatedTarget)
})
}
}
@@ -565,7 +433,7 @@ class Modal extends BaseComponent {
EventHandler.on(document, EVENT_CLICK_DATA_API, SELECTOR_DATA_TOGGLE, function (event) {
const target = getElementFromSelector(this)
- if (this.tagName === 'A' || this.tagName === 'AREA') {
+ if (['A', 'AREA'].includes(this.tagName)) {
event.preventDefault()
}
@@ -582,17 +450,9 @@ EventHandler.on(document, EVENT_CLICK_DATA_API, SELECTOR_DATA_TOGGLE, function (
})
})
- let data = Data.getData(target, DATA_KEY)
- if (!data) {
- const config = {
- ...Manipulator.getDataAttributes(target),
- ...Manipulator.getDataAttributes(this)
- }
-
- data = new Modal(target, config)
- }
+ const data = Modal.getInstance(target) || new Modal(target)
- data.show(this)
+ data.toggle(this)
})
/**
@@ -602,18 +462,6 @@ EventHandler.on(document, EVENT_CLICK_DATA_API, SELECTOR_DATA_TOGGLE, function (
* add .Modal to jQuery only if jQuery is present
*/
-onDOMContentLoaded(() => {
- const $ = getjQuery()
- /* istanbul ignore if */
- if ($) {
- const JQUERY_NO_CONFLICT = $.fn[NAME]
- $.fn[NAME] = Modal.jQueryInterface
- $.fn[NAME].Constructor = Modal
- $.fn[NAME].noConflict = () => {
- $.fn[NAME] = JQUERY_NO_CONFLICT
- return Modal.jQueryInterface
- }
- }
-})
+defineJQueryPlugin(NAME, Modal)
export default Modal
diff --git a/js/src/offcanvas.js b/js/src/offcanvas.js
new file mode 100644
index 000000000..7fcdfb48a
--- /dev/null
+++ b/js/src/offcanvas.js
@@ -0,0 +1,285 @@
+/**
+ * --------------------------------------------------------------------------
+ * Bootstrap (v5.0.0-beta3): offcanvas.js
+ * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
+ * --------------------------------------------------------------------------
+ */
+
+import {
+ defineJQueryPlugin,
+ emulateTransitionEnd,
+ getElementFromSelector,
+ getTransitionDurationFromElement,
+ isDisabled,
+ isVisible,
+ typeCheckConfig
+} from './util/index'
+import { hide as scrollBarHide, reset as scrollBarReset } from './util/scrollbar'
+import Data from './dom/data'
+import EventHandler from './dom/event-handler'
+import BaseComponent from './base-component'
+import SelectorEngine from './dom/selector-engine'
+import Manipulator from './dom/manipulator'
+import Backdrop from './util/backdrop'
+
+/**
+ * ------------------------------------------------------------------------
+ * Constants
+ * ------------------------------------------------------------------------
+ */
+
+const NAME = 'offcanvas'
+const DATA_KEY = 'bs.offcanvas'
+const EVENT_KEY = `.${DATA_KEY}`
+const DATA_API_KEY = '.data-api'
+const EVENT_LOAD_DATA_API = `load${EVENT_KEY}${DATA_API_KEY}`
+const ESCAPE_KEY = 'Escape'
+
+const Default = {
+ backdrop: true,
+ keyboard: true,
+ scroll: false
+}
+
+const DefaultType = {
+ backdrop: 'boolean',
+ keyboard: 'boolean',
+ scroll: 'boolean'
+}
+
+const CLASS_NAME_SHOW = 'show'
+const OPEN_SELECTOR = '.offcanvas.show'
+
+const EVENT_SHOW = `show${EVENT_KEY}`
+const EVENT_SHOWN = `shown${EVENT_KEY}`
+const EVENT_HIDE = `hide${EVENT_KEY}`
+const EVENT_HIDDEN = `hidden${EVENT_KEY}`
+const EVENT_FOCUSIN = `focusin${EVENT_KEY}`
+const EVENT_CLICK_DATA_API = `click${EVENT_KEY}${DATA_API_KEY}`
+const EVENT_CLICK_DISMISS = `click.dismiss${EVENT_KEY}`
+const EVENT_KEYDOWN_DISMISS = `keydown.dismiss${EVENT_KEY}`
+
+const SELECTOR_DATA_DISMISS = '[data-bs-dismiss="offcanvas"]'
+const SELECTOR_DATA_TOGGLE = '[data-bs-toggle="offcanvas"]'
+
+/**
+ * ------------------------------------------------------------------------
+ * Class Definition
+ * ------------------------------------------------------------------------
+ */
+
+class Offcanvas extends BaseComponent {
+ constructor(element, config) {
+ super(element)
+
+ this._config = this._getConfig(config)
+ this._isShown = false
+ this._backdrop = this._initializeBackDrop()
+ this._addEventListeners()
+ }
+
+ // Getters
+
+ static get Default() {
+ return Default
+ }
+
+ static get DATA_KEY() {
+ return DATA_KEY
+ }
+
+ // Public
+
+ toggle(relatedTarget) {
+ return this._isShown ? this.hide() : this.show(relatedTarget)
+ }
+
+ show(relatedTarget) {
+ if (this._isShown) {
+ return
+ }
+
+ const showEvent = EventHandler.trigger(this._element, EVENT_SHOW, { relatedTarget })
+
+ if (showEvent.defaultPrevented) {
+ return
+ }
+
+ this._isShown = true
+ this._element.style.visibility = 'visible'
+
+ this._backdrop.show()
+
+ if (!this._config.scroll) {
+ scrollBarHide()
+ this._enforceFocusOnElement(this._element)
+ }
+
+ this._element.removeAttribute('aria-hidden')
+ this._element.setAttribute('aria-modal', true)
+ this._element.setAttribute('role', 'dialog')
+ this._element.classList.add(CLASS_NAME_SHOW)
+
+ const completeCallBack = () => {
+ EventHandler.trigger(this._element, EVENT_SHOWN, { relatedTarget })
+ }
+
+ const transitionDuration = getTransitionDurationFromElement(this._element)
+ EventHandler.one(this._element, 'transitionend', completeCallBack)
+ emulateTransitionEnd(this._element, transitionDuration)
+ }
+
+ hide() {
+ if (!this._isShown) {
+ return
+ }
+
+ const hideEvent = EventHandler.trigger(this._element, EVENT_HIDE)
+
+ if (hideEvent.defaultPrevented) {
+ return
+ }
+
+ EventHandler.off(document, EVENT_FOCUSIN)
+ this._element.blur()
+ this._isShown = false
+ this._element.classList.remove(CLASS_NAME_SHOW)
+ this._backdrop.hide()
+
+ const completeCallback = () => {
+ this._element.setAttribute('aria-hidden', true)
+ this._element.removeAttribute('aria-modal')
+ this._element.removeAttribute('role')
+ this._element.style.visibility = 'hidden'
+
+ if (!this._config.scroll) {
+ scrollBarReset()
+ }
+
+ EventHandler.trigger(this._element, EVENT_HIDDEN)
+ }
+
+ const transitionDuration = getTransitionDurationFromElement(this._element)
+ EventHandler.one(this._element, 'transitionend', completeCallback)
+ emulateTransitionEnd(this._element, transitionDuration)
+ }
+
+ dispose() {
+ this._backdrop.dispose()
+ super.dispose()
+ EventHandler.off(document, EVENT_FOCUSIN)
+
+ this._config = null
+ this._backdrop = null
+ }
+
+ // Private
+
+ _getConfig(config) {
+ config = {
+ ...Default,
+ ...Manipulator.getDataAttributes(this._element),
+ ...(typeof config === 'object' ? config : {})
+ }
+ typeCheckConfig(NAME, config, DefaultType)
+ return config
+ }
+
+ _initializeBackDrop() {
+ return new Backdrop({
+ isVisible: this._config.backdrop,
+ isAnimated: true,
+ rootElement: this._element.parentNode,
+ clickCallback: () => this.hide()
+ })
+ }
+
+ _enforceFocusOnElement(element) {
+ EventHandler.off(document, EVENT_FOCUSIN) // guard against infinite focus loop
+ EventHandler.on(document, EVENT_FOCUSIN, event => {
+ if (document !== event.target &&
+ element !== event.target &&
+ !element.contains(event.target)) {
+ element.focus()
+ }
+ })
+ element.focus()
+ }
+
+ _addEventListeners() {
+ EventHandler.on(this._element, EVENT_CLICK_DISMISS, SELECTOR_DATA_DISMISS, () => this.hide())
+
+ EventHandler.on(this._element, EVENT_KEYDOWN_DISMISS, event => {
+ if (this._config.keyboard && event.key === ESCAPE_KEY) {
+ this.hide()
+ }
+ })
+ }
+
+ // Static
+
+ static jQueryInterface(config) {
+ return this.each(function () {
+ const data = Data.get(this, DATA_KEY) || new Offcanvas(this, typeof config === 'object' ? config : {})
+
+ if (typeof config !== 'string') {
+ return
+ }
+
+ if (data[config] === undefined || config.startsWith('_') || config === 'constructor') {
+ throw new TypeError(`No method named "${config}"`)
+ }
+
+ data[config](this)
+ })
+ }
+}
+
+/**
+ * ------------------------------------------------------------------------
+ * Data Api implementation
+ * ------------------------------------------------------------------------
+ */
+
+EventHandler.on(document, EVENT_CLICK_DATA_API, SELECTOR_DATA_TOGGLE, function (event) {
+ const target = getElementFromSelector(this)
+
+ if (['A', 'AREA'].includes(this.tagName)) {
+ event.preventDefault()
+ }
+
+ if (isDisabled(this)) {
+ return
+ }
+
+ EventHandler.one(target, EVENT_HIDDEN, () => {
+ // focus on trigger when it is closed
+ if (isVisible(this)) {
+ this.focus()
+ }
+ })
+
+ // avoid conflict when clicking a toggler of an offcanvas, while another is open
+ const allReadyOpen = SelectorEngine.findOne(OPEN_SELECTOR)
+ if (allReadyOpen && allReadyOpen !== target) {
+ Offcanvas.getInstance(allReadyOpen).hide()
+ }
+
+ const data = Data.get(target, DATA_KEY) || new Offcanvas(target)
+
+ data.toggle(this)
+})
+
+EventHandler.on(window, EVENT_LOAD_DATA_API, () => {
+ SelectorEngine.find(OPEN_SELECTOR).forEach(el => (Data.get(el, DATA_KEY) || new Offcanvas(el)).show())
+})
+
+/**
+ * ------------------------------------------------------------------------
+ * jQuery
+ * ------------------------------------------------------------------------
+ */
+
+defineJQueryPlugin(NAME, Offcanvas)
+
+export default Offcanvas
diff --git a/js/src/popover.js b/js/src/popover.js
index d8bd92eef..fa8d2c961 100644
--- a/js/src/popover.js
+++ b/js/src/popover.js
@@ -1,11 +1,11 @@
/**
* --------------------------------------------------------------------------
- * Bootstrap (v5.0.0-alpha3): popover.js
+ * Bootstrap (v5.0.0-beta3): popover.js
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
* --------------------------------------------------------------------------
*/
-import { getjQuery, onDOMContentLoaded } from './util/index'
+import { defineJQueryPlugin } from './util/index'
import Data from './dom/data'
import SelectorEngine from './dom/selector-engine'
import Tooltip from './tooltip'
@@ -25,6 +25,7 @@ const BSCLS_PREFIX_REGEX = new RegExp(`(^|\\s)${CLASS_PREFIX}\\S+`, 'g')
const Default = {
...Tooltip.Default,
placement: 'right',
+ offset: [0, 8],
trigger: 'click',
content: '',
template: '<div class="popover" role="tooltip">' +
@@ -135,7 +136,7 @@ class Popover extends Tooltip {
static jQueryInterface(config) {
return this.each(function () {
- let data = Data.getData(this, DATA_KEY)
+ let data = Data.get(this, DATA_KEY)
const _config = typeof config === 'object' ? config : null
if (!data && /dispose|hide/.test(config)) {
@@ -144,7 +145,7 @@ class Popover extends Tooltip {
if (!data) {
data = new Popover(this, _config)
- Data.setData(this, DATA_KEY, data)
+ Data.set(this, DATA_KEY, data)
}
if (typeof config === 'string') {
@@ -165,18 +166,6 @@ class Popover extends Tooltip {
* add .Popover to jQuery only if jQuery is present
*/
-onDOMContentLoaded(() => {
- const $ = getjQuery()
- /* istanbul ignore if */
- if ($) {
- const JQUERY_NO_CONFLICT = $.fn[NAME]
- $.fn[NAME] = Popover.jQueryInterface
- $.fn[NAME].Constructor = Popover
- $.fn[NAME].noConflict = () => {
- $.fn[NAME] = JQUERY_NO_CONFLICT
- return Popover.jQueryInterface
- }
- }
-})
+defineJQueryPlugin(NAME, Popover)
export default Popover
diff --git a/js/src/scrollspy.js b/js/src/scrollspy.js
index a05e57d62..4e830b530 100644
--- a/js/src/scrollspy.js
+++ b/js/src/scrollspy.js
@@ -1,19 +1,17 @@
/**
* --------------------------------------------------------------------------
- * Bootstrap (v5.0.0-alpha3): scrollspy.js
+ * Bootstrap (v5.0.0-beta3): scrollspy.js
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
* --------------------------------------------------------------------------
*/
import {
- getjQuery,
- onDOMContentLoaded,
+ defineJQueryPlugin,
getSelectorFromElement,
getUID,
isElement,
typeCheckConfig
} from './util/index'
-import Data from './dom/data'
import EventHandler from './dom/event-handler'
import Manipulator from './dom/manipulator'
import SelectorEngine from './dom/selector-engine'
@@ -69,7 +67,7 @@ const METHOD_POSITION = 'position'
class ScrollSpy extends BaseComponent {
constructor(element, config) {
super(element)
- this._scrollElement = element.tagName === 'BODY' ? window : element
+ this._scrollElement = this._element.tagName === 'BODY' ? window : this._element
this._config = this._getConfig(config)
this._selector = `${this._config.target} ${SELECTOR_NAV_LINKS}, ${this._config.target} ${SELECTOR_LIST_ITEMS}, ${this._config.target} .${CLASS_NAME_DROPDOWN_ITEM}`
this._offsets = []
@@ -77,7 +75,7 @@ class ScrollSpy extends BaseComponent {
this._activeTarget = null
this._scrollHeight = 0
- EventHandler.on(this._scrollElement, EVENT_SCROLL, event => this._process(event))
+ EventHandler.on(this._scrollElement, EVENT_SCROLL, () => this._process())
this.refresh()
this._process()
@@ -156,6 +154,7 @@ class ScrollSpy extends BaseComponent {
_getConfig(config) {
config = {
...Default,
+ ...Manipulator.getDataAttributes(this._element),
...(typeof config === 'object' && config ? config : {})
}
@@ -279,20 +278,17 @@ class ScrollSpy extends BaseComponent {
static jQueryInterface(config) {
return this.each(function () {
- let data = Data.getData(this, DATA_KEY)
- const _config = typeof config === 'object' && config
+ const data = ScrollSpy.getInstance(this) || new ScrollSpy(this, typeof config === 'object' ? config : {})
- if (!data) {
- data = new ScrollSpy(this, _config)
+ if (typeof config !== 'string') {
+ return
}
- if (typeof config === 'string') {
- if (typeof data[config] === 'undefined') {
- throw new TypeError(`No method named "${config}"`)
- }
-
- data[config]()
+ if (typeof data[config] === 'undefined') {
+ throw new TypeError(`No method named "${config}"`)
}
+
+ data[config]()
})
}
}
@@ -305,7 +301,7 @@ class ScrollSpy extends BaseComponent {
EventHandler.on(window, EVENT_LOAD_DATA_API, () => {
SelectorEngine.find(SELECTOR_DATA_SPY)
- .forEach(spy => new ScrollSpy(spy, Manipulator.getDataAttributes(spy)))
+ .forEach(spy => new ScrollSpy(spy))
})
/**
@@ -315,18 +311,6 @@ EventHandler.on(window, EVENT_LOAD_DATA_API, () => {
* add .ScrollSpy to jQuery only if jQuery is present
*/
-onDOMContentLoaded(() => {
- const $ = getjQuery()
- /* istanbul ignore if */
- if ($) {
- const JQUERY_NO_CONFLICT = $.fn[NAME]
- $.fn[NAME] = ScrollSpy.jQueryInterface
- $.fn[NAME].Constructor = ScrollSpy
- $.fn[NAME].noConflict = () => {
- $.fn[NAME] = JQUERY_NO_CONFLICT
- return ScrollSpy.jQueryInterface
- }
- }
-})
+defineJQueryPlugin(NAME, ScrollSpy)
export default ScrollSpy
diff --git a/js/src/tab.js b/js/src/tab.js
index c8aac3be7..7301779d6 100644
--- a/js/src/tab.js
+++ b/js/src/tab.js
@@ -1,17 +1,16 @@
/**
* --------------------------------------------------------------------------
- * Bootstrap (v5.0.0-alpha3): tab.js
+ * Bootstrap (v5.0.0-beta3): tab.js
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
* --------------------------------------------------------------------------
*/
import {
- getjQuery,
- onDOMContentLoaded,
- TRANSITION_END,
+ defineJQueryPlugin,
emulateTransitionEnd,
getElementFromSelector,
getTransitionDurationFromElement,
+ isDisabled,
reflow
} from './util/index'
import Data from './dom/data'
@@ -38,7 +37,6 @@ const EVENT_CLICK_DATA_API = `click${EVENT_KEY}${DATA_API_KEY}`
const CLASS_NAME_DROPDOWN_MENU = 'dropdown-menu'
const CLASS_NAME_ACTIVE = 'active'
-const CLASS_NAME_DISABLED = 'disabled'
const CLASS_NAME_FADE = 'fade'
const CLASS_NAME_SHOW = 'show'
@@ -68,8 +66,7 @@ class Tab extends BaseComponent {
show() {
if ((this._element.parentNode &&
this._element.parentNode.nodeType === Node.ELEMENT_NODE &&
- this._element.classList.contains(CLASS_NAME_ACTIVE)) ||
- this._element.classList.contains(CLASS_NAME_DISABLED)) {
+ this._element.classList.contains(CLASS_NAME_ACTIVE))) {
return
}
@@ -83,13 +80,11 @@ class Tab extends BaseComponent {
previous = previous[previous.length - 1]
}
- let hideEvent = null
-
- if (previous) {
- hideEvent = EventHandler.trigger(previous, EVENT_HIDE, {
+ const hideEvent = previous ?
+ EventHandler.trigger(previous, EVENT_HIDE, {
relatedTarget: this._element
- })
- }
+ }) :
+ null
const showEvent = EventHandler.trigger(this._element, EVENT_SHOW, {
relatedTarget: previous
@@ -133,7 +128,7 @@ class Tab extends BaseComponent {
const transitionDuration = getTransitionDurationFromElement(active)
active.classList.remove(CLASS_NAME_SHOW)
- EventHandler.one(active, TRANSITION_END, complete)
+ EventHandler.one(active, 'transitionend', complete)
emulateTransitionEnd(active, transitionDuration)
} else {
complete()
@@ -166,11 +161,16 @@ class Tab extends BaseComponent {
element.classList.add(CLASS_NAME_SHOW)
}
- if (element.parentNode && element.parentNode.classList.contains(CLASS_NAME_DROPDOWN_MENU)) {
+ let parent = element.parentNode
+ if (parent && parent.nodeName === 'LI') {
+ parent = parent.parentNode
+ }
+
+ if (parent && parent.classList.contains(CLASS_NAME_DROPDOWN_MENU)) {
const dropdownElement = element.closest(SELECTOR_DROPDOWN)
if (dropdownElement) {
- SelectorEngine.find(SELECTOR_DROPDOWN_TOGGLE)
+ SelectorEngine.find(SELECTOR_DROPDOWN_TOGGLE, dropdownElement)
.forEach(dropdown => dropdown.classList.add(CLASS_NAME_ACTIVE))
}
@@ -186,7 +186,7 @@ class Tab extends BaseComponent {
static jQueryInterface(config) {
return this.each(function () {
- const data = Data.getData(this, DATA_KEY) || new Tab(this)
+ const data = Data.get(this, DATA_KEY) || new Tab(this)
if (typeof config === 'string') {
if (typeof data[config] === 'undefined') {
@@ -206,9 +206,15 @@ class Tab extends BaseComponent {
*/
EventHandler.on(document, EVENT_CLICK_DATA_API, SELECTOR_DATA_TOGGLE, function (event) {
- event.preventDefault()
+ if (['A', 'AREA'].includes(this.tagName)) {
+ event.preventDefault()
+ }
- const data = Data.getData(this, DATA_KEY) || new Tab(this)
+ if (isDisabled(this)) {
+ return
+ }
+
+ const data = Data.get(this, DATA_KEY) || new Tab(this)
data.show()
})
@@ -219,18 +225,6 @@ EventHandler.on(document, EVENT_CLICK_DATA_API, SELECTOR_DATA_TOGGLE, function (
* add .Tab to jQuery only if jQuery is present
*/
-onDOMContentLoaded(() => {
- const $ = getjQuery()
- /* istanbul ignore if */
- if ($) {
- const JQUERY_NO_CONFLICT = $.fn[NAME]
- $.fn[NAME] = Tab.jQueryInterface
- $.fn[NAME].Constructor = Tab
- $.fn[NAME].noConflict = () => {
- $.fn[NAME] = JQUERY_NO_CONFLICT
- return Tab.jQueryInterface
- }
- }
-})
+defineJQueryPlugin(NAME, Tab)
export default Tab
diff --git a/js/src/toast.js b/js/src/toast.js
index 30df4606a..5d762b29d 100644
--- a/js/src/toast.js
+++ b/js/src/toast.js
@@ -1,14 +1,12 @@
/**
* --------------------------------------------------------------------------
- * Bootstrap (v5.0.0-alpha3): toast.js
+ * Bootstrap (v5.0.0-beta3): toast.js
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
* --------------------------------------------------------------------------
*/
import {
- getjQuery,
- onDOMContentLoaded,
- TRANSITION_END,
+ defineJQueryPlugin,
emulateTransitionEnd,
getTransitionDurationFromElement,
reflow,
@@ -117,7 +115,7 @@ class Toast extends BaseComponent {
if (this._config.animation) {
const transitionDuration = getTransitionDurationFromElement(this._element)
- EventHandler.one(this._element, TRANSITION_END, complete)
+ EventHandler.one(this._element, 'transitionend', complete)
emulateTransitionEnd(this._element, transitionDuration)
} else {
complete()
@@ -144,7 +142,7 @@ class Toast extends BaseComponent {
if (this._config.animation) {
const transitionDuration = getTransitionDurationFromElement(this._element)
- EventHandler.one(this._element, TRANSITION_END, complete)
+ EventHandler.one(this._element, 'transitionend', complete)
emulateTransitionEnd(this._element, transitionDuration)
} else {
complete()
@@ -158,8 +156,6 @@ class Toast extends BaseComponent {
this._element.classList.remove(CLASS_NAME_SHOW)
}
- EventHandler.off(this._element, EVENT_CLICK_DISMISS)
-
super.dispose()
this._config = null
}
@@ -191,7 +187,7 @@ class Toast extends BaseComponent {
static jQueryInterface(config) {
return this.each(function () {
- let data = Data.getData(this, DATA_KEY)
+ let data = Data.get(this, DATA_KEY)
const _config = typeof config === 'object' && config
if (!data) {
@@ -216,18 +212,6 @@ class Toast extends BaseComponent {
* add .Toast to jQuery only if jQuery is present
*/
-onDOMContentLoaded(() => {
- const $ = getjQuery()
- /* istanbul ignore if */
- if ($) {
- const JQUERY_NO_CONFLICT = $.fn[NAME]
- $.fn[NAME] = Toast.jQueryInterface
- $.fn[NAME].Constructor = Toast
- $.fn[NAME].noConflict = () => {
- $.fn[NAME] = JQUERY_NO_CONFLICT
- return Toast.jQueryInterface
- }
- }
-})
+defineJQueryPlugin(NAME, Toast)
export default Toast
diff --git a/js/src/tooltip.js b/js/src/tooltip.js
index 17148ed9a..ecea04390 100644
--- a/js/src/tooltip.js
+++ b/js/src/tooltip.js
@@ -1,14 +1,14 @@
/**
* --------------------------------------------------------------------------
- * Bootstrap (v5.0.0-alpha3): tooltip.js
+ * Bootstrap (v5.0.0-beta3): tooltip.js
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
* --------------------------------------------------------------------------
*/
+import * as Popper from '@popperjs/core'
+
import {
- getjQuery,
- onDOMContentLoaded,
- TRANSITION_END,
+ defineJQueryPlugin,
emulateTransitionEnd,
findShadowRoot,
getTransitionDurationFromElement,
@@ -25,7 +25,6 @@ import {
import Data from './dom/data'
import EventHandler from './dom/event-handler'
import Manipulator from './dom/manipulator'
-import Popper from 'popper.js'
import SelectorEngine from './dom/selector-engine'
import BaseComponent from './base-component'
@@ -51,23 +50,23 @@ const DefaultType = {
html: 'boolean',
selector: '(string|boolean)',
placement: '(string|function)',
- offset: '(number|string|function)',
+ offset: '(array|string|function)',
container: '(string|element|boolean)',
- fallbackPlacement: '(string|array)',
+ fallbackPlacements: 'array',
boundary: '(string|element)',
customClass: '(string|function)',
sanitize: 'boolean',
sanitizeFn: '(null|function)',
allowList: 'object',
- popperConfig: '(null|object)'
+ popperConfig: '(null|object|function)'
}
const AttachmentMap = {
AUTO: 'auto',
TOP: 'top',
- RIGHT: isRTL ? 'left' : 'right',
+ RIGHT: isRTL() ? 'left' : 'right',
BOTTOM: 'bottom',
- LEFT: isRTL ? 'right' : 'left'
+ LEFT: isRTL() ? 'right' : 'left'
}
const Default = {
@@ -82,10 +81,10 @@ const Default = {
html: false,
selector: false,
placement: 'top',
- offset: 0,
+ offset: [0, 0],
container: false,
- fallbackPlacement: 'flip',
- boundary: 'scrollParent',
+ fallbackPlacements: ['top', 'right', 'bottom', 'left'],
+ boundary: 'clippingParents',
customClass: '',
sanitize: true,
sanitizeFn: null,
@@ -194,13 +193,7 @@ class Tooltip extends BaseComponent {
}
if (event) {
- const dataKey = this.constructor.DATA_KEY
- let context = Data.getData(event.delegateTarget, dataKey)
-
- if (!context) {
- context = new this.constructor(event.delegateTarget, this._getDelegateConfig())
- Data.setData(event.delegateTarget, dataKey, context)
- }
+ const context = this._initializeOnDelegatedTarget(event)
context._activeTrigger.click = !context._activeTrigger.click
@@ -222,10 +215,9 @@ class Tooltip extends BaseComponent {
dispose() {
clearTimeout(this._timeout)
- EventHandler.off(this._element, this.constructor.EVENT_KEY)
EventHandler.off(this._element.closest(`.${CLASS_NAME_MODAL}`), 'hide.bs.modal', this._hideModalHandler)
- if (this.tip) {
+ if (this.tip && this.tip.parentNode) {
this.tip.parentNode.removeChild(this.tip)
}
@@ -248,86 +240,87 @@ class Tooltip extends BaseComponent {
throw new Error('Please use show on visible elements')
}
- if (this.isWithContent() && this._isEnabled) {
- const showEvent = EventHandler.trigger(this._element, this.constructor.Event.SHOW)
- const shadowRoot = findShadowRoot(this._element)
- const isInTheDom = shadowRoot === null ?
- this._element.ownerDocument.documentElement.contains(this._element) :
- shadowRoot.contains(this._element)
+ if (!(this.isWithContent() && this._isEnabled)) {
+ return
+ }
- if (showEvent.defaultPrevented || !isInTheDom) {
- return
- }
+ const showEvent = EventHandler.trigger(this._element, this.constructor.Event.SHOW)
+ const shadowRoot = findShadowRoot(this._element)
+ const isInTheDom = shadowRoot === null ?
+ this._element.ownerDocument.documentElement.contains(this._element) :
+ shadowRoot.contains(this._element)
- const tip = this.getTipElement()
- const tipId = getUID(this.constructor.NAME)
+ if (showEvent.defaultPrevented || !isInTheDom) {
+ return
+ }
- tip.setAttribute('id', tipId)
- this._element.setAttribute('aria-describedby', tipId)
+ const tip = this.getTipElement()
+ const tipId = getUID(this.constructor.NAME)
- this.setContent()
+ tip.setAttribute('id', tipId)
+ this._element.setAttribute('aria-describedby', tipId)
- if (this.config.animation) {
- tip.classList.add(CLASS_NAME_FADE)
- }
+ this.setContent()
- const placement = typeof this.config.placement === 'function' ?
- this.config.placement.call(this, tip, this._element) :
- this.config.placement
+ if (this.config.animation) {
+ tip.classList.add(CLASS_NAME_FADE)
+ }
- const attachment = this._getAttachment(placement)
- this._addAttachmentClass(attachment)
+ const placement = typeof this.config.placement === 'function' ?
+ this.config.placement.call(this, tip, this._element) :
+ this.config.placement
- const container = this._getContainer()
- Data.setData(tip, this.constructor.DATA_KEY, this)
+ const attachment = this._getAttachment(placement)
+ this._addAttachmentClass(attachment)
- if (!this._element.ownerDocument.documentElement.contains(this.tip)) {
- container.appendChild(tip)
- }
+ const container = this._getContainer()
+ Data.set(tip, this.constructor.DATA_KEY, this)
+ if (!this._element.ownerDocument.documentElement.contains(this.tip)) {
+ container.appendChild(tip)
EventHandler.trigger(this._element, this.constructor.Event.INSERTED)
+ }
- this._popper = new Popper(this._element, tip, this._getPopperConfig(attachment))
-
- tip.classList.add(CLASS_NAME_SHOW)
+ if (this._popper) {
+ this._popper.update()
+ } else {
+ this._popper = Popper.createPopper(this._element, tip, this._getPopperConfig(attachment))
+ }
- const customClass = typeof this.config.customClass === 'function' ? this.config.customClass() : this.config.customClass
- if (customClass) {
- tip.classList.add(...customClass.split(' '))
- }
+ tip.classList.add(CLASS_NAME_SHOW)
- // If this is a touch-enabled device we add extra
- // empty mouseover listeners to the body's immediate children;
- // only needed because of broken event delegation on iOS
- // https://www.quirksmode.org/blog/archives/2014/02/mouse_event_bub.html
- if ('ontouchstart' in document.documentElement) {
- [].concat(...document.body.children).forEach(element => {
- EventHandler.on(element, 'mouseover', noop())
- })
- }
+ const customClass = typeof this.config.customClass === 'function' ? this.config.customClass() : this.config.customClass
+ if (customClass) {
+ tip.classList.add(...customClass.split(' '))
+ }
- const complete = () => {
- if (this.config.animation) {
- this._fixTransition()
- }
+ // If this is a touch-enabled device we add extra
+ // empty mouseover listeners to the body's immediate children;
+ // only needed because of broken event delegation on iOS
+ // https://www.quirksmode.org/blog/archives/2014/02/mouse_event_bub.html
+ if ('ontouchstart' in document.documentElement) {
+ [].concat(...document.body.children).forEach(element => {
+ EventHandler.on(element, 'mouseover', noop)
+ })
+ }
- const prevHoverState = this._hoverState
- this._hoverState = null
+ const complete = () => {
+ const prevHoverState = this._hoverState
- EventHandler.trigger(this._element, this.constructor.Event.SHOWN)
+ this._hoverState = null
+ EventHandler.trigger(this._element, this.constructor.Event.SHOWN)
- if (prevHoverState === HOVER_STATE_OUT) {
- this._leave(null, this)
- }
+ if (prevHoverState === HOVER_STATE_OUT) {
+ this._leave(null, this)
}
+ }
- if (this.tip.classList.contains(CLASS_NAME_FADE)) {
- const transitionDuration = getTransitionDurationFromElement(this.tip)
- EventHandler.one(this.tip, TRANSITION_END, complete)
- emulateTransitionEnd(this.tip, transitionDuration)
- } else {
- complete()
- }
+ if (this.tip.classList.contains(CLASS_NAME_FADE)) {
+ const transitionDuration = getTransitionDurationFromElement(this.tip)
+ EventHandler.one(this.tip, 'transitionend', complete)
+ emulateTransitionEnd(this.tip, transitionDuration)
+ } else {
+ complete()
}
}
@@ -338,6 +331,10 @@ class Tooltip extends BaseComponent {
const tip = this.getTipElement()
const complete = () => {
+ if (this._isWithActiveTrigger()) {
+ return
+ }
+
if (this._hoverState !== HOVER_STATE_SHOW && tip.parentNode) {
tip.parentNode.removeChild(tip)
}
@@ -345,7 +342,11 @@ class Tooltip extends BaseComponent {
this._cleanTipClass()
this._element.removeAttribute('aria-describedby')
EventHandler.trigger(this._element, this.constructor.Event.HIDDEN)
- this._popper.destroy()
+
+ if (this._popper) {
+ this._popper.destroy()
+ this._popper = null
+ }
}
const hideEvent = EventHandler.trigger(this._element, this.constructor.Event.HIDE)
@@ -369,7 +370,7 @@ class Tooltip extends BaseComponent {
if (this.tip.classList.contains(CLASS_NAME_FADE)) {
const transitionDuration = getTransitionDurationFromElement(tip)
- EventHandler.one(tip, TRANSITION_END, complete)
+ EventHandler.one(tip, 'transitionend', complete)
emulateTransitionEnd(tip, transitionDuration)
} else {
complete()
@@ -380,7 +381,7 @@ class Tooltip extends BaseComponent {
update() {
if (this._popper !== null) {
- this._popper.scheduleUpdate()
+ this._popper.update()
}
}
@@ -468,32 +469,77 @@ class Tooltip extends BaseComponent {
// Private
+ _initializeOnDelegatedTarget(event, context) {
+ const dataKey = this.constructor.DATA_KEY
+ context = context || Data.get(event.delegateTarget, dataKey)
+
+ if (!context) {
+ context = new this.constructor(event.delegateTarget, this._getDelegateConfig())
+ Data.set(event.delegateTarget, dataKey, context)
+ }
+
+ return context
+ }
+
+ _getOffset() {
+ const { offset } = this.config
+
+ if (typeof offset === 'string') {
+ return offset.split(',').map(val => Number.parseInt(val, 10))
+ }
+
+ if (typeof offset === 'function') {
+ return popperData => offset(popperData, this._element)
+ }
+
+ return offset
+ }
+
_getPopperConfig(attachment) {
- const defaultBsConfig = {
+ const defaultBsPopperConfig = {
placement: attachment,
- modifiers: {
- offset: this._getOffset(),
- flip: {
- behavior: this.config.fallbackPlacement
+ modifiers: [
+ {
+ name: 'flip',
+ options: {
+ fallbackPlacements: this.config.fallbackPlacements
+ }
+ },
+ {
+ name: 'offset',
+ options: {
+ offset: this._getOffset()
+ }
+ },
+ {
+ name: 'preventOverflow',
+ options: {
+ boundary: this.config.boundary
+ }
},
- arrow: {
- element: `.${this.constructor.NAME}-arrow`
+ {
+ name: 'arrow',
+ options: {
+ element: `.${this.constructor.NAME}-arrow`
+ }
},
- preventOverflow: {
- boundariesElement: this.config.boundary
+ {
+ name: 'onChange',
+ enabled: true,
+ phase: 'afterWrite',
+ fn: data => this._handlePopperPlacementChange(data)
}
- },
- onCreate: data => {
- if (data.originalPlacement !== data.placement) {
+ ],
+ onFirstUpdate: data => {
+ if (data.options.placement !== data.placement) {
this._handlePopperPlacementChange(data)
}
- },
- onUpdate: data => this._handlePopperPlacementChange(data)
+ }
}
return {
- ...defaultBsConfig,
- ...this.config.popperConfig
+ ...defaultBsPopperConfig,
+ ...(typeof this.config.popperConfig === 'function' ? this.config.popperConfig(defaultBsPopperConfig) : this.config.popperConfig)
}
}
@@ -501,25 +547,6 @@ class Tooltip extends BaseComponent {
this.getTipElement().classList.add(`${CLASS_PREFIX}-${this.updateAttachment(attachment)}`)
}
- _getOffset() {
- const offset = {}
-
- if (typeof this.config.offset === 'function') {
- offset.fn = data => {
- data.offsets = {
- ...data.offsets,
- ...(this.config.offset(data.offsets, this._element) || {})
- }
-
- return data
- }
- } else {
- offset.offset = this.config.offset
- }
-
- return offset
- }
-
_getContainer() {
if (this.config.container === false) {
return document.body
@@ -541,8 +568,7 @@ class Tooltip extends BaseComponent {
triggers.forEach(trigger => {
if (trigger === 'click') {
- EventHandler.on(this._element, this.constructor.Event.CLICK, this.config.selector, event => this.toggle(event)
- )
+ EventHandler.on(this._element, this.constructor.Event.CLICK, this.config.selector, event => this.toggle(event))
} else if (trigger !== TRIGGER_MANUAL) {
const eventIn = trigger === TRIGGER_HOVER ?
this.constructor.Event.MOUSEENTER :
@@ -590,16 +616,7 @@ class Tooltip extends BaseComponent {
}
_enter(event, context) {
- const dataKey = this.constructor.DATA_KEY
- context = context || Data.getData(event.delegateTarget, dataKey)
-
- if (!context) {
- context = new this.constructor(
- event.delegateTarget,
- this._getDelegateConfig()
- )
- Data.setData(event.delegateTarget, dataKey, context)
- }
+ context = this._initializeOnDelegatedTarget(event, context)
if (event) {
context._activeTrigger[
@@ -629,21 +646,12 @@ class Tooltip extends BaseComponent {
}
_leave(event, context) {
- const dataKey = this.constructor.DATA_KEY
- context = context || Data.getData(event.delegateTarget, dataKey)
-
- if (!context) {
- context = new this.constructor(
- event.delegateTarget,
- this._getDelegateConfig()
- )
- Data.setData(event.delegateTarget, dataKey, context)
- }
+ context = this._initializeOnDelegatedTarget(event, context)
if (event) {
context._activeTrigger[
event.type === 'focusout' ? TRIGGER_FOCUS : TRIGGER_HOVER
- ] = false
+ ] = context._element.contains(event.relatedTarget)
}
if (context._isWithActiveTrigger()) {
@@ -743,30 +751,22 @@ class Tooltip extends BaseComponent {
}
_handlePopperPlacementChange(popperData) {
- this.tip = popperData.instance.popper
- this._cleanTipClass()
- this._addAttachmentClass(this._getAttachment(popperData.placement))
- }
+ const { state } = popperData
- _fixTransition() {
- const tip = this.getTipElement()
- const initConfigAnimation = this.config.animation
- if (tip.getAttribute('x-placement') !== null) {
+ if (!state) {
return
}
- tip.classList.remove(CLASS_NAME_FADE)
- this.config.animation = false
- this.hide()
- this.show()
- this.config.animation = initConfigAnimation
+ this.tip = state.elements.popper
+ this._cleanTipClass()
+ this._addAttachmentClass(this._getAttachment(state.placement))
}
// Static
static jQueryInterface(config) {
return this.each(function () {
- let data = Data.getData(this, DATA_KEY)
+ let data = Data.get(this, DATA_KEY)
const _config = typeof config === 'object' && config
if (!data && /dispose|hide/.test(config)) {
@@ -795,18 +795,6 @@ class Tooltip extends BaseComponent {
* add .Tooltip to jQuery only if jQuery is present
*/
-onDOMContentLoaded(() => {
- const $ = getjQuery()
- /* istanbul ignore if */
- if ($) {
- const JQUERY_NO_CONFLICT = $.fn[NAME]
- $.fn[NAME] = Tooltip.jQueryInterface
- $.fn[NAME].Constructor = Tooltip
- $.fn[NAME].noConflict = () => {
- $.fn[NAME] = JQUERY_NO_CONFLICT
- return Tooltip.jQueryInterface
- }
- }
-})
+defineJQueryPlugin(NAME, Tooltip)
export default Tooltip
diff --git a/js/src/util/backdrop.js b/js/src/util/backdrop.js
new file mode 100644
index 000000000..a9d28bd10
--- /dev/null
+++ b/js/src/util/backdrop.js
@@ -0,0 +1,133 @@
+/**
+ * --------------------------------------------------------------------------
+ * Bootstrap (v5.0.0-beta3): util/backdrop.js
+ * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
+ * --------------------------------------------------------------------------
+ */
+
+import EventHandler from '../dom/event-handler'
+import { emulateTransitionEnd, execute, getTransitionDurationFromElement, reflow, typeCheckConfig } from './index'
+
+const Default = {
+ isVisible: true, // if false, we use the backdrop helper without adding any element to the dom
+ isAnimated: false,
+ rootElement: document.body, // give the choice to place backdrop under different elements
+ clickCallback: null
+}
+
+const DefaultType = {
+ isVisible: 'boolean',
+ isAnimated: 'boolean',
+ rootElement: 'element',
+ clickCallback: '(function|null)'
+}
+const NAME = 'backdrop'
+const CLASS_NAME_BACKDROP = 'modal-backdrop'
+const CLASS_NAME_FADE = 'fade'
+const CLASS_NAME_SHOW = 'show'
+
+const EVENT_MOUSEDOWN = `mousedown.bs.${NAME}`
+
+class Backdrop {
+ constructor(config) {
+ this._config = this._getConfig(config)
+ this._isAppended = false
+ this._element = null
+ }
+
+ show(callback) {
+ if (!this._config.isVisible) {
+ execute(callback)
+ return
+ }
+
+ this._append()
+
+ if (this._config.isAnimated) {
+ reflow(this._getElement())
+ }
+
+ this._getElement().classList.add(CLASS_NAME_SHOW)
+
+ this._emulateAnimation(() => {
+ execute(callback)
+ })
+ }
+
+ hide(callback) {
+ if (!this._config.isVisible) {
+ execute(callback)
+ return
+ }
+
+ this._getElement().classList.remove(CLASS_NAME_SHOW)
+
+ this._emulateAnimation(() => {
+ this.dispose()
+ execute(callback)
+ })
+ }
+
+ // Private
+
+ _getElement() {
+ if (!this._element) {
+ const backdrop = document.createElement('div')
+ backdrop.className = CLASS_NAME_BACKDROP
+ if (this._config.isAnimated) {
+ backdrop.classList.add(CLASS_NAME_FADE)
+ }
+
+ this._element = backdrop
+ }
+
+ return this._element
+ }
+
+ _getConfig(config) {
+ config = {
+ ...Default,
+ ...(typeof config === 'object' ? config : {})
+ }
+ typeCheckConfig(NAME, config, DefaultType)
+ return config
+ }
+
+ _append() {
+ if (this._isAppended) {
+ return
+ }
+
+ this._config.rootElement.appendChild(this._getElement())
+
+ EventHandler.on(this._getElement(), EVENT_MOUSEDOWN, () => {
+ execute(this._config.clickCallback)
+ })
+
+ this._isAppended = true
+ }
+
+ dispose() {
+ if (!this._isAppended) {
+ return
+ }
+
+ EventHandler.off(this._element, EVENT_MOUSEDOWN)
+
+ this._getElement().parentNode.removeChild(this._element)
+ this._isAppended = false
+ }
+
+ _emulateAnimation(callback) {
+ if (!this._config.isAnimated) {
+ execute(callback)
+ return
+ }
+
+ const backdropTransitionDuration = getTransitionDurationFromElement(this._getElement())
+ EventHandler.one(this._getElement(), 'transitionend', () => execute(callback))
+ emulateTransitionEnd(this._getElement(), backdropTransitionDuration)
+ }
+}
+
+export default Backdrop
diff --git a/js/src/util/index.js b/js/src/util/index.js
index 96cadc65b..c27c470e9 100644
--- a/js/src/util/index.js
+++ b/js/src/util/index.js
@@ -1,6 +1,6 @@
/**
* --------------------------------------------------------------------------
- * Bootstrap (v5.0.0-alpha3): util/index.js
+ * Bootstrap (v5.0.0-beta3): util/index.js
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
* --------------------------------------------------------------------------
*/
@@ -36,7 +36,20 @@ const getSelector = element => {
let selector = element.getAttribute('data-bs-target')
if (!selector || selector === '#') {
- const hrefAttr = element.getAttribute('href')
+ let hrefAttr = element.getAttribute('href')
+
+ // The only valid content that could double as a selector are IDs or classes,
+ // so everything starting with `#` or `.`. If a "real" URL is used as the selector,
+ // `document.querySelector` will rightfully complain it is invalid.
+ // See https://github.com/twbs/bootstrap/issues/32273
+ if (!hrefAttr || (!hrefAttr.includes('#') && !hrefAttr.startsWith('.'))) {
+ return null
+ }
+
+ // Just in case some CMS puts out a full URL with the anchor appended
+ if (hrefAttr.includes('#') && !hrefAttr.startsWith('#')) {
+ hrefAttr = `#${hrefAttr.split('#')[1]}`
+ }
selector = hrefAttr && hrefAttr !== '#' ? hrefAttr.trim() : null
}
@@ -111,15 +124,12 @@ const typeCheckConfig = (componentName, config, configTypes) => {
Object.keys(configTypes).forEach(property => {
const expectedTypes = configTypes[property]
const value = config[property]
- const valueType = value && isElement(value) ?
- 'element' :
- toType(value)
+ const valueType = value && isElement(value) ? 'element' : toType(value)
if (!new RegExp(expectedTypes).test(valueType)) {
- throw new Error(
- `${componentName.toUpperCase()}: ` +
- `Option "${property}" provided type "${valueType}" ` +
- `but expected type "${expectedTypes}".`)
+ throw new TypeError(
+ `${componentName.toUpperCase()}: Option "${property}" provided type "${valueType}" but expected type "${expectedTypes}".`
+ )
}
})
}
@@ -141,6 +151,22 @@ const isVisible = element => {
return false
}
+const isDisabled = element => {
+ if (!element || element.nodeType !== Node.ELEMENT_NODE) {
+ return true
+ }
+
+ if (element.classList.contains('disabled')) {
+ return true
+ }
+
+ if (typeof element.disabled !== 'undefined') {
+ return element.disabled
+ }
+
+ return element.hasAttribute('disabled') && element.getAttribute('disabled') !== 'false'
+}
+
const findShadowRoot = element => {
if (!document.documentElement.attachShadow) {
return null
@@ -164,7 +190,7 @@ const findShadowRoot = element => {
return findShadowRoot(element.parentNode)
}
-const noop = () => function () {}
+const noop = () => {}
const reflow = element => element.offsetHeight
@@ -186,10 +212,31 @@ const onDOMContentLoaded = callback => {
}
}
-const isRTL = document.documentElement.dir === 'rtl'
+const isRTL = () => document.documentElement.dir === 'rtl'
+
+const defineJQueryPlugin = (name, plugin) => {
+ onDOMContentLoaded(() => {
+ const $ = getjQuery()
+ /* istanbul ignore if */
+ if ($) {
+ const JQUERY_NO_CONFLICT = $.fn[name]
+ $.fn[name] = plugin.jQueryInterface
+ $.fn[name].Constructor = plugin
+ $.fn[name].noConflict = () => {
+ $.fn[name] = JQUERY_NO_CONFLICT
+ return plugin.jQueryInterface
+ }
+ }
+ })
+}
+
+const execute = callback => {
+ if (typeof callback === 'function') {
+ callback()
+ }
+}
export {
- TRANSITION_END,
getUID,
getSelectorFromElement,
getElementFromSelector,
@@ -199,10 +246,13 @@ export {
emulateTransitionEnd,
typeCheckConfig,
isVisible,
+ isDisabled,
findShadowRoot,
noop,
reflow,
getjQuery,
onDOMContentLoaded,
- isRTL
+ isRTL,
+ defineJQueryPlugin,
+ execute
}
diff --git a/js/src/util/sanitizer.js b/js/src/util/sanitizer.js
index 68469285a..232a55e6b 100644
--- a/js/src/util/sanitizer.js
+++ b/js/src/util/sanitizer.js
@@ -1,6 +1,6 @@
/**
* --------------------------------------------------------------------------
- * Bootstrap (v5.0.0-alpha3): util/sanitizer.js
+ * Bootstrap (v5.0.0-beta3): util/sanitizer.js
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
* --------------------------------------------------------------------------
*/
@@ -23,7 +23,7 @@ const ARIA_ATTRIBUTE_PATTERN = /^aria-[\w-]*$/i
*
* Shoutout to Angular 7 https://github.com/angular/angular/blob/7.2.4/packages/core/src/sanitization/url_sanitizer.ts
*/
-const SAFE_URL_PATTERN = /^(?:(?:https?|mailto|ftp|tel|file):|[^#&/:?]*(?:[#/?]|$))/gi
+const SAFE_URL_PATTERN = /^(?:(?:https?|mailto|ftp|tel|file):|[^#&/:?]*(?:[#/?]|$))/i
/**
* A pattern that matches safe data URLs. Only matches image, video and audio types.
@@ -37,7 +37,7 @@ const allowedAttribute = (attr, allowedAttributeList) => {
if (allowedAttributeList.includes(attrName)) {
if (uriAttrs.has(attrName)) {
- return Boolean(attr.nodeValue.match(SAFE_URL_PATTERN) || attr.nodeValue.match(DATA_URL_PATTERN))
+ return Boolean(SAFE_URL_PATTERN.test(attr.nodeValue) || DATA_URL_PATTERN.test(attr.nodeValue))
}
return true
@@ -47,7 +47,7 @@ const allowedAttribute = (attr, allowedAttributeList) => {
// Check if a regular expression validates the attribute.
for (let i = 0, len = regExp.length; i < len; i++) {
- if (attrName.match(regExp[i])) {
+ if (regExp[i].test(attrName)) {
return true
}
}
diff --git a/js/src/util/scrollbar.js b/js/src/util/scrollbar.js
new file mode 100644
index 000000000..352e3e11d
--- /dev/null
+++ b/js/src/util/scrollbar.js
@@ -0,0 +1,81 @@
+/**
+ * --------------------------------------------------------------------------
+ * Bootstrap (v5.0.0-beta3): util/scrollBar.js
+ * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
+ * --------------------------------------------------------------------------
+ */
+
+import SelectorEngine from '../dom/selector-engine'
+import Manipulator from '../dom/manipulator'
+
+const SELECTOR_FIXED_CONTENT = '.fixed-top, .fixed-bottom, .is-fixed, .sticky-top'
+const SELECTOR_STICKY_CONTENT = '.sticky-top'
+
+const getWidth = () => {
+ // https://developer.mozilla.org/en-US/docs/Web/API/Window/innerWidth#usage_notes
+ const documentWidth = document.documentElement.clientWidth
+ return Math.abs(window.innerWidth - documentWidth)
+}
+
+const hide = (width = getWidth()) => {
+ _disableOverFlow()
+ // give padding to element to balances the hidden scrollbar width
+ _setElementAttributes('body', 'paddingRight', calculatedValue => calculatedValue + width)
+ // trick: We adjust positive paddingRight and negative marginRight to sticky-top elements, to keep shown fullwidth
+ _setElementAttributes(SELECTOR_FIXED_CONTENT, 'paddingRight', calculatedValue => calculatedValue + width)
+ _setElementAttributes(SELECTOR_STICKY_CONTENT, 'marginRight', calculatedValue => calculatedValue - width)
+}
+
+const _disableOverFlow = () => {
+ const actualValue = document.body.style.overflow
+ if (actualValue) {
+ Manipulator.setDataAttribute(document.body, 'overflow', actualValue)
+ }
+
+ document.body.style.overflow = 'hidden'
+}
+
+const _setElementAttributes = (selector, styleProp, callback) => {
+ const scrollbarWidth = getWidth()
+ SelectorEngine.find(selector)
+ .forEach(element => {
+ if (element !== document.body && window.innerWidth > element.clientWidth + scrollbarWidth) {
+ return
+ }
+
+ const actualValue = element.style[styleProp]
+ const calculatedValue = window.getComputedStyle(element)[styleProp]
+ Manipulator.setDataAttribute(element, styleProp, actualValue)
+ element.style[styleProp] = `${callback(Number.parseFloat(calculatedValue))}px`
+ })
+}
+
+const reset = () => {
+ _resetElementAttributes('body', 'overflow')
+ _resetElementAttributes('body', 'paddingRight')
+ _resetElementAttributes(SELECTOR_FIXED_CONTENT, 'paddingRight')
+ _resetElementAttributes(SELECTOR_STICKY_CONTENT, 'marginRight')
+}
+
+const _resetElementAttributes = (selector, styleProp) => {
+ SelectorEngine.find(selector).forEach(element => {
+ const value = Manipulator.getDataAttribute(element, styleProp)
+ if (typeof value === 'undefined') {
+ element.style.removeProperty(styleProp)
+ } else {
+ Manipulator.removeDataAttribute(element, styleProp)
+ element.style[styleProp] = value
+ }
+ })
+}
+
+const isBodyOverflowing = () => {
+ return getWidth() > 0
+}
+
+export {
+ getWidth,
+ hide,
+ isBodyOverflowing,
+ reset
+}