aboutsummaryrefslogtreecommitdiff
path: root/js/src
diff options
context:
space:
mode:
Diffstat (limited to 'js/src')
-rw-r--r--js/src/alert.js92
-rw-r--r--js/src/base-component.js43
-rw-r--r--js/src/button.js21
-rw-r--r--js/src/carousel.js216
-rw-r--r--js/src/collapse.js218
-rw-r--r--js/src/dom/data.js82
-rw-r--r--js/src/dom/event-handler.js40
-rw-r--r--js/src/dom/manipulator.js6
-rw-r--r--js/src/dom/selector-engine.js19
-rw-r--r--js/src/dropdown.js300
-rw-r--r--js/src/modal.js350
-rw-r--r--js/src/offcanvas.js272
-rw-r--r--js/src/popover.js63
-rw-r--r--js/src/scrollspy.js70
-rw-r--r--js/src/tab.js42
-rw-r--r--js/src/toast.js115
-rw-r--r--js/src/tooltip.js247
-rw-r--r--js/src/util/backdrop.js130
-rw-r--r--js/src/util/component-functions.js34
-rw-r--r--js/src/util/focustrap.js109
-rw-r--r--js/src/util/index.js163
-rw-r--r--js/src/util/sanitizer.js6
-rw-r--r--js/src/util/scrollbar.js97
23 files changed, 1556 insertions, 1179 deletions
diff --git a/js/src/alert.js b/js/src/alert.js
index 3a018a638..601078fc6 100644
--- a/js/src/alert.js
+++ b/js/src/alert.js
@@ -1,19 +1,14 @@
/**
* --------------------------------------------------------------------------
- * Bootstrap (v5.0.0-beta2): alert.js
+ * Bootstrap (v5.1.0): alert.js
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
* --------------------------------------------------------------------------
*/
-import {
- defineJQueryPlugin,
- emulateTransitionEnd,
- getElementFromSelector,
- getTransitionDurationFromElement
-} from './util/index'
-import Data from './dom/data'
+import { defineJQueryPlugin } from './util/index'
import EventHandler from './dom/event-handler'
import BaseComponent from './base-component'
+import { enableDismissTrigger } from './util/component-functions'
/**
* ------------------------------------------------------------------------
@@ -24,15 +19,9 @@ import BaseComponent from './base-component'
const NAME = 'alert'
const DATA_KEY = 'bs.alert'
const EVENT_KEY = `.${DATA_KEY}`
-const DATA_API_KEY = '.data-api'
-
-const SELECTOR_DISMISS = '[data-bs-dismiss="alert"]'
const EVENT_CLOSE = `close${EVENT_KEY}`
const EVENT_CLOSED = `closed${EVENT_KEY}`
-const EVENT_CLICK_DATA_API = `click${EVENT_KEY}${DATA_API_KEY}`
-
-const CLASS_NAME_ALERT = 'alert'
const CLASS_NAME_FADE = 'fade'
const CLASS_NAME_SHOW = 'show'
@@ -45,79 +34,48 @@ const CLASS_NAME_SHOW = 'show'
class Alert extends BaseComponent {
// Getters
- static get DATA_KEY() {
- return DATA_KEY
+ static get NAME() {
+ return NAME
}
// Public
- close(element) {
- const rootElement = element ? this._getRootElement(element) : this._element
- const customEvent = this._triggerCloseEvent(rootElement)
+ close() {
+ const closeEvent = EventHandler.trigger(this._element, EVENT_CLOSE)
- if (customEvent === null || customEvent.defaultPrevented) {
+ if (closeEvent.defaultPrevented) {
return
}
- this._removeElement(rootElement)
- }
-
- // Private
-
- _getRootElement(element) {
- return getElementFromSelector(element) || element.closest(`.${CLASS_NAME_ALERT}`)
- }
+ this._element.classList.remove(CLASS_NAME_SHOW)
- _triggerCloseEvent(element) {
- return EventHandler.trigger(element, EVENT_CLOSE)
+ const isAnimated = this._element.classList.contains(CLASS_NAME_FADE)
+ this._queueCallback(() => this._destroyElement(), this._element, isAnimated)
}
- _removeElement(element) {
- element.classList.remove(CLASS_NAME_SHOW)
-
- if (!element.classList.contains(CLASS_NAME_FADE)) {
- this._destroyElement(element)
- return
- }
-
- const transitionDuration = getTransitionDurationFromElement(element)
-
- EventHandler.one(element, 'transitionend', () => this._destroyElement(element))
- emulateTransitionEnd(element, transitionDuration)
- }
-
- _destroyElement(element) {
- if (element.parentNode) {
- element.parentNode.removeChild(element)
- }
-
- EventHandler.trigger(element, EVENT_CLOSED)
+ // Private
+ _destroyElement() {
+ this._element.remove()
+ EventHandler.trigger(this._element, EVENT_CLOSED)
+ this.dispose()
}
// Static
static jQueryInterface(config) {
return this.each(function () {
- let data = Data.getData(this, DATA_KEY)
+ const data = Alert.getOrCreateInstance(this)
- if (!data) {
- data = new Alert(this)
+ if (typeof config !== 'string') {
+ return
}
- if (config === 'close') {
- data[config](this)
- }
- })
- }
-
- static handleDismiss(alertInstance) {
- return function (event) {
- if (event) {
- event.preventDefault()
+ if (data[config] === undefined || config.startsWith('_') || config === 'constructor') {
+ throw new TypeError(`No method named "${config}"`)
}
- alertInstance.close(this)
- }
+ data[config](this)
+ })
}
}
@@ -126,8 +84,8 @@ class Alert extends BaseComponent {
* Data Api implementation
* ------------------------------------------------------------------------
*/
-EventHandler.on(document, EVENT_CLICK_DATA_API, SELECTOR_DISMISS, Alert.handleDismiss(new Alert()))
+enableDismissTrigger(Alert, 'close')
/**
* ------------------------------------------------------------------------
* jQuery
@@ -135,6 +93,6 @@ EventHandler.on(document, EVENT_CLICK_DATA_API, SELECTOR_DISMISS, Alert.handleDi
* add .Alert to jQuery only if jQuery is present
*/
-defineJQueryPlugin(NAME, Alert)
+defineJQueryPlugin(Alert)
export default Alert
diff --git a/js/src/base-component.js b/js/src/base-component.js
index 9de274bd0..e7b4112a9 100644
--- a/js/src/base-component.js
+++ b/js/src/base-component.js
@@ -1,11 +1,16 @@
/**
* --------------------------------------------------------------------------
- * Bootstrap (v5.0.0-beta2): base-component.js
+ * Bootstrap (v5.1.0): base-component.js
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
* --------------------------------------------------------------------------
*/
import Data from './dom/data'
+import {
+ executeAfterTransition,
+ getElement
+} from './util/index'
+import EventHandler from './dom/event-handler'
/**
* ------------------------------------------------------------------------
@@ -13,32 +18,58 @@ import Data from './dom/data'
* ------------------------------------------------------------------------
*/
-const VERSION = '5.0.0-beta2'
+const VERSION = '5.1.0'
class BaseComponent {
constructor(element) {
+ element = getElement(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)
- this._element = null
+ Data.remove(this._element, this.constructor.DATA_KEY)
+ EventHandler.off(this._element, this.constructor.EVENT_KEY)
+
+ Object.getOwnPropertyNames(this).forEach(propertyName => {
+ this[propertyName] = null
+ })
+ }
+
+ _queueCallback(callback, element, isAnimated = true) {
+ executeAfterTransition(callback, element, isAnimated)
}
/** Static */
static getInstance(element) {
- return Data.getData(element, this.DATA_KEY)
+ return Data.get(getElement(element), this.DATA_KEY)
+ }
+
+ static getOrCreateInstance(element, config = {}) {
+ return this.getInstance(element) || new this(element, typeof config === 'object' ? config : null)
}
static get VERSION() {
return VERSION
}
+
+ static get NAME() {
+ throw new Error('You have to implement the static method "NAME", for each component!')
+ }
+
+ static get DATA_KEY() {
+ return `bs.${this.NAME}`
+ }
+
+ static get EVENT_KEY() {
+ return `.${this.DATA_KEY}`
+ }
}
export default BaseComponent
diff --git a/js/src/button.js b/js/src/button.js
index 4ec48ca08..a145fd845 100644
--- a/js/src/button.js
+++ b/js/src/button.js
@@ -1,12 +1,11 @@
/**
* --------------------------------------------------------------------------
- * Bootstrap (v5.0.0-beta2): button.js
+ * Bootstrap (v5.1.0): button.js
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
* --------------------------------------------------------------------------
*/
import { defineJQueryPlugin } from './util/index'
-import Data from './dom/data'
import EventHandler from './dom/event-handler'
import BaseComponent from './base-component'
@@ -36,8 +35,8 @@ const EVENT_CLICK_DATA_API = `click${EVENT_KEY}${DATA_API_KEY}`
class Button extends BaseComponent {
// Getters
- static get DATA_KEY() {
- return DATA_KEY
+ static get NAME() {
+ return NAME
}
// Public
@@ -51,11 +50,7 @@ class Button extends BaseComponent {
static jQueryInterface(config) {
return this.each(function () {
- let data = Data.getData(this, DATA_KEY)
-
- if (!data) {
- data = new Button(this)
- }
+ const data = Button.getOrCreateInstance(this)
if (config === 'toggle') {
data[config]()
@@ -74,11 +69,7 @@ EventHandler.on(document, EVENT_CLICK_DATA_API, SELECTOR_DATA_TOGGLE, event => {
event.preventDefault()
const button = event.target.closest(SELECTOR_DATA_TOGGLE)
-
- let data = Data.getData(button, DATA_KEY)
- if (!data) {
- data = new Button(button)
- }
+ const data = Button.getOrCreateInstance(button)
data.toggle()
})
@@ -90,6 +81,6 @@ EventHandler.on(document, EVENT_CLICK_DATA_API, SELECTOR_DATA_TOGGLE, event => {
* add .Button to jQuery only if jQuery is present
*/
-defineJQueryPlugin(NAME, Button)
+defineJQueryPlugin(Button)
export default Button
diff --git a/js/src/carousel.js b/js/src/carousel.js
index 75f8a4da7..b0aed3872 100644
--- a/js/src/carousel.js
+++ b/js/src/carousel.js
@@ -1,22 +1,20 @@
/**
* --------------------------------------------------------------------------
- * Bootstrap (v5.0.0-beta2): carousel.js
+ * Bootstrap (v5.1.0): carousel.js
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
* --------------------------------------------------------------------------
*/
import {
defineJQueryPlugin,
- emulateTransitionEnd,
getElementFromSelector,
- getTransitionDurationFromElement,
- isVisible,
isRTL,
+ isVisible,
+ getNextActiveElement,
reflow,
triggerTransitionEnd,
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'
@@ -56,11 +54,16 @@ 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'
+const KEY_TO_DIRECTION = {
+ [ARROW_LEFT_KEY]: DIRECTION_RIGHT,
+ [ARROW_RIGHT_KEY]: DIRECTION_LEFT
+}
+
const EVENT_SLIDE = `slide${EVENT_KEY}`
const EVENT_SLID = `slid${EVENT_KEY}`
const EVENT_KEYDOWN = `keydown${EVENT_KEY}`
@@ -129,16 +132,14 @@ class Carousel extends BaseComponent {
return Default
}
- static get DATA_KEY() {
- return DATA_KEY
+ static get NAME() {
+ return NAME
}
// Public
next() {
- if (!this._isSliding) {
- this._slide(DIRECTION_NEXT)
- }
+ this._slide(ORDER_NEXT)
}
nextWhenVisible() {
@@ -150,9 +151,7 @@ class Carousel extends BaseComponent {
}
prev() {
- if (!this._isSliding) {
- this._slide(DIRECTION_PREV)
- }
+ this._slide(ORDER_PREV)
}
pause(event) {
@@ -208,25 +207,11 @@ class Carousel extends BaseComponent {
return
}
- const direction = index > activeIndex ?
- DIRECTION_NEXT :
- DIRECTION_PREV
-
- this._slide(direction, this._items[index])
- }
-
- dispose() {
- EventHandler.off(this._element, EVENT_KEY)
-
- this._items = null
- this._config = null
- this._interval = null
- this._isPaused = null
- this._isSliding = null
- this._activeElement = null
- this._indicatorsElement = null
+ const order = index > activeIndex ?
+ ORDER_NEXT :
+ ORDER_PREV
- super.dispose()
+ this._slide(order, this._items[index])
}
// Private
@@ -234,7 +219,8 @@ class Carousel extends BaseComponent {
_getConfig(config) {
config = {
...Default,
- ...config
+ ...Manipulator.getDataAttributes(this._element),
+ ...(typeof config === 'object' ? config : {})
}
typeCheckConfig(NAME, config, DefaultType)
return config
@@ -251,23 +237,11 @@ class Carousel extends BaseComponent {
this.touchDeltaX = 0
- // swipe left
- if (direction > 0) {
- if (isRTL()) {
- this.next()
- } else {
- this.prev()
- }
+ if (!direction) {
+ return
}
- // swipe right
- if (direction < 0) {
- if (isRTL()) {
- this.prev()
- } else {
- this.next()
- }
- }
+ this._slide(direction > 0 ? DIRECTION_RIGHT : DIRECTION_LEFT)
}
_addEventListeners() {
@@ -286,8 +260,13 @@ class Carousel extends BaseComponent {
}
_addTouchEventListeners() {
+ const hasPointerPenTouch = event => {
+ return this._pointerEvent &&
+ (event.pointerType === POINTER_TYPE_PEN || event.pointerType === POINTER_TYPE_TOUCH)
+ }
+
const start = event => {
- if (this._pointerEvent && (event.pointerType === POINTER_TYPE_PEN || event.pointerType === POINTER_TYPE_TOUCH)) {
+ if (hasPointerPenTouch(event)) {
this.touchStartX = event.clientX
} else if (!this._pointerEvent) {
this.touchStartX = event.touches[0].clientX
@@ -296,15 +275,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 && (event.pointerType === POINTER_TYPE_PEN || event.pointerType === POINTER_TYPE_TOUCH)) {
+ if (hasPointerPenTouch(event)) {
this.touchDeltaX = event.clientX - this.touchStartX
}
@@ -348,20 +325,10 @@ class Carousel extends BaseComponent {
return
}
- if (event.key === ARROW_LEFT_KEY) {
+ const direction = KEY_TO_DIRECTION[event.key]
+ if (direction) {
event.preventDefault()
- if (isRTL()) {
- this.next()
- } else {
- this.prev()
- }
- } else if (event.key === ARROW_RIGHT_KEY) {
- event.preventDefault()
- if (isRTL()) {
- this.prev()
- } else {
- this.next()
- }
+ this._slide(direction)
}
}
@@ -373,24 +340,9 @@ class Carousel extends BaseComponent {
return this._items.indexOf(element)
}
- _getItemByDirection(direction, activeElement) {
- const isNextDirection = direction === DIRECTION_NEXT
- const isPrevDirection = direction === DIRECTION_PREV
- const activeIndex = this._getItemIndex(activeElement)
- const lastItemIndex = this._items.length - 1
- const isGoingToWrap = (isPrevDirection && activeIndex === 0) ||
- (isNextDirection && activeIndex === lastItemIndex)
-
- if (isGoingToWrap && !this._config.wrap) {
- return activeElement
- }
-
- const delta = direction === DIRECTION_PREV ? -1 : 1
- const itemIndex = (activeIndex + delta) % this._items.length
-
- return itemIndex === -1 ?
- this._items[this._items.length - 1] :
- this._items[itemIndex]
+ _getItemByOrder(order, activeElement) {
+ const isNext = order === ORDER_NEXT
+ return getNextActiveElement(this._items, activeElement, isNext, this._config.wrap)
}
_triggerSlideEvent(relatedTarget, eventDirectionName) {
@@ -441,23 +393,29 @@ 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)
- const directionalClassName = direction === DIRECTION_NEXT ? CLASS_NAME_START : CLASS_NAME_END
- const orderClassName = direction === DIRECTION_NEXT ? CLASS_NAME_NEXT : CLASS_NAME_PREV
- const eventDirectionName = direction === DIRECTION_NEXT ? DIRECTION_LEFT : 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
return
}
+ if (this._isSliding) {
+ return
+ }
+
const slideEvent = this._triggerSlideEvent(nextElement, eventDirectionName)
if (slideEvent.defaultPrevented) {
return
@@ -477,6 +435,15 @@ class Carousel extends BaseComponent {
this._setActiveIndicatorElement(nextElement)
this._activeElement = nextElement
+ const triggerSlidEvent = () => {
+ EventHandler.trigger(this._element, EVENT_SLID, {
+ relatedTarget: nextElement,
+ direction: eventDirectionName,
+ from: activeElementIndex,
+ to: nextElementIndex
+ })
+ }
+
if (this._element.classList.contains(CLASS_NAME_SLIDE)) {
nextElement.classList.add(orderClassName)
@@ -485,9 +452,7 @@ class Carousel extends BaseComponent {
activeElement.classList.add(directionalClassName)
nextElement.classList.add(directionalClassName)
- const transitionDuration = getTransitionDurationFromElement(activeElement)
-
- EventHandler.one(activeElement, 'transitionend', () => {
+ const completeCallBack = () => {
nextElement.classList.remove(directionalClassName, orderClassName)
nextElement.classList.add(CLASS_NAME_ACTIVE)
@@ -495,28 +460,16 @@ class Carousel extends BaseComponent {
this._isSliding = false
- setTimeout(() => {
- EventHandler.trigger(this._element, EVENT_SLID, {
- relatedTarget: nextElement,
- direction: eventDirectionName,
- from: activeElementIndex,
- to: nextElementIndex
- })
- }, 0)
- })
+ setTimeout(triggerSlidEvent, 0)
+ }
- emulateTransitionEnd(activeElement, transitionDuration)
+ this._queueCallback(completeCallBack, activeElement, true)
} else {
activeElement.classList.remove(CLASS_NAME_ACTIVE)
nextElement.classList.add(CLASS_NAME_ACTIVE)
this._isSliding = false
- EventHandler.trigger(this._element, EVENT_SLID, {
- relatedTarget: nextElement,
- direction: eventDirectionName,
- from: activeElementIndex,
- to: nextElementIndex
- })
+ triggerSlidEvent()
}
if (isCycling) {
@@ -524,15 +477,36 @@ 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 _config = {
- ...Default,
- ...Manipulator.getDataAttributes(element)
- }
+ const data = Carousel.getOrCreateInstance(element, config)
+ let { _config } = data
if (typeof config === 'object') {
_config = {
..._config,
@@ -542,10 +516,6 @@ class Carousel extends BaseComponent {
const action = typeof config === 'string' ? config : _config.slide
- if (!data) {
- data = new Carousel(element, _config)
- }
-
if (typeof config === 'number') {
data.to(config)
} else if (typeof action === 'string') {
@@ -586,7 +556,7 @@ class Carousel extends BaseComponent {
Carousel.carouselInterface(target, config)
if (slideIndex) {
- Data.getData(target, DATA_KEY).to(slideIndex)
+ Carousel.getInstance(target).to(slideIndex)
}
event.preventDefault()
@@ -605,7 +575,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], Carousel.getInstance(carousels[i]))
}
})
@@ -616,6 +586,6 @@ EventHandler.on(window, EVENT_LOAD_DATA_API, () => {
* add .Carousel to jQuery only if jQuery is present
*/
-defineJQueryPlugin(NAME, Carousel)
+defineJQueryPlugin(Carousel)
export default Carousel
diff --git a/js/src/collapse.js b/js/src/collapse.js
index 0a1b47547..f39c55b92 100644
--- a/js/src/collapse.js
+++ b/js/src/collapse.js
@@ -1,17 +1,15 @@
/**
* --------------------------------------------------------------------------
- * Bootstrap (v5.0.0-beta2): collapse.js
+ * Bootstrap (v5.1.0): collapse.js
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
* --------------------------------------------------------------------------
*/
import {
defineJQueryPlugin,
- emulateTransitionEnd,
+ getElement,
getSelectorFromElement,
getElementFromSelector,
- getTransitionDurationFromElement,
- isElement,
reflow,
typeCheckConfig
} from './util/index'
@@ -34,12 +32,12 @@ const DATA_API_KEY = '.data-api'
const Default = {
toggle: true,
- parent: ''
+ parent: null
}
const DefaultType = {
toggle: 'boolean',
- parent: '(string|element)'
+ parent: '(null|element)'
}
const EVENT_SHOW = `show${EVENT_KEY}`
@@ -52,6 +50,7 @@ const CLASS_NAME_SHOW = 'show'
const CLASS_NAME_COLLAPSE = 'collapse'
const CLASS_NAME_COLLAPSING = 'collapsing'
const CLASS_NAME_COLLAPSED = 'collapsed'
+const CLASS_NAME_HORIZONTAL = 'collapse-horizontal'
const WIDTH = 'width'
const HEIGHT = 'height'
@@ -71,10 +70,7 @@ 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}"]`
- )
+ this._triggerArray = []
const toggleList = SelectorEngine.find(SELECTOR_DATA_TOGGLE)
@@ -82,7 +78,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
@@ -90,10 +86,10 @@ class Collapse extends BaseComponent {
}
}
- this._parent = this._config.parent ? this._getParent() : null
+ this._initializeChildren()
if (!this._config.parent) {
- this._addAriaAndCollapsedClass(this._element, this._triggerArray)
+ this._addAriaAndCollapsedClass(this._triggerArray, this._isShown())
}
if (this._config.toggle) {
@@ -107,14 +103,14 @@ class Collapse extends BaseComponent {
return Default
}
- static get DATA_KEY() {
- return DATA_KEY
+ static get NAME() {
+ return NAME
}
// Public
toggle() {
- if (this._element.classList.contains(CLASS_NAME_SHOW)) {
+ if (this._isShown()) {
this.hide()
} else {
this.show()
@@ -122,32 +118,22 @@ class Collapse extends BaseComponent {
}
show() {
- if (this._isTransitioning || this._element.classList.contains(CLASS_NAME_SHOW)) {
+ if (this._isTransitioning || this._isShown()) {
return
}
- let actives
+ let actives = []
let activesData
- if (this._parent) {
- actives = SelectorEngine.find(SELECTOR_ACTIVES, this._parent)
- .filter(elem => {
- if (typeof this._config.parent === 'string') {
- return elem.getAttribute('data-bs-parent') === this._config.parent
- }
-
- return elem.classList.contains(CLASS_NAME_COLLAPSE)
- })
-
- if (actives.length === 0) {
- actives = null
- }
+ if (this._config.parent) {
+ const children = SelectorEngine.find(`.${CLASS_NAME_COLLAPSE} .${CLASS_NAME_COLLAPSE}`, this._config.parent)
+ actives = SelectorEngine.find(SELECTOR_ACTIVES, this._config.parent).filter(elem => !children.includes(elem)) // remove children if greater depth
}
const container = SelectorEngine.findOne(this._selector)
- if (actives) {
+ if (actives.length) {
const tempActiveData = actives.find(elem => container !== elem)
- activesData = tempActiveData ? Data.getData(tempActiveData, DATA_KEY) : null
+ activesData = tempActiveData ? Collapse.getInstance(tempActiveData) : null
if (activesData && activesData._isTransitioning) {
return
@@ -159,17 +145,15 @@ class Collapse extends BaseComponent {
return
}
- if (actives) {
- actives.forEach(elemActive => {
- if (container !== elemActive) {
- Collapse.collapseInterface(elemActive, 'hide')
- }
+ actives.forEach(elemActive => {
+ if (container !== elemActive) {
+ Collapse.getOrCreateInstance(elemActive, { toggle: false }).hide()
+ }
- if (!activesData) {
- Data.setData(elemActive, DATA_KEY, null)
- }
- })
- }
+ if (!activesData) {
+ Data.set(elemActive, DATA_KEY, null)
+ }
+ })
const dimension = this._getDimension()
@@ -178,38 +162,29 @@ class Collapse extends BaseComponent {
this._element.style[dimension] = 0
- if (this._triggerArray.length) {
- this._triggerArray.forEach(element => {
- element.classList.remove(CLASS_NAME_COLLAPSED)
- element.setAttribute('aria-expanded', true)
- })
- }
-
- this.setTransitioning(true)
+ this._addAriaAndCollapsedClass(this._triggerArray, true)
+ this._isTransitioning = true
const complete = () => {
+ this._isTransitioning = false
+
this._element.classList.remove(CLASS_NAME_COLLAPSING)
this._element.classList.add(CLASS_NAME_COLLAPSE, CLASS_NAME_SHOW)
this._element.style[dimension] = ''
- this.setTransitioning(false)
-
EventHandler.trigger(this._element, EVENT_SHOWN)
}
const capitalizedDimension = dimension[0].toUpperCase() + dimension.slice(1)
const scrollSize = `scroll${capitalizedDimension}`
- const transitionDuration = getTransitionDurationFromElement(this._element)
- EventHandler.one(this._element, 'transitionend', complete)
-
- emulateTransitionEnd(this._element, transitionDuration)
+ this._queueCallback(complete, this._element, true)
this._element.style[dimension] = `${this._element[scrollSize]}px`
}
hide() {
- if (this._isTransitioning || !this._element.classList.contains(CLASS_NAME_SHOW)) {
+ if (this._isTransitioning || !this._isShown()) {
return
}
@@ -228,44 +203,31 @@ class Collapse extends BaseComponent {
this._element.classList.remove(CLASS_NAME_COLLAPSE, CLASS_NAME_SHOW)
const triggerArrayLength = this._triggerArray.length
- if (triggerArrayLength > 0) {
- for (let i = 0; i < triggerArrayLength; i++) {
- const trigger = this._triggerArray[i]
- const elem = getElementFromSelector(trigger)
-
- if (elem && !elem.classList.contains(CLASS_NAME_SHOW)) {
- trigger.classList.add(CLASS_NAME_COLLAPSED)
- trigger.setAttribute('aria-expanded', false)
- }
+ for (let i = 0; i < triggerArrayLength; i++) {
+ const trigger = this._triggerArray[i]
+ const elem = getElementFromSelector(trigger)
+
+ if (elem && !this._isShown(elem)) {
+ this._addAriaAndCollapsedClass([trigger], false)
}
}
- this.setTransitioning(true)
+ this._isTransitioning = true
const complete = () => {
- this.setTransitioning(false)
+ this._isTransitioning = false
this._element.classList.remove(CLASS_NAME_COLLAPSING)
this._element.classList.add(CLASS_NAME_COLLAPSE)
EventHandler.trigger(this._element, EVENT_HIDDEN)
}
this._element.style[dimension] = ''
- const transitionDuration = getTransitionDurationFromElement(this._element)
- EventHandler.one(this._element, 'transitionend', complete)
- emulateTransitionEnd(this._element, transitionDuration)
+ this._queueCallback(complete, this._element, true)
}
- setTransitioning(isTransitioning) {
- this._isTransitioning = isTransitioning
- }
-
- dispose() {
- super.dispose()
- this._config = null
- this._parent = null
- this._triggerArray = null
- this._isTransitioning = null
+ _isShown(element = this._element) {
+ return element.classList.contains(CLASS_NAME_SHOW)
}
// Private
@@ -273,51 +235,40 @@ class Collapse extends BaseComponent {
_getConfig(config) {
config = {
...Default,
+ ...Manipulator.getDataAttributes(this._element),
...config
}
config.toggle = Boolean(config.toggle) // Coerce string values
+ config.parent = getElement(config.parent)
typeCheckConfig(NAME, config, DefaultType)
return config
}
_getDimension() {
- return this._element.classList.contains(WIDTH) ? WIDTH : HEIGHT
+ return this._element.classList.contains(CLASS_NAME_HORIZONTAL) ? WIDTH : HEIGHT
}
- _getParent() {
- let { parent } = this._config
-
- if (isElement(parent)) {
- // it's a jQuery object
- if (typeof parent.jquery !== 'undefined' || typeof parent[0] !== 'undefined') {
- parent = parent[0]
- }
- } else {
- parent = SelectorEngine.findOne(parent)
+ _initializeChildren() {
+ if (!this._config.parent) {
+ return
}
- const selector = `${SELECTOR_DATA_TOGGLE}[data-bs-parent="${parent}"]`
-
- SelectorEngine.find(selector, parent)
+ const children = SelectorEngine.find(`.${CLASS_NAME_COLLAPSE} .${CLASS_NAME_COLLAPSE}`, this._config.parent)
+ SelectorEngine.find(SELECTOR_DATA_TOGGLE, this._config.parent).filter(elem => !children.includes(elem))
.forEach(element => {
const selected = getElementFromSelector(element)
- this._addAriaAndCollapsedClass(
- selected,
- [element]
- )
+ if (selected) {
+ this._addAriaAndCollapsedClass([element], this._isShown(selected))
+ }
})
-
- return parent
}
- _addAriaAndCollapsedClass(element, triggerArray) {
- if (!element || !triggerArray.length) {
+ _addAriaAndCollapsedClass(triggerArray, isOpen) {
+ if (!triggerArray.length) {
return
}
- const isOpen = element.classList.contains(CLASS_NAME_SHOW)
-
triggerArray.forEach(elem => {
if (isOpen) {
elem.classList.remove(CLASS_NAME_COLLAPSED)
@@ -331,34 +282,22 @@ class Collapse extends BaseComponent {
// Static
- static collapseInterface(element, config) {
- let data = Data.getData(element, DATA_KEY)
- const _config = {
- ...Default,
- ...Manipulator.getDataAttributes(element),
- ...(typeof config === 'object' && config ? config : {})
- }
+ static jQueryInterface(config) {
+ return this.each(function () {
+ const _config = {}
+ if (typeof config === 'string' && /show|hide/.test(config)) {
+ _config.toggle = false
+ }
- if (!data && _config.toggle && typeof config === 'string' && /show|hide/.test(config)) {
- _config.toggle = false
- }
+ const data = Collapse.getOrCreateInstance(this, _config)
- if (!data) {
- data = new Collapse(element, _config)
- }
+ if (typeof config === 'string') {
+ if (typeof data[config] === 'undefined') {
+ throw new TypeError(`No method named "${config}"`)
+ }
- if (typeof config === 'string') {
- if (typeof data[config] === 'undefined') {
- throw new TypeError(`No method named "${config}"`)
+ data[config]()
}
-
- data[config]()
- }
- }
-
- static jQueryInterface(config) {
- return this.each(function () {
- Collapse.collapseInterface(this, config)
})
}
}
@@ -375,26 +314,11 @@ EventHandler.on(document, EVENT_CLICK_DATA_API, SELECTOR_DATA_TOGGLE, function (
event.preventDefault()
}
- const triggerData = Manipulator.getDataAttributes(this)
const selector = getSelectorFromElement(this)
const selectorElements = SelectorEngine.find(selector)
selectorElements.forEach(element => {
- const data = Data.getData(element, DATA_KEY)
- let config
- if (data) {
- // update parent attribute
- if (data._parent === null && typeof triggerData.parent === 'string') {
- data._config.parent = triggerData.parent
- data._parent = data._getParent()
- }
-
- config = 'toggle'
- } else {
- config = triggerData
- }
-
- Collapse.collapseInterface(element, config)
+ Collapse.getOrCreateInstance(element, { toggle: false }).toggle()
})
})
@@ -405,6 +329,6 @@ EventHandler.on(document, EVENT_CLICK_DATA_API, SELECTOR_DATA_TOGGLE, function (
* add .Collapse to jQuery only if jQuery is present
*/
-defineJQueryPlugin(NAME, Collapse)
+defineJQueryPlugin(Collapse)
export default Collapse
diff --git a/js/src/dom/data.js b/js/src/dom/data.js
index c93a8dc7c..69488b510 100644
--- a/js/src/dom/data.js
+++ b/js/src/dom/data.js
@@ -1,6 +1,6 @@
/**
* --------------------------------------------------------------------------
- * Bootstrap (v5.0.0-beta2): dom/data.js
+ * Bootstrap (v5.1.0): 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 5b11ae3d0..087afde07 100644
--- a/js/src/dom/event-handler.js
+++ b/js/src/dom/event-handler.js
@@ -1,6 +1,6 @@
/**
* --------------------------------------------------------------------------
- * Bootstrap (v5.0.0-beta2): dom/event-handler.js
+ * Bootstrap (v5.1.0): 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',
@@ -113,7 +114,7 @@ function bootstrapDelegationHandler(element, selector, fn) {
if (handler.oneOff) {
// eslint-disable-next-line unicorn/consistent-destructuring
- EventHandler.off(element, event.type, fn)
+ EventHandler.off(element, event.type, selector, fn)
}
return fn.apply(target, [event])
@@ -144,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) {
@@ -171,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] = {})
@@ -219,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)
@@ -272,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 509797bc0..fdeb69ed8 100644
--- a/js/src/dom/manipulator.js
+++ b/js/src/dom/manipulator.js
@@ -1,6 +1,6 @@
/**
* --------------------------------------------------------------------------
- * Bootstrap (v5.0.0-beta2): dom/manipulator.js
+ * Bootstrap (v5.1.0): dom/manipulator.js
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
* --------------------------------------------------------------------------
*/
@@ -64,8 +64,8 @@ const Manipulator = {
const rect = element.getBoundingClientRect()
return {
- top: rect.top + document.body.scrollTop,
- left: rect.left + document.body.scrollLeft
+ top: rect.top + window.pageYOffset,
+ left: rect.left + window.pageXOffset
}
},
diff --git a/js/src/dom/selector-engine.js b/js/src/dom/selector-engine.js
index b310098b5..e3988a986 100644
--- a/js/src/dom/selector-engine.js
+++ b/js/src/dom/selector-engine.js
@@ -1,6 +1,6 @@
/**
* --------------------------------------------------------------------------
- * Bootstrap (v5.0.0-beta2): dom/selector-engine.js
+ * Bootstrap (v5.1.0): dom/selector-engine.js
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
* --------------------------------------------------------------------------
*/
@@ -11,6 +11,8 @@
* ------------------------------------------------------------------------
*/
+import { isDisabled, isVisible } from '../util/index'
+
const NODE_TEXT = 3
const SelectorEngine = {
@@ -69,6 +71,21 @@ const SelectorEngine = {
}
return []
+ },
+
+ focusableChildren(element) {
+ const focusables = [
+ 'a',
+ 'button',
+ 'input',
+ 'textarea',
+ 'select',
+ 'details',
+ '[tabindex]',
+ '[contenteditable="true"]'
+ ].map(selector => `${selector}:not([tabindex^="-"])`).join(', ')
+
+ return this.find(focusables, element).filter(el => !isDisabled(el) && isVisible(el))
}
}
diff --git a/js/src/dropdown.js b/js/src/dropdown.js
index 590c74801..d1f573fc8 100644
--- a/js/src/dropdown.js
+++ b/js/src/dropdown.js
@@ -1,6 +1,6 @@
/**
* --------------------------------------------------------------------------
- * Bootstrap (v5.0.0-beta2): dropdown.js
+ * Bootstrap (v5.1.0): dropdown.js
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
* --------------------------------------------------------------------------
*/
@@ -9,14 +9,16 @@ import * as Popper from '@popperjs/core'
import {
defineJQueryPlugin,
+ getElement,
getElementFromSelector,
+ getNextActiveElement,
+ isDisabled,
isElement,
- isVisible,
isRTL,
+ isVisible,
noop,
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'
@@ -46,12 +48,10 @@ const EVENT_HIDE = `hide${EVENT_KEY}`
const EVENT_HIDDEN = `hidden${EVENT_KEY}`
const EVENT_SHOW = `show${EVENT_KEY}`
const EVENT_SHOWN = `shown${EVENT_KEY}`
-const EVENT_CLICK = `click${EVENT_KEY}`
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'
@@ -59,7 +59,6 @@ const CLASS_NAME_DROPSTART = 'dropstart'
const CLASS_NAME_NAVBAR = 'navbar'
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)'
@@ -73,20 +72,20 @@ const PLACEMENT_LEFT = isRTL() ? 'right-start' : 'left-start'
const Default = {
offset: [0, 2],
- flip: true,
boundary: 'clippingParents',
reference: 'toggle',
display: 'dynamic',
- popperConfig: null
+ popperConfig: null,
+ autoClose: true
}
const DefaultType = {
offset: '(array|string|function)',
- flip: 'boolean',
boundary: '(string|element)',
reference: '(string|element|object)',
display: 'string',
- popperConfig: '(null|object|function)'
+ popperConfig: '(null|object|function)',
+ autoClose: '(boolean|string)'
}
/**
@@ -103,8 +102,6 @@ class Dropdown extends BaseComponent {
this._config = this._getConfig(config)
this._menu = this._getMenuElement()
this._inNavbar = this._detectNavbar()
-
- this._addEventListeners()
}
// Getters
@@ -117,34 +114,21 @@ class Dropdown extends BaseComponent {
return DefaultType
}
- static get DATA_KEY() {
- return DATA_KEY
+ static get NAME() {
+ return NAME
}
// Public
toggle() {
- if (this._element.disabled || this._element.classList.contains(CLASS_NAME_DISABLED)) {
- return
- }
-
- const isActive = this._element.classList.contains(CLASS_NAME_SHOW)
-
- Dropdown.clearMenus()
-
- if (isActive) {
- return
- }
-
- this.show()
+ return this._isShown() ? this.hide() : this.show()
}
show() {
- if (this._element.disabled || this._element.classList.contains(CLASS_NAME_DISABLED) || this._menu.classList.contains(CLASS_NAME_SHOW)) {
+ if (isDisabled(this._element) || this._isShown(this._menu)) {
return
}
- const parent = Dropdown.getParentFromElement(this._element)
const relatedTarget = {
relatedTarget: this._element
}
@@ -155,37 +139,12 @@ class Dropdown extends BaseComponent {
return
}
+ const parent = Dropdown.getParentFromElement(this._element)
// Totally disable Popper for Dropdowns in Navbar
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)')
- }
-
- let referenceElement = this._element
-
- if (this._config.reference === 'parent') {
- referenceElement = parent
- } else if (isElement(this._config.reference)) {
- referenceElement = this._config.reference
-
- // Check if it's jQuery element
- if (typeof this._config.reference.jquery !== 'undefined') {
- referenceElement = this._config.reference[0]
- }
- } else if (typeof this._config.reference === 'object') {
- referenceElement = this._config.reference
- }
-
- const popperConfig = this._getPopperConfig()
- const isDisplayStatic = popperConfig.modifiers.find(modifier => modifier.name === 'applyStyles' && modifier.enabled === false)
-
- this._popper = Popper.createPopper(referenceElement, this._menu, popperConfig)
-
- if (isDisplayStatic) {
- Manipulator.setDataAttribute(this._menu, 'popper', 'static')
- }
+ this._createPopper(parent)
}
// If this is a touch-enabled device we add extra
@@ -195,19 +154,19 @@ 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()
this._element.setAttribute('aria-expanded', true)
- this._menu.classList.toggle(CLASS_NAME_SHOW)
- this._element.classList.toggle(CLASS_NAME_SHOW)
+ this._menu.classList.add(CLASS_NAME_SHOW)
+ this._element.classList.add(CLASS_NAME_SHOW)
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._isShown(this._menu)) {
return
}
@@ -215,29 +174,12 @@ class Dropdown extends BaseComponent {
relatedTarget: this._element
}
- const hideEvent = EventHandler.trigger(this._element, 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)
- Manipulator.removeDataAttribute(this._menu, 'popper')
- EventHandler.trigger(this._element, EVENT_HIDDEN, relatedTarget)
+ this._completeHide(relatedTarget)
}
dispose() {
- EventHandler.off(this._element, EVENT_KEY)
- this._menu = null
-
if (this._popper) {
this._popper.destroy()
- this._popper = null
}
super.dispose()
@@ -252,12 +194,28 @@ class Dropdown extends BaseComponent {
// Private
- _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) {
@@ -279,6 +237,35 @@ class Dropdown extends BaseComponent {
return config
}
+ _createPopper(parent) {
+ if (typeof Popper === 'undefined') {
+ throw new TypeError('Bootstrap\'s dropdowns require Popper (https://popper.js.org)')
+ }
+
+ let referenceElement = this._element
+
+ if (this._config.reference === 'parent') {
+ referenceElement = parent
+ } else if (isElement(this._config.reference)) {
+ referenceElement = getElement(this._config.reference)
+ } else if (typeof this._config.reference === 'object') {
+ referenceElement = this._config.reference
+ }
+
+ const popperConfig = this._getPopperConfig()
+ const isDisplayStatic = popperConfig.modifiers.find(modifier => modifier.name === 'applyStyles' && modifier.enabled === false)
+
+ this._popper = Popper.createPopper(referenceElement, this._menu, popperConfig)
+
+ if (isDisplayStatic) {
+ Manipulator.setDataAttribute(this._menu, 'popper', 'static')
+ }
+ }
+
+ _isShown(element = this._element) {
+ return element.classList.contains(CLASS_NAME_SHOW)
+ }
+
_getMenuElement() {
return SelectorEngine.next(this._element, SELECTOR_MENU)[0]
}
@@ -328,7 +315,6 @@ class Dropdown extends BaseComponent {
modifiers: [{
name: 'preventOverflow',
options: {
- altBoundary: this._config.flip,
boundary: this._config.boundary
}
},
@@ -354,28 +340,33 @@ class Dropdown extends BaseComponent {
}
}
+ _selectMenuItem({ key, target }) {
+ const items = SelectorEngine.find(SELECTOR_VISIBLE_ITEMS, this._menu).filter(isVisible)
+
+ if (!items.length) {
+ return
+ }
+
+ // if target isn't included in items (e.g. when expanding the dropdown)
+ // allow cycling to get the last item in case key equals ARROW_UP_KEY
+ getNextActiveElement(items, target, key === ARROW_DOWN_KEY, !items.includes(target)).focus()
+ }
+
// Static
- static dropdownInterface(element, config) {
- let data = Data.getData(element, DATA_KEY)
- const _config = typeof config === 'object' ? config : null
+ static jQueryInterface(config) {
+ return this.each(function () {
+ const data = Dropdown.getOrCreateInstance(this, config)
- if (!data) {
- data = new Dropdown(element, _config)
- }
+ if (typeof config !== 'string') {
+ return
+ }
- if (typeof config === 'string') {
if (typeof data[config] === 'undefined') {
throw new TypeError(`No method named "${config}"`)
}
data[config]()
- }
- }
-
- static jQueryInterface(config) {
- return this.each(function () {
- Dropdown.dropdownInterface(this, config)
})
}
@@ -387,53 +378,41 @@ class Dropdown extends BaseComponent {
const toggles = SelectorEngine.find(SELECTOR_DATA_TOGGLE)
for (let i = 0, len = toggles.length; i < len; i++) {
- const context = Data.getData(toggles[i], DATA_KEY)
- const relatedTarget = {
- relatedTarget: toggles[i]
- }
-
- if (event && event.type === 'click') {
- relatedTarget.clickEvent = event
- }
-
- if (!context) {
- continue
- }
-
- const dropdownMenu = context._menu
- if (!toggles[i].classList.contains(CLASS_NAME_SHOW)) {
+ const context = Dropdown.getInstance(toggles[i])
+ 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._isShown()) {
continue
}
- const hideEvent = EventHandler.trigger(toggles[i], 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 or events from contained inputs shouldn't close the menu
+ if (context._menu.contains(event.target) && ((event.type === 'keyup' && event.key === TAB_KEY) || /input|select|option|textarea|form/i.test(event.target.tagName))) {
+ 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)
- Manipulator.removeDataAttribute(dropdownMenu, 'popper')
- EventHandler.trigger(toggles[i], EVENT_HIDDEN, relatedTarget)
+ context._completeHide(relatedTarget)
}
}
@@ -457,56 +436,39 @@ class Dropdown extends BaseComponent {
return
}
- event.preventDefault()
- event.stopPropagation()
-
- if (this.disabled || this.classList.contains(CLASS_NAME_DISABLED)) {
- return
- }
-
- const parent = Dropdown.getParentFromElement(this)
const isActive = this.classList.contains(CLASS_NAME_SHOW)
- if (event.key === ESCAPE_KEY) {
- const button = this.matches(SELECTOR_DATA_TOGGLE) ? this : SelectorEngine.prev(this, SELECTOR_DATA_TOGGLE)[0]
- button.focus()
- Dropdown.clearMenus()
+ if (!isActive && event.key === ESCAPE_KEY) {
return
}
- if (!isActive && (event.key === ARROW_UP_KEY || event.key === ARROW_DOWN_KEY)) {
- const button = this.matches(SELECTOR_DATA_TOGGLE) ? this : SelectorEngine.prev(this, SELECTOR_DATA_TOGGLE)[0]
- button.click()
- return
- }
+ event.preventDefault()
+ event.stopPropagation()
- if (!isActive || event.key === SPACE_KEY) {
- Dropdown.clearMenus()
+ if (isDisabled(this)) {
return
}
- const items = SelectorEngine.find(SELECTOR_VISIBLE_ITEMS, parent).filter(isVisible)
+ const getToggleButton = this.matches(SELECTOR_DATA_TOGGLE) ? this : SelectorEngine.prev(this, SELECTOR_DATA_TOGGLE)[0]
+ const instance = Dropdown.getOrCreateInstance(getToggleButton)
- if (!items.length) {
+ if (event.key === ESCAPE_KEY) {
+ instance.hide()
return
}
- let index = items.indexOf(event.target)
+ if (event.key === ARROW_UP_KEY || event.key === ARROW_DOWN_KEY) {
+ if (!isActive) {
+ instance.show()
+ }
- // Up
- if (event.key === ARROW_UP_KEY && index > 0) {
- index--
+ instance._selectMenuItem(event)
+ return
}
- // Down
- if (event.key === ARROW_DOWN_KEY && index < items.length - 1) {
- index++
+ if (!isActive || event.key === SPACE_KEY) {
+ Dropdown.clearMenus()
}
-
- // index is -1 if the first keydown is an ArrowUp
- index = index === -1 ? 0 : index
-
- items[index].focus()
}
}
@@ -522,10 +484,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.getOrCreateInstance(this).toggle()
})
-EventHandler.on(document, EVENT_CLICK_DATA_API, SELECTOR_FORM_CHILD, e => e.stopPropagation())
/**
* ------------------------------------------------------------------------
@@ -534,6 +494,6 @@ EventHandler.on(document, EVENT_CLICK_DATA_API, SELECTOR_FORM_CHILD, e => e.stop
* add .Dropdown to jQuery only if jQuery is present
*/
-defineJQueryPlugin(NAME, Dropdown)
+defineJQueryPlugin(Dropdown)
export default Dropdown
diff --git a/js/src/modal.js b/js/src/modal.js
index 79a2f143a..7d44c31e8 100644
--- a/js/src/modal.js
+++ b/js/src/modal.js
@@ -1,25 +1,26 @@
/**
* --------------------------------------------------------------------------
- * Bootstrap (v5.0.0-beta2): modal.js
+ * Bootstrap (v5.1.0): modal.js
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
* --------------------------------------------------------------------------
*/
import {
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 ScrollBarHelper from './util/scrollbar'
import BaseComponent from './base-component'
+import Backdrop from './util/backdrop'
+import FocusTrap from './util/focustrap'
+import { enableDismissTrigger } from './util/component-functions'
/**
* ------------------------------------------------------------------------
@@ -50,7 +51,6 @@ const EVENT_HIDE_PREVENTED = `hidePrevented${EVENT_KEY}`
const EVENT_HIDDEN = `hidden${EVENT_KEY}`
const EVENT_SHOW = `show${EVENT_KEY}`
const EVENT_SHOWN = `shown${EVENT_KEY}`
-const EVENT_FOCUSIN = `focusin${EVENT_KEY}`
const EVENT_RESIZE = `resize${EVENT_KEY}`
const EVENT_CLICK_DISMISS = `click.dismiss${EVENT_KEY}`
const EVENT_KEYDOWN_DISMISS = `keydown.dismiss${EVENT_KEY}`
@@ -58,19 +58,15 @@ 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'
const CLASS_NAME_STATIC = 'modal-static'
+const OPEN_SELECTOR = '.modal.show'
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'
/**
* ------------------------------------------------------------------------
@@ -83,13 +79,13 @@ 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._focustrap = this._initializeFocusTrap()
this._isShown = false
- this._isBodyOverflowing = false
this._ignoreBackdropClick = false
this._isTransitioning = false
- this._scrollbarWidth = 0
+ this._scrollBar = new ScrollBarHelper()
}
// Getters
@@ -98,8 +94,8 @@ class Modal extends BaseComponent {
return Default
}
- static get DATA_KEY() {
- return DATA_KEY
+ static get NAME() {
+ return NAME
}
// Public
@@ -113,30 +109,29 @@ class Modal extends BaseComponent {
return
}
- if (this._element.classList.contains(CLASS_NAME_FADE)) {
- this._isTransitioning = true
- }
-
const showEvent = EventHandler.trigger(this._element, EVENT_SHOW, {
relatedTarget
})
- if (this._isShown || showEvent.defaultPrevented) {
+ if (showEvent.defaultPrevented) {
return
}
this._isShown = true
- this._checkScrollbar()
- this._setScrollbar()
+ if (this._isAnimated()) {
+ this._isTransitioning = true
+ }
+
+ this._scrollBar.hide()
+
+ document.body.classList.add(CLASS_NAME_OPEN)
this._adjustDialog()
this._setEscapeEvent()
this._setResizeEvent()
- EventHandler.on(this._element, EVENT_CLICK_DISMISS, SELECTOR_DATA_DISMISS, event => this.hide(event))
-
EventHandler.on(this._dialog, EVENT_MOUSEDOWN_DISMISS, () => {
EventHandler.one(this._element, EVENT_MOUSEUP_DISMISS, event => {
if (event.target === this._element) {
@@ -148,11 +143,7 @@ class Modal extends BaseComponent {
this._showBackdrop(() => this._showElement(relatedTarget))
}
- hide(event) {
- if (event) {
- event.preventDefault()
- }
-
+ hide() {
if (!this._isShown || this._isTransitioning) {
return
}
@@ -164,53 +155,32 @@ 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
}
this._setEscapeEvent()
this._setResizeEvent()
- EventHandler.off(document, EVENT_FOCUSIN)
+ this._focustrap.deactivate()
this._element.classList.remove(CLASS_NAME_SHOW)
EventHandler.off(this._element, EVENT_CLICK_DISMISS)
EventHandler.off(this._dialog, EVENT_MOUSEDOWN_DISMISS)
- if (transition) {
- const transitionDuration = getTransitionDurationFromElement(this._element)
-
- EventHandler.one(this._element, 'transitionend', event => this._hideModal(event))
- emulateTransitionEnd(this._element, transitionDuration)
- } else {
- this._hideModal()
- }
+ this._queueCallback(() => this._hideModal(), this._element, isAnimated)
}
dispose() {
- [window, this._element, this._dialog]
+ [window, this._dialog]
.forEach(htmlElement => EventHandler.off(htmlElement, EVENT_KEY))
+ this._backdrop.dispose()
+ this._focustrap.deactivate()
super.dispose()
-
- /**
- * `document` has 2 events `EVENT_FOCUSIN` and `EVENT_CLICK_DATA_API`
- * Do not move `document` in `htmlElements` array
- * It will remove `EVENT_CLICK_DATA_API` event that should remain
- */
- EventHandler.off(document, EVENT_FOCUSIN)
-
- this._config = null
- this._dialog = null
- this._backdrop = null
- this._isShown = null
- this._isBodyOverflowing = null
- this._ignoreBackdropClick = null
- this._isTransitioning = null
- this._scrollbarWidth = null
}
handleUpdate() {
@@ -219,22 +189,36 @@ 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()
+ })
+ }
+
+ _initializeFocusTrap() {
+ return new FocusTrap({
+ trapElement: this._element
+ })
+ }
+
_getConfig(config) {
config = {
...Default,
- ...config
+ ...Manipulator.getDataAttributes(this._element),
+ ...(typeof config === 'object' ? config : {})
}
typeCheckConfig(NAME, config, DefaultType)
return config
}
_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) {
// Don't move modal's DOM position
- document.body.appendChild(this._element)
+ document.body.append(this._element)
}
this._element.style.display = 'block'
@@ -247,19 +231,15 @@ class Modal extends BaseComponent {
modalBody.scrollTop = 0
}
- if (transition) {
+ if (isAnimated) {
reflow(this._element)
}
this._element.classList.add(CLASS_NAME_SHOW)
- if (this._config.focus) {
- this._enforceFocus()
- }
-
const transitionComplete = () => {
if (this._config.focus) {
- this._element.focus()
+ this._focustrap.activate()
}
this._isTransitioning = false
@@ -268,25 +248,7 @@ class Modal extends BaseComponent {
})
}
- if (transition) {
- const transitionDuration = getTransitionDurationFromElement(this._dialog)
-
- EventHandler.one(this._dialog, 'transitionend', transitionComplete)
- emulateTransitionEnd(this._dialog, transitionDuration)
- } else {
- transitionComplete()
- }
- }
-
- _enforceFocus() {
- EventHandler.off(document, EVENT_FOCUSIN) // guard against infinite focus loop
- EventHandler.on(document, EVENT_FOCUSIN, event => {
- if (document !== event.target &&
- this._element !== event.target &&
- !this._element.contains(event.target)) {
- this._element.focus()
- }
- })
+ this._queueCallback(transitionComplete, this._dialog, isAnimated)
}
_setEscapeEvent() {
@@ -318,84 +280,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()
+ this._scrollBar.reset()
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, 'transitionend', 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, 'transitionend', callbackRemove)
- emulateTransitionEnd(this._backdrop, backdropTransitionDuration)
- } else {
- callbackRemove()
- }
- } else {
- callback()
- }
+ this._backdrop.show(callback)
+ }
+
+ _isAnimated() {
+ return this._element.classList.contains(CLASS_NAME_FADE)
}
_triggerBackdropTransition() {
@@ -404,25 +319,28 @@ class Modal extends BaseComponent {
return
}
- const isModalOverflowing = this._element.scrollHeight > document.documentElement.clientHeight
+ const { classList, scrollHeight, style } = this._element
+ const isModalOverflowing = scrollHeight > document.documentElement.clientHeight
+
+ // return if the following background transition hasn't yet completed
+ if ((!isModalOverflowing && style.overflowY === 'hidden') || classList.contains(CLASS_NAME_STATIC)) {
+ return
+ }
if (!isModalOverflowing) {
- this._element.style.overflowY = 'hidden'
+ style.overflowY = 'hidden'
}
- this._element.classList.add(CLASS_NAME_STATIC)
- const modalTransitionDuration = getTransitionDurationFromElement(this._dialog)
- EventHandler.off(this._element, 'transitionend')
- EventHandler.one(this._element, 'transitionend', () => {
- this._element.classList.remove(CLASS_NAME_STATIC)
+ classList.add(CLASS_NAME_STATIC)
+ this._queueCallback(() => {
+ classList.remove(CLASS_NAME_STATIC)
if (!isModalOverflowing) {
- EventHandler.one(this._element, 'transitionend', () => {
- this._element.style.overflowY = ''
- })
- emulateTransitionEnd(this._element, modalTransitionDuration)
+ this._queueCallback(() => {
+ style.overflowY = ''
+ }, this._dialog)
}
- })
- emulateTransitionEnd(this._element, modalTransitionDuration)
+ }, this._dialog)
+
this._element.focus()
}
@@ -432,13 +350,15 @@ class Modal extends BaseComponent {
_adjustDialog() {
const isModalOverflowing = this._element.scrollHeight > document.documentElement.clientHeight
+ const scrollbarWidth = this._scrollBar.getWidth()
+ 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`
}
}
@@ -447,81 +367,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) {
- this._setElementAttributes(SELECTOR_FIXED_CONTENT, 'paddingRight', calculatedValue => calculatedValue + this._scrollbarWidth)
- this._setElementAttributes(SELECTOR_STICKY_CONTENT, 'marginRight', calculatedValue => calculatedValue - this._scrollbarWidth)
- this._setElementAttributes('body', 'paddingRight', calculatedValue => calculatedValue + this._scrollbarWidth)
- }
-
- document.body.classList.add(CLASS_NAME_OPEN)
- }
-
- _setElementAttributes(selector, styleProp, callback) {
- SelectorEngine.find(selector)
- .forEach(element => {
- const actualValue = element.style[styleProp]
- const calculatedValue = window.getComputedStyle(element)[styleProp]
- Manipulator.setDataAttribute(element, styleProp, actualValue)
- element.style[styleProp] = callback(Number.parseFloat(calculatedValue)) + 'px'
- })
- }
-
- _resetScrollbar() {
- this._resetElementAttributes(SELECTOR_FIXED_CONTENT, 'paddingRight')
- this._resetElementAttributes(SELECTOR_STICKY_CONTENT, 'marginRight')
- this._resetElementAttributes('body', 'paddingRight')
- }
-
- _resetElementAttributes(selector, styleProp) {
- SelectorEngine.find(selector).forEach(element => {
- const value = Manipulator.getDataAttribute(element, styleProp)
- if (typeof value === 'undefined' && element === document.body) {
- element.style[styleProp] = ''
- } else {
- Manipulator.removeDataAttribute(element, styleProp)
- element.style[styleProp] = value
- }
- })
- }
-
- _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.getOrCreateInstance(this, 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)
})
}
}
@@ -535,7 +395,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()
}
@@ -552,19 +412,19 @@ 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)
+ // avoid conflict when clicking moddal toggler while another one is open
+ const allReadyOpen = SelectorEngine.findOne(OPEN_SELECTOR)
+ if (allReadyOpen) {
+ Modal.getInstance(allReadyOpen).hide()
}
+ const data = Modal.getOrCreateInstance(target)
+
data.toggle(this)
})
+enableDismissTrigger(Modal)
+
/**
* ------------------------------------------------------------------------
* jQuery
@@ -572,6 +432,6 @@ EventHandler.on(document, EVENT_CLICK_DATA_API, SELECTOR_DATA_TOGGLE, function (
* add .Modal to jQuery only if jQuery is present
*/
-defineJQueryPlugin(NAME, Modal)
+defineJQueryPlugin(Modal)
export default Modal
diff --git a/js/src/offcanvas.js b/js/src/offcanvas.js
new file mode 100644
index 000000000..60ce8a6c9
--- /dev/null
+++ b/js/src/offcanvas.js
@@ -0,0 +1,272 @@
+/**
+ * --------------------------------------------------------------------------
+ * Bootstrap (v5.1.0): offcanvas.js
+ * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
+ * --------------------------------------------------------------------------
+ */
+
+import {
+ defineJQueryPlugin,
+ getElementFromSelector,
+ isDisabled,
+ isVisible,
+ typeCheckConfig
+} from './util/index'
+import ScrollBarHelper from './util/scrollbar'
+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'
+import FocusTrap from './util/focustrap'
+import { enableDismissTrigger } from './util/component-functions'
+
+/**
+ * ------------------------------------------------------------------------
+ * 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 CLASS_NAME_BACKDROP = 'offcanvas-backdrop'
+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_CLICK_DATA_API = `click${EVENT_KEY}${DATA_API_KEY}`
+const EVENT_KEYDOWN_DISMISS = `keydown.dismiss${EVENT_KEY}`
+
+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._focustrap = this._initializeFocusTrap()
+ this._addEventListeners()
+ }
+
+ // Getters
+
+ static get NAME() {
+ return NAME
+ }
+
+ static get Default() {
+ return Default
+ }
+
+ // 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) {
+ new ScrollBarHelper().hide()
+ }
+
+ 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 = () => {
+ if (!this._config.scroll) {
+ this._focustrap.activate()
+ }
+
+ EventHandler.trigger(this._element, EVENT_SHOWN, { relatedTarget })
+ }
+
+ this._queueCallback(completeCallBack, this._element, true)
+ }
+
+ hide() {
+ if (!this._isShown) {
+ return
+ }
+
+ const hideEvent = EventHandler.trigger(this._element, EVENT_HIDE)
+
+ if (hideEvent.defaultPrevented) {
+ return
+ }
+
+ this._focustrap.deactivate()
+ 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) {
+ new ScrollBarHelper().reset()
+ }
+
+ EventHandler.trigger(this._element, EVENT_HIDDEN)
+ }
+
+ this._queueCallback(completeCallback, this._element, true)
+ }
+
+ dispose() {
+ this._backdrop.dispose()
+ this._focustrap.deactivate()
+ super.dispose()
+ }
+
+ // Private
+
+ _getConfig(config) {
+ config = {
+ ...Default,
+ ...Manipulator.getDataAttributes(this._element),
+ ...(typeof config === 'object' ? config : {})
+ }
+ typeCheckConfig(NAME, config, DefaultType)
+ return config
+ }
+
+ _initializeBackDrop() {
+ return new Backdrop({
+ className: CLASS_NAME_BACKDROP,
+ isVisible: this._config.backdrop,
+ isAnimated: true,
+ rootElement: this._element.parentNode,
+ clickCallback: () => this.hide()
+ })
+ }
+
+ _initializeFocusTrap() {
+ return new FocusTrap({
+ trapElement: this._element
+ })
+ }
+
+ _addEventListeners() {
+ 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 = Offcanvas.getOrCreateInstance(this, 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 = Offcanvas.getOrCreateInstance(target)
+ data.toggle(this)
+})
+
+EventHandler.on(window, EVENT_LOAD_DATA_API, () =>
+ SelectorEngine.find(OPEN_SELECTOR).forEach(el => Offcanvas.getOrCreateInstance(el).show())
+)
+
+enableDismissTrigger(Offcanvas)
+/**
+ * ------------------------------------------------------------------------
+ * jQuery
+ * ------------------------------------------------------------------------
+ */
+
+defineJQueryPlugin(Offcanvas)
+
+export default Offcanvas
diff --git a/js/src/popover.js b/js/src/popover.js
index 0677dafa0..a01e2d294 100644
--- a/js/src/popover.js
+++ b/js/src/popover.js
@@ -1,13 +1,11 @@
/**
* --------------------------------------------------------------------------
- * Bootstrap (v5.0.0-beta2): popover.js
+ * Bootstrap (v5.1.0): popover.js
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
* --------------------------------------------------------------------------
*/
import { defineJQueryPlugin } from './util/index'
-import Data from './dom/data'
-import SelectorEngine from './dom/selector-engine'
import Tooltip from './tooltip'
/**
@@ -20,7 +18,6 @@ const NAME = 'popover'
const DATA_KEY = 'bs.popover'
const EVENT_KEY = `.${DATA_KEY}`
const CLASS_PREFIX = 'bs-popover'
-const BSCLS_PREFIX_REGEX = new RegExp(`(^|\\s)${CLASS_PREFIX}\\S+`, 'g')
const Default = {
...Tooltip.Default,
@@ -30,7 +27,7 @@ const Default = {
content: '',
template: '<div class="popover" role="tooltip">' +
'<div class="popover-arrow"></div>' +
- '<h3 class="popover-header"></h3>' +
+ '<h3 class="popover-header"></h3>' +
'<div class="popover-body"></div>' +
'</div>'
}
@@ -53,9 +50,6 @@ const Event = {
MOUSELEAVE: `mouseleave${EVENT_KEY}`
}
-const CLASS_NAME_FADE = 'fade'
-const CLASS_NAME_SHOW = 'show'
-
const SELECTOR_TITLE = '.popover-header'
const SELECTOR_CONTENT = '.popover-body'
@@ -76,18 +70,10 @@ class Popover extends Tooltip {
return NAME
}
- static get DATA_KEY() {
- return DATA_KEY
- }
-
static get Event() {
return Event
}
- static get EVENT_KEY() {
- return EVENT_KEY
- }
-
static get DefaultType() {
return DefaultType
}
@@ -98,55 +84,26 @@ class Popover extends Tooltip {
return this.getTitle() || this._getContent()
}
- setContent() {
- const tip = this.getTipElement()
-
- // we use append for html objects to maintain js events
- this.setElementContent(SelectorEngine.findOne(SELECTOR_TITLE, tip), this.getTitle())
- let content = this._getContent()
- if (typeof content === 'function') {
- content = content.call(this._element)
- }
-
- this.setElementContent(SelectorEngine.findOne(SELECTOR_CONTENT, tip), content)
-
- tip.classList.remove(CLASS_NAME_FADE, CLASS_NAME_SHOW)
+ setContent(tip) {
+ this._sanitizeAndSetContent(tip, this.getTitle(), SELECTOR_TITLE)
+ this._sanitizeAndSetContent(tip, this._getContent(), SELECTOR_CONTENT)
}
// Private
- _addAttachmentClass(attachment) {
- this.getTipElement().classList.add(`${CLASS_PREFIX}-${this.updateAttachment(attachment)}`)
- }
-
_getContent() {
- return this._element.getAttribute('data-bs-content') || this.config.content
+ return this._resolvePossibleFunction(this._config.content)
}
- _cleanTipClass() {
- const tip = this.getTipElement()
- const tabClass = tip.getAttribute('class').match(BSCLS_PREFIX_REGEX)
- if (tabClass !== null && tabClass.length > 0) {
- tabClass.map(token => token.trim())
- .forEach(tClass => tip.classList.remove(tClass))
- }
+ _getBasicClassPrefix() {
+ return CLASS_PREFIX
}
// Static
static jQueryInterface(config) {
return this.each(function () {
- let data = Data.getData(this, DATA_KEY)
- const _config = typeof config === 'object' ? config : null
-
- if (!data && /dispose|hide/.test(config)) {
- return
- }
-
- if (!data) {
- data = new Popover(this, _config)
- Data.setData(this, DATA_KEY, data)
- }
+ const data = Popover.getOrCreateInstance(this, config)
if (typeof config === 'string') {
if (typeof data[config] === 'undefined') {
@@ -166,6 +123,6 @@ class Popover extends Tooltip {
* add .Popover to jQuery only if jQuery is present
*/
-defineJQueryPlugin(NAME, Popover)
+defineJQueryPlugin(Popover)
export default Popover
diff --git a/js/src/scrollspy.js b/js/src/scrollspy.js
index 43a91e5e9..ad170fcbd 100644
--- a/js/src/scrollspy.js
+++ b/js/src/scrollspy.js
@@ -1,18 +1,16 @@
/**
* --------------------------------------------------------------------------
- * Bootstrap (v5.0.0-beta2): scrollspy.js
+ * Bootstrap (v5.1.0): scrollspy.js
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
* --------------------------------------------------------------------------
*/
import {
defineJQueryPlugin,
+ getElement,
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'
@@ -53,6 +51,7 @@ const SELECTOR_NAV_LIST_GROUP = '.nav, .list-group'
const SELECTOR_NAV_LINKS = '.nav-link'
const SELECTOR_NAV_ITEMS = '.nav-item'
const SELECTOR_LIST_ITEMS = '.list-group-item'
+const SELECTOR_LINK_ITEMS = `${SELECTOR_NAV_LINKS}, ${SELECTOR_LIST_ITEMS}, .${CLASS_NAME_DROPDOWN_ITEM}`
const SELECTOR_DROPDOWN = '.dropdown'
const SELECTOR_DROPDOWN_TOGGLE = '.dropdown-toggle'
@@ -68,9 +67,8 @@ 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 = []
this._targets = []
this._activeTarget = null
@@ -88,8 +86,8 @@ class ScrollSpy extends BaseComponent {
return Default
}
- static get DATA_KEY() {
- return DATA_KEY
+ static get NAME() {
+ return NAME
}
// Public
@@ -111,7 +109,7 @@ class ScrollSpy extends BaseComponent {
this._targets = []
this._scrollHeight = this._getScrollHeight()
- const targets = SelectorEngine.find(this._selector)
+ const targets = SelectorEngine.find(SELECTOR_LINK_ITEMS, this._config.target)
targets.map(element => {
const targetSelector = getSelectorFromElement(element)
@@ -138,16 +136,8 @@ class ScrollSpy extends BaseComponent {
}
dispose() {
- super.dispose()
EventHandler.off(this._scrollElement, EVENT_KEY)
-
- this._scrollElement = null
- this._config = null
- this._selector = null
- this._offsets = null
- this._targets = null
- this._activeTarget = null
- this._scrollHeight = null
+ super.dispose()
}
// Private
@@ -155,18 +145,11 @@ class ScrollSpy extends BaseComponent {
_getConfig(config) {
config = {
...Default,
+ ...Manipulator.getDataAttributes(this._element),
...(typeof config === 'object' && config ? config : {})
}
- if (typeof config.target !== 'string' && isElement(config.target)) {
- let { id } = config.target
- if (!id) {
- id = getUID(NAME)
- config.target.id = id
- }
-
- config.target = `#${id}`
- }
+ config.target = getElement(config.target) || document.documentElement
typeCheckConfig(NAME, config, DefaultType)
@@ -233,20 +216,16 @@ class ScrollSpy extends BaseComponent {
this._clear()
- const queries = this._selector.split(',')
+ const queries = SELECTOR_LINK_ITEMS.split(',')
.map(selector => `${selector}[data-bs-target="${target}"],${selector}[href="${target}"]`)
- const link = SelectorEngine.findOne(queries.join(','))
+ const link = SelectorEngine.findOne(queries.join(','), this._config.target)
+ link.classList.add(CLASS_NAME_ACTIVE)
if (link.classList.contains(CLASS_NAME_DROPDOWN_ITEM)) {
SelectorEngine.findOne(SELECTOR_DROPDOWN_TOGGLE, link.closest(SELECTOR_DROPDOWN))
.classList.add(CLASS_NAME_ACTIVE)
-
- link.classList.add(CLASS_NAME_ACTIVE)
} else {
- // Set triggered link as active
- link.classList.add(CLASS_NAME_ACTIVE)
-
SelectorEngine.parents(link, SELECTOR_NAV_LIST_GROUP)
.forEach(listGroup => {
// Set triggered links parents as active
@@ -269,7 +248,7 @@ class ScrollSpy extends BaseComponent {
}
_clear() {
- SelectorEngine.find(this._selector)
+ SelectorEngine.find(SELECTOR_LINK_ITEMS, this._config.target)
.filter(node => node.classList.contains(CLASS_NAME_ACTIVE))
.forEach(node => node.classList.remove(CLASS_NAME_ACTIVE))
}
@@ -278,20 +257,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.getOrCreateInstance(this, 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]()
})
}
}
@@ -304,7 +280,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))
})
/**
@@ -314,6 +290,6 @@ EventHandler.on(window, EVENT_LOAD_DATA_API, () => {
* add .ScrollSpy to jQuery only if jQuery is present
*/
-defineJQueryPlugin(NAME, ScrollSpy)
+defineJQueryPlugin(ScrollSpy)
export default ScrollSpy
diff --git a/js/src/tab.js b/js/src/tab.js
index e60ecddb5..e50f41456 100644
--- a/js/src/tab.js
+++ b/js/src/tab.js
@@ -1,18 +1,16 @@
/**
* --------------------------------------------------------------------------
- * Bootstrap (v5.0.0-beta2): tab.js
+ * Bootstrap (v5.1.0): tab.js
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
* --------------------------------------------------------------------------
*/
import {
defineJQueryPlugin,
- emulateTransitionEnd,
getElementFromSelector,
- getTransitionDurationFromElement,
+ isDisabled,
reflow
} from './util/index'
-import Data from './dom/data'
import EventHandler from './dom/event-handler'
import SelectorEngine from './dom/selector-engine'
import BaseComponent from './base-component'
@@ -36,7 +34,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'
@@ -57,8 +54,8 @@ const SELECTOR_DROPDOWN_ACTIVE_CHILD = ':scope > .dropdown-menu .active'
class Tab extends BaseComponent {
// Getters
- static get DATA_KEY() {
- return DATA_KEY
+ static get NAME() {
+ return NAME
}
// Public
@@ -66,8 +63,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
}
@@ -126,11 +122,8 @@ class Tab extends BaseComponent {
const complete = () => this._transitionComplete(element, active, callback)
if (active && isTransitioning) {
- const transitionDuration = getTransitionDurationFromElement(active)
active.classList.remove(CLASS_NAME_SHOW)
-
- EventHandler.one(active, 'transitionend', complete)
- emulateTransitionEnd(active, transitionDuration)
+ this._queueCallback(complete, element, true)
} else {
complete()
}
@@ -162,11 +155,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))
}
@@ -182,7 +180,7 @@ class Tab extends BaseComponent {
static jQueryInterface(config) {
return this.each(function () {
- const data = Data.getData(this, DATA_KEY) || new Tab(this)
+ const data = Tab.getOrCreateInstance(this)
if (typeof config === 'string') {
if (typeof data[config] === 'undefined') {
@@ -202,9 +200,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()
+ }
+
+ if (isDisabled(this)) {
+ return
+ }
- const data = Data.getData(this, DATA_KEY) || new Tab(this)
+ const data = Tab.getOrCreateInstance(this)
data.show()
})
@@ -215,6 +219,6 @@ EventHandler.on(document, EVENT_CLICK_DATA_API, SELECTOR_DATA_TOGGLE, function (
* add .Tab to jQuery only if jQuery is present
*/
-defineJQueryPlugin(NAME, Tab)
+defineJQueryPlugin(Tab)
export default Tab
diff --git a/js/src/toast.js b/js/src/toast.js
index 2f451aab7..442200738 100644
--- a/js/src/toast.js
+++ b/js/src/toast.js
@@ -1,21 +1,19 @@
/**
* --------------------------------------------------------------------------
- * Bootstrap (v5.0.0-beta2): toast.js
+ * Bootstrap (v5.1.0): toast.js
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
* --------------------------------------------------------------------------
*/
import {
defineJQueryPlugin,
- emulateTransitionEnd,
- getTransitionDurationFromElement,
reflow,
typeCheckConfig
} from './util/index'
-import Data from './dom/data'
import EventHandler from './dom/event-handler'
import Manipulator from './dom/manipulator'
import BaseComponent from './base-component'
+import { enableDismissTrigger } from './util/component-functions'
/**
* ------------------------------------------------------------------------
@@ -27,14 +25,17 @@ const NAME = 'toast'
const DATA_KEY = 'bs.toast'
const EVENT_KEY = `.${DATA_KEY}`
-const EVENT_CLICK_DISMISS = `click.dismiss${EVENT_KEY}`
+const EVENT_MOUSEOVER = `mouseover${EVENT_KEY}`
+const EVENT_MOUSEOUT = `mouseout${EVENT_KEY}`
+const EVENT_FOCUSIN = `focusin${EVENT_KEY}`
+const EVENT_FOCUSOUT = `focusout${EVENT_KEY}`
const EVENT_HIDE = `hide${EVENT_KEY}`
const EVENT_HIDDEN = `hidden${EVENT_KEY}`
const EVENT_SHOW = `show${EVENT_KEY}`
const EVENT_SHOWN = `shown${EVENT_KEY}`
const CLASS_NAME_FADE = 'fade'
-const CLASS_NAME_HIDE = 'hide'
+const CLASS_NAME_HIDE = 'hide' // @deprecated - kept here only for backwards compatibility
const CLASS_NAME_SHOW = 'show'
const CLASS_NAME_SHOWING = 'showing'
@@ -50,8 +51,6 @@ const Default = {
delay: 5000
}
-const SELECTOR_DATA_DISMISS = '[data-bs-dismiss="toast"]'
-
/**
* ------------------------------------------------------------------------
* Class Definition
@@ -64,6 +63,8 @@ class Toast extends BaseComponent {
this._config = this._getConfig(config)
this._timeout = null
+ this._hasMouseInteraction = false
+ this._hasKeyboardInteraction = false
this._setListeners()
}
@@ -77,8 +78,8 @@ class Toast extends BaseComponent {
return Default
}
- static get DATA_KEY() {
- return DATA_KEY
+ static get NAME() {
+ return NAME
}
// Public
@@ -98,28 +99,17 @@ class Toast extends BaseComponent {
const complete = () => {
this._element.classList.remove(CLASS_NAME_SHOWING)
- this._element.classList.add(CLASS_NAME_SHOW)
-
EventHandler.trigger(this._element, EVENT_SHOWN)
- if (this._config.autohide) {
- this._timeout = setTimeout(() => {
- this.hide()
- }, this._config.delay)
- }
+ this._maybeScheduleHide()
}
- this._element.classList.remove(CLASS_NAME_HIDE)
+ this._element.classList.remove(CLASS_NAME_HIDE) // @deprecated
reflow(this._element)
+ this._element.classList.add(CLASS_NAME_SHOW)
this._element.classList.add(CLASS_NAME_SHOWING)
- if (this._config.animation) {
- const transitionDuration = getTransitionDurationFromElement(this._element)
- EventHandler.one(this._element, 'transitionend', complete)
- emulateTransitionEnd(this._element, transitionDuration)
- } else {
- complete()
- }
+ this._queueCallback(complete, this._element, this._config.animation)
}
hide() {
@@ -134,19 +124,14 @@ class Toast extends BaseComponent {
}
const complete = () => {
- this._element.classList.add(CLASS_NAME_HIDE)
+ this._element.classList.add(CLASS_NAME_HIDE) // @deprecated
+ this._element.classList.remove(CLASS_NAME_SHOWING)
+ this._element.classList.remove(CLASS_NAME_SHOW)
EventHandler.trigger(this._element, EVENT_HIDDEN)
}
- this._element.classList.remove(CLASS_NAME_SHOW)
- if (this._config.animation) {
- const transitionDuration = getTransitionDurationFromElement(this._element)
-
- EventHandler.one(this._element, 'transitionend', complete)
- emulateTransitionEnd(this._element, transitionDuration)
- } else {
- complete()
- }
+ this._element.classList.add(CLASS_NAME_SHOWING)
+ this._queueCallback(complete, this._element, this._config.animation)
}
dispose() {
@@ -156,10 +141,7 @@ class Toast extends BaseComponent {
this._element.classList.remove(CLASS_NAME_SHOW)
}
- EventHandler.off(this._element, EVENT_CLICK_DISMISS)
-
super.dispose()
- this._config = null
}
// Private
@@ -176,8 +158,52 @@ class Toast extends BaseComponent {
return config
}
+ _maybeScheduleHide() {
+ if (!this._config.autohide) {
+ return
+ }
+
+ if (this._hasMouseInteraction || this._hasKeyboardInteraction) {
+ return
+ }
+
+ this._timeout = setTimeout(() => {
+ this.hide()
+ }, this._config.delay)
+ }
+
+ _onInteraction(event, isInteracting) {
+ switch (event.type) {
+ case 'mouseover':
+ case 'mouseout':
+ this._hasMouseInteraction = isInteracting
+ break
+ case 'focusin':
+ case 'focusout':
+ this._hasKeyboardInteraction = isInteracting
+ break
+ default:
+ break
+ }
+
+ if (isInteracting) {
+ this._clearTimeout()
+ return
+ }
+
+ const nextElement = event.relatedTarget
+ if (this._element === nextElement || this._element.contains(nextElement)) {
+ return
+ }
+
+ this._maybeScheduleHide()
+ }
+
_setListeners() {
- EventHandler.on(this._element, EVENT_CLICK_DISMISS, SELECTOR_DATA_DISMISS, () => this.hide())
+ EventHandler.on(this._element, EVENT_MOUSEOVER, event => this._onInteraction(event, true))
+ EventHandler.on(this._element, EVENT_MOUSEOUT, event => this._onInteraction(event, false))
+ EventHandler.on(this._element, EVENT_FOCUSIN, event => this._onInteraction(event, true))
+ EventHandler.on(this._element, EVENT_FOCUSOUT, event => this._onInteraction(event, false))
}
_clearTimeout() {
@@ -189,12 +215,7 @@ class Toast extends BaseComponent {
static jQueryInterface(config) {
return this.each(function () {
- let data = Data.getData(this, DATA_KEY)
- const _config = typeof config === 'object' && config
-
- if (!data) {
- data = new Toast(this, _config)
- }
+ const data = Toast.getOrCreateInstance(this, config)
if (typeof config === 'string') {
if (typeof data[config] === 'undefined') {
@@ -207,6 +228,8 @@ class Toast extends BaseComponent {
}
}
+enableDismissTrigger(Toast)
+
/**
* ------------------------------------------------------------------------
* jQuery
@@ -214,6 +237,6 @@ class Toast extends BaseComponent {
* add .Toast to jQuery only if jQuery is present
*/
-defineJQueryPlugin(NAME, Toast)
+defineJQueryPlugin(Toast)
export default Toast
diff --git a/js/src/tooltip.js b/js/src/tooltip.js
index d35b5e0ab..288146472 100644
--- a/js/src/tooltip.js
+++ b/js/src/tooltip.js
@@ -1,6 +1,6 @@
/**
* --------------------------------------------------------------------------
- * Bootstrap (v5.0.0-beta2): tooltip.js
+ * Bootstrap (v5.1.0): tooltip.js
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
* --------------------------------------------------------------------------
*/
@@ -9,19 +9,15 @@ import * as Popper from '@popperjs/core'
import {
defineJQueryPlugin,
- emulateTransitionEnd,
findShadowRoot,
- getTransitionDurationFromElement,
+ getElement,
getUID,
isElement,
isRTL,
noop,
typeCheckConfig
} from './util/index'
-import {
- DefaultAllowlist,
- sanitizeHtml
-} from './util/sanitizer'
+import { DefaultAllowlist, sanitizeHtml } from './util/sanitizer'
import Data from './dom/data'
import EventHandler from './dom/event-handler'
import Manipulator from './dom/manipulator'
@@ -38,7 +34,6 @@ const NAME = 'tooltip'
const DATA_KEY = 'bs.tooltip'
const EVENT_KEY = `.${DATA_KEY}`
const CLASS_PREFIX = 'bs-tooltip'
-const BSCLS_PREFIX_REGEX = new RegExp(`(^|\\s)${CLASS_PREFIX}\\S+`, 'g')
const DISALLOWED_ATTRIBUTES = new Set(['sanitize', 'allowList', 'sanitizeFn'])
const DefaultType = {
@@ -113,6 +108,9 @@ const HOVER_STATE_SHOW = 'show'
const HOVER_STATE_OUT = 'out'
const SELECTOR_TOOLTIP_INNER = '.tooltip-inner'
+const SELECTOR_MODAL = `.${CLASS_NAME_MODAL}`
+
+const EVENT_MODAL_HIDE = 'hide.bs.modal'
const TRIGGER_HOVER = 'hover'
const TRIGGER_FOCUS = 'focus'
@@ -141,7 +139,7 @@ class Tooltip extends BaseComponent {
this._popper = null
// Protected
- this.config = this._getConfig(config)
+ this._config = this._getConfig(config)
this.tip = null
this._setListeners()
@@ -157,18 +155,10 @@ class Tooltip extends BaseComponent {
return NAME
}
- static get DATA_KEY() {
- return DATA_KEY
- }
-
static get Event() {
return Event
}
- static get EVENT_KEY() {
- return EVENT_KEY
- }
-
static get DefaultType() {
return DefaultType
}
@@ -215,24 +205,16 @@ 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)
+ EventHandler.off(this._element.closest(SELECTOR_MODAL), EVENT_MODAL_HIDE, this._hideModalHandler)
- if (this.tip && this.tip.parentNode) {
- this.tip.parentNode.removeChild(this.tip)
+ if (this.tip) {
+ this.tip.remove()
}
- this._isEnabled = null
- this._timeout = null
- this._hoverState = null
- this._activeTrigger = null
if (this._popper) {
this._popper.destroy()
}
- this._popper = null
- this.config = null
- this.tip = null
super.dispose()
}
@@ -261,33 +243,34 @@ class Tooltip extends BaseComponent {
tip.setAttribute('id', tipId)
this._element.setAttribute('aria-describedby', tipId)
- this.setContent()
-
- if (this.config.animation) {
+ if (this._config.animation) {
tip.classList.add(CLASS_NAME_FADE)
}
- const placement = typeof this.config.placement === 'function' ?
- this.config.placement.call(this, tip, this._element) :
- this.config.placement
+ const placement = typeof this._config.placement === 'function' ?
+ this._config.placement.call(this, tip, this._element) :
+ this._config.placement
const attachment = this._getAttachment(placement)
this._addAttachmentClass(attachment)
- const container = this._getContainer()
- Data.setData(tip, this.constructor.DATA_KEY, this)
+ const { container } = this._config
+ Data.set(tip, this.constructor.DATA_KEY, this)
if (!this._element.ownerDocument.documentElement.contains(this.tip)) {
- container.appendChild(tip)
+ container.append(tip)
+ EventHandler.trigger(this._element, this.constructor.Event.INSERTED)
}
- EventHandler.trigger(this._element, this.constructor.Event.INSERTED)
-
- this._popper = Popper.createPopper(this._element, tip, this._getPopperConfig(attachment))
+ if (this._popper) {
+ this._popper.update()
+ } else {
+ this._popper = Popper.createPopper(this._element, tip, this._getPopperConfig(attachment))
+ }
tip.classList.add(CLASS_NAME_SHOW)
- const customClass = typeof this.config.customClass === 'function' ? this.config.customClass() : this.config.customClass
+ const customClass = this._resolvePossibleFunction(this._config.customClass)
if (customClass) {
tip.classList.add(...customClass.split(' '))
}
@@ -298,7 +281,7 @@ class Tooltip extends BaseComponent {
// 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())
+ EventHandler.on(element, 'mouseover', noop)
})
}
@@ -313,13 +296,8 @@ class Tooltip extends BaseComponent {
}
}
- 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()
- }
+ const isAnimated = this.tip.classList.contains(CLASS_NAME_FADE)
+ this._queueCallback(complete, this.tip, isAnimated)
}
hide() {
@@ -329,8 +307,12 @@ class Tooltip extends BaseComponent {
const tip = this.getTipElement()
const complete = () => {
- if (this._hoverState !== HOVER_STATE_SHOW && tip.parentNode) {
- tip.parentNode.removeChild(tip)
+ if (this._isWithActiveTrigger()) {
+ return
+ }
+
+ if (this._hoverState !== HOVER_STATE_SHOW) {
+ tip.remove()
}
this._cleanTipClass()
@@ -361,15 +343,8 @@ class Tooltip extends BaseComponent {
this._activeTrigger[TRIGGER_FOCUS] = false
this._activeTrigger[TRIGGER_HOVER] = false
- if (this.tip.classList.contains(CLASS_NAME_FADE)) {
- const transitionDuration = getTransitionDurationFromElement(tip)
-
- EventHandler.one(tip, 'transitionend', complete)
- emulateTransitionEnd(tip, transitionDuration)
- } else {
- complete()
- }
-
+ const isAnimated = this.tip.classList.contains(CLASS_NAME_FADE)
+ this._queueCallback(complete, this.tip, isAnimated)
this._hoverState = ''
}
@@ -391,16 +366,30 @@ class Tooltip extends BaseComponent {
}
const element = document.createElement('div')
- element.innerHTML = this.config.template
+ element.innerHTML = this._config.template
+
+ const tip = element.children[0]
+ this.setContent(tip)
+ tip.classList.remove(CLASS_NAME_FADE, CLASS_NAME_SHOW)
- this.tip = element.children[0]
+ this.tip = tip
return this.tip
}
- setContent() {
- const tip = this.getTipElement()
- this.setElementContent(SelectorEngine.findOne(SELECTOR_TOOLTIP_INNER, tip), this.getTitle())
- tip.classList.remove(CLASS_NAME_FADE, CLASS_NAME_SHOW)
+ setContent(tip) {
+ this._sanitizeAndSetContent(tip, this.getTitle(), SELECTOR_TOOLTIP_INNER)
+ }
+
+ _sanitizeAndSetContent(template, content, selector) {
+ const templateElement = SelectorEngine.findOne(selector, template)
+
+ if (!content && templateElement) {
+ templateElement.remove()
+ return
+ }
+
+ // we use append for html objects to maintain js events
+ this.setElementContent(templateElement, content)
}
setElementContent(element, content) {
@@ -408,16 +397,14 @@ class Tooltip extends BaseComponent {
return
}
- if (typeof content === 'object' && isElement(content)) {
- if (content.jquery) {
- content = content[0]
- }
+ if (isElement(content)) {
+ content = getElement(content)
// content is a DOM node or a jQuery
- if (this.config.html) {
+ if (this._config.html) {
if (content.parentNode !== element) {
element.innerHTML = ''
- element.appendChild(content)
+ element.append(content)
}
} else {
element.textContent = content.textContent
@@ -426,9 +413,9 @@ class Tooltip extends BaseComponent {
return
}
- if (this.config.html) {
- if (this.config.sanitize) {
- content = sanitizeHtml(content, this.config.allowList, this.config.sanitizeFn)
+ if (this._config.html) {
+ if (this._config.sanitize) {
+ content = sanitizeHtml(content, this._config.allowList, this._config.sanitizeFn)
}
element.innerHTML = content
@@ -438,15 +425,9 @@ class Tooltip extends BaseComponent {
}
getTitle() {
- let title = this._element.getAttribute('data-bs-original-title')
-
- if (!title) {
- title = typeof this.config.title === 'function' ?
- this.config.title.call(this._element) :
- this.config.title
- }
+ const title = this._element.getAttribute('data-bs-original-title') || this._config.title
- return title
+ return this._resolvePossibleFunction(title)
}
updateAttachment(attachment) {
@@ -464,19 +445,11 @@ class Tooltip extends BaseComponent {
// Private
_initializeOnDelegatedTarget(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)
- }
-
- return context
+ return context || this.constructor.getOrCreateInstance(event.delegateTarget, this._getDelegateConfig())
}
_getOffset() {
- const { offset } = this.config
+ const { offset } = this._config
if (typeof offset === 'string') {
return offset.split(',').map(val => Number.parseInt(val, 10))
@@ -489,6 +462,10 @@ class Tooltip extends BaseComponent {
return offset
}
+ _resolvePossibleFunction(content) {
+ return typeof content === 'function' ? content.call(this._element) : content
+ }
+
_getPopperConfig(attachment) {
const defaultBsPopperConfig = {
placement: attachment,
@@ -496,8 +473,7 @@ class Tooltip extends BaseComponent {
{
name: 'flip',
options: {
- altBoundary: true,
- fallbackPlacements: this.config.fallbackPlacements
+ fallbackPlacements: this._config.fallbackPlacements
}
},
{
@@ -509,7 +485,7 @@ class Tooltip extends BaseComponent {
{
name: 'preventOverflow',
options: {
- boundary: this.config.boundary
+ boundary: this._config.boundary
}
},
{
@@ -534,24 +510,12 @@ class Tooltip extends BaseComponent {
return {
...defaultBsPopperConfig,
- ...(typeof this.config.popperConfig === 'function' ? this.config.popperConfig(defaultBsPopperConfig) : this.config.popperConfig)
+ ...(typeof this._config.popperConfig === 'function' ? this._config.popperConfig(defaultBsPopperConfig) : this._config.popperConfig)
}
}
_addAttachmentClass(attachment) {
- this.getTipElement().classList.add(`${CLASS_PREFIX}-${this.updateAttachment(attachment)}`)
- }
-
- _getContainer() {
- if (this.config.container === false) {
- return document.body
- }
-
- if (isElement(this.config.container)) {
- return this.config.container
- }
-
- return SelectorEngine.findOne(this.config.container)
+ this.getTipElement().classList.add(`${this._getBasicClassPrefix()}-${this.updateAttachment(attachment)}`)
}
_getAttachment(placement) {
@@ -559,11 +523,11 @@ class Tooltip extends BaseComponent {
}
_setListeners() {
- const triggers = this.config.trigger.split(' ')
+ const triggers = this._config.trigger.split(' ')
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 :
@@ -572,8 +536,8 @@ class Tooltip extends BaseComponent {
this.constructor.Event.MOUSELEAVE :
this.constructor.Event.FOCUSOUT
- EventHandler.on(this._element, eventIn, this.config.selector, event => this._enter(event))
- EventHandler.on(this._element, eventOut, this.config.selector, event => this._leave(event))
+ EventHandler.on(this._element, eventIn, this._config.selector, event => this._enter(event))
+ EventHandler.on(this._element, eventOut, this._config.selector, event => this._leave(event))
}
})
@@ -583,11 +547,11 @@ class Tooltip extends BaseComponent {
}
}
- EventHandler.on(this._element.closest(`.${CLASS_NAME_MODAL}`), 'hide.bs.modal', this._hideModalHandler)
+ EventHandler.on(this._element.closest(SELECTOR_MODAL), EVENT_MODAL_HIDE, this._hideModalHandler)
- if (this.config.selector) {
- this.config = {
- ...this.config,
+ if (this._config.selector) {
+ this._config = {
+ ...this._config,
trigger: 'manual',
selector: ''
}
@@ -628,7 +592,7 @@ class Tooltip extends BaseComponent {
context._hoverState = HOVER_STATE_SHOW
- if (!context.config.delay || !context.config.delay.show) {
+ if (!context._config.delay || !context._config.delay.show) {
context.show()
return
}
@@ -637,7 +601,7 @@ class Tooltip extends BaseComponent {
if (context._hoverState === HOVER_STATE_SHOW) {
context.show()
}
- }, context.config.delay.show)
+ }, context._config.delay.show)
}
_leave(event, context) {
@@ -646,7 +610,7 @@ class Tooltip extends BaseComponent {
if (event) {
context._activeTrigger[
event.type === 'focusout' ? TRIGGER_FOCUS : TRIGGER_HOVER
- ] = false
+ ] = context._element.contains(event.relatedTarget)
}
if (context._isWithActiveTrigger()) {
@@ -657,7 +621,7 @@ class Tooltip extends BaseComponent {
context._hoverState = HOVER_STATE_OUT
- if (!context.config.delay || !context.config.delay.hide) {
+ if (!context._config.delay || !context._config.delay.hide) {
context.hide()
return
}
@@ -666,7 +630,7 @@ class Tooltip extends BaseComponent {
if (context._hoverState === HOVER_STATE_OUT) {
context.hide()
}
- }, context.config.delay.hide)
+ }, context._config.delay.hide)
}
_isWithActiveTrigger() {
@@ -688,16 +652,14 @@ class Tooltip extends BaseComponent {
}
})
- if (config && typeof config.container === 'object' && config.container.jquery) {
- config.container = config.container[0]
- }
-
config = {
...this.constructor.Default,
...dataAttributes,
...(typeof config === 'object' && config ? config : {})
}
+ config.container = config.container === false ? document.body : getElement(config.container)
+
if (typeof config.delay === 'number') {
config.delay = {
show: config.delay,
@@ -725,26 +687,32 @@ class Tooltip extends BaseComponent {
_getDelegateConfig() {
const config = {}
- if (this.config) {
- for (const key in this.config) {
- if (this.constructor.Default[key] !== this.config[key]) {
- config[key] = this.config[key]
- }
+ for (const key in this._config) {
+ if (this.constructor.Default[key] !== this._config[key]) {
+ config[key] = this._config[key]
}
}
+ // In the future can be replaced with:
+ // const keysWithDifferentValues = Object.entries(this._config).filter(entry => this.constructor.Default[entry[0]] !== this._config[entry[0]])
+ // `Object.fromEntries(keysWithDifferentValues)`
return config
}
_cleanTipClass() {
const tip = this.getTipElement()
- const tabClass = tip.getAttribute('class').match(BSCLS_PREFIX_REGEX)
+ const basicClassPrefixRegex = new RegExp(`(^|\\s)${this._getBasicClassPrefix()}\\S+`, 'g')
+ const tabClass = tip.getAttribute('class').match(basicClassPrefixRegex)
if (tabClass !== null && tabClass.length > 0) {
tabClass.map(token => token.trim())
.forEach(tClass => tip.classList.remove(tClass))
}
}
+ _getBasicClassPrefix() {
+ return CLASS_PREFIX
+ }
+
_handlePopperPlacementChange(popperData) {
const { state } = popperData
@@ -761,16 +729,7 @@ class Tooltip extends BaseComponent {
static jQueryInterface(config) {
return this.each(function () {
- let data = Data.getData(this, DATA_KEY)
- const _config = typeof config === 'object' && config
-
- if (!data && /dispose|hide/.test(config)) {
- return
- }
-
- if (!data) {
- data = new Tooltip(this, _config)
- }
+ const data = Tooltip.getOrCreateInstance(this, config)
if (typeof config === 'string') {
if (typeof data[config] === 'undefined') {
@@ -790,6 +749,6 @@ class Tooltip extends BaseComponent {
* add .Tooltip to jQuery only if jQuery is present
*/
-defineJQueryPlugin(NAME, Tooltip)
+defineJQueryPlugin(Tooltip)
export default Tooltip
diff --git a/js/src/util/backdrop.js b/js/src/util/backdrop.js
new file mode 100644
index 000000000..0f515b3d3
--- /dev/null
+++ b/js/src/util/backdrop.js
@@ -0,0 +1,130 @@
+/**
+ * --------------------------------------------------------------------------
+ * Bootstrap (v5.1.0): util/backdrop.js
+ * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
+ * --------------------------------------------------------------------------
+ */
+
+import EventHandler from '../dom/event-handler'
+import { execute, executeAfterTransition, getElement, reflow, typeCheckConfig } from './index'
+
+const Default = {
+ className: 'modal-backdrop',
+ isVisible: true, // if false, we use the backdrop helper without adding any element to the dom
+ isAnimated: false,
+ rootElement: 'body', // give the choice to place backdrop under different elements
+ clickCallback: null
+}
+
+const DefaultType = {
+ className: 'string',
+ isVisible: 'boolean',
+ isAnimated: 'boolean',
+ rootElement: '(element|string)',
+ clickCallback: '(function|null)'
+}
+const NAME = '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 = this._config.className
+ if (this._config.isAnimated) {
+ backdrop.classList.add(CLASS_NAME_FADE)
+ }
+
+ this._element = backdrop
+ }
+
+ return this._element
+ }
+
+ _getConfig(config) {
+ config = {
+ ...Default,
+ ...(typeof config === 'object' ? config : {})
+ }
+
+ // use getElement() with the default "body" to get a fresh Element on each instantiation
+ config.rootElement = getElement(config.rootElement)
+ typeCheckConfig(NAME, config, DefaultType)
+ return config
+ }
+
+ _append() {
+ if (this._isAppended) {
+ return
+ }
+
+ this._config.rootElement.append(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._element.remove()
+ this._isAppended = false
+ }
+
+ _emulateAnimation(callback) {
+ executeAfterTransition(callback, this._getElement(), this._config.isAnimated)
+ }
+}
+
+export default Backdrop
diff --git a/js/src/util/component-functions.js b/js/src/util/component-functions.js
new file mode 100644
index 000000000..0737b6374
--- /dev/null
+++ b/js/src/util/component-functions.js
@@ -0,0 +1,34 @@
+/**
+ * --------------------------------------------------------------------------
+ * Bootstrap (v5.1.0): util/component-functions.js
+ * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
+ * --------------------------------------------------------------------------
+ */
+
+import EventHandler from '../dom/event-handler'
+import { getElementFromSelector, isDisabled } from './index'
+
+const enableDismissTrigger = (component, method = 'hide') => {
+ const clickEvent = `click.dismiss${component.EVENT_KEY}`
+ const name = component.NAME
+
+ EventHandler.on(document, clickEvent, `[data-bs-dismiss="${name}"]`, function (event) {
+ if (['A', 'AREA'].includes(this.tagName)) {
+ event.preventDefault()
+ }
+
+ if (isDisabled(this)) {
+ return
+ }
+
+ const target = getElementFromSelector(this) || this.closest(`.${name}`)
+ const instance = component.getOrCreateInstance(target)
+
+ // Method argument is left, for Alert and only, as it doesn't implement the 'hide' method
+ instance[method]()
+ })
+}
+
+export {
+ enableDismissTrigger
+}
diff --git a/js/src/util/focustrap.js b/js/src/util/focustrap.js
new file mode 100644
index 000000000..e35bbe6ab
--- /dev/null
+++ b/js/src/util/focustrap.js
@@ -0,0 +1,109 @@
+/**
+ * --------------------------------------------------------------------------
+ * Bootstrap (v5.1.0): util/focustrap.js
+ * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
+ * --------------------------------------------------------------------------
+ */
+
+import EventHandler from '../dom/event-handler'
+import SelectorEngine from '../dom/selector-engine'
+import { typeCheckConfig } from './index'
+
+const Default = {
+ trapElement: null, // The element to trap focus inside of
+ autofocus: true
+}
+
+const DefaultType = {
+ trapElement: 'element',
+ autofocus: 'boolean'
+}
+
+const NAME = 'focustrap'
+const DATA_KEY = 'bs.focustrap'
+const EVENT_KEY = `.${DATA_KEY}`
+const EVENT_FOCUSIN = `focusin${EVENT_KEY}`
+const EVENT_KEYDOWN_TAB = `keydown.tab${EVENT_KEY}`
+
+const TAB_KEY = 'Tab'
+const TAB_NAV_FORWARD = 'forward'
+const TAB_NAV_BACKWARD = 'backward'
+
+class FocusTrap {
+ constructor(config) {
+ this._config = this._getConfig(config)
+ this._isActive = false
+ this._lastTabNavDirection = null
+ }
+
+ activate() {
+ const { trapElement, autofocus } = this._config
+
+ if (this._isActive) {
+ return
+ }
+
+ if (autofocus) {
+ trapElement.focus()
+ }
+
+ EventHandler.off(document, EVENT_KEY) // guard against infinite focus loop
+ EventHandler.on(document, EVENT_FOCUSIN, event => this._handleFocusin(event))
+ EventHandler.on(document, EVENT_KEYDOWN_TAB, event => this._handleKeydown(event))
+
+ this._isActive = true
+ }
+
+ deactivate() {
+ if (!this._isActive) {
+ return
+ }
+
+ this._isActive = false
+ EventHandler.off(document, EVENT_KEY)
+ }
+
+ // Private
+
+ _handleFocusin(event) {
+ const { target } = event
+ const { trapElement } = this._config
+
+ if (
+ target === document ||
+ target === trapElement ||
+ trapElement.contains(target)
+ ) {
+ return
+ }
+
+ const elements = SelectorEngine.focusableChildren(trapElement)
+
+ if (elements.length === 0) {
+ trapElement.focus()
+ } else if (this._lastTabNavDirection === TAB_NAV_BACKWARD) {
+ elements[elements.length - 1].focus()
+ } else {
+ elements[0].focus()
+ }
+ }
+
+ _handleKeydown(event) {
+ if (event.key !== TAB_KEY) {
+ return
+ }
+
+ this._lastTabNavDirection = event.shiftKey ? TAB_NAV_BACKWARD : TAB_NAV_FORWARD
+ }
+
+ _getConfig(config) {
+ config = {
+ ...Default,
+ ...(typeof config === 'object' ? config : {})
+ }
+ typeCheckConfig(NAME, config, DefaultType)
+ return config
+ }
+}
+
+export default FocusTrap
diff --git a/js/src/util/index.js b/js/src/util/index.js
index ae3cd2ac0..bed2534e5 100644
--- a/js/src/util/index.js
+++ b/js/src/util/index.js
@@ -1,6 +1,6 @@
/**
* --------------------------------------------------------------------------
- * Bootstrap (v5.0.0-beta2): util/index.js
+ * Bootstrap (v5.1.0): util/index.js
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
* --------------------------------------------------------------------------
*/
@@ -48,7 +48,7 @@ const getSelector = element => {
// Just in case some CMS puts out a full URL with the anchor appended
if (hrefAttr.includes('#') && !hrefAttr.startsWith('#')) {
- hrefAttr = '#' + hrefAttr.split('#')[1]
+ hrefAttr = `#${hrefAttr.split('#')[1]}`
}
selector = hrefAttr && hrefAttr !== '#' ? hrefAttr.trim() : null
@@ -100,24 +100,28 @@ const triggerTransitionEnd = element => {
element.dispatchEvent(new Event(TRANSITION_END))
}
-const isElement = obj => (obj[0] || obj).nodeType
+const isElement = obj => {
+ if (!obj || typeof obj !== 'object') {
+ return false
+ }
-const emulateTransitionEnd = (element, duration) => {
- let called = false
- const durationPadding = 5
- const emulatedDuration = duration + durationPadding
+ if (typeof obj.jquery !== 'undefined') {
+ obj = obj[0]
+ }
- function listener() {
- called = true
- element.removeEventListener(TRANSITION_END, listener)
+ return typeof obj.nodeType !== 'undefined'
+}
+
+const getElement = obj => {
+ if (isElement(obj)) { // it's a jQuery object or a node element
+ return obj.jquery ? obj[0] : obj
}
- element.addEventListener(TRANSITION_END, listener)
- setTimeout(() => {
- if (!called) {
- triggerTransitionEnd(element)
- }
- }, emulatedDuration)
+ if (typeof obj === 'string' && obj.length > 0) {
+ return document.querySelector(obj)
+ }
+
+ return null
}
const typeCheckConfig = (componentName, config, configTypes) => {
@@ -128,29 +132,34 @@ const typeCheckConfig = (componentName, config, configTypes) => {
if (!new RegExp(expectedTypes).test(valueType)) {
throw new TypeError(
- `${componentName.toUpperCase()}: ` +
- `Option "${property}" provided type "${valueType}" ` +
- `but expected type "${expectedTypes}".`
+ `${componentName.toUpperCase()}: Option "${property}" provided type "${valueType}" but expected type "${expectedTypes}".`
)
}
})
}
const isVisible = element => {
- if (!element) {
+ if (!isElement(element) || element.getClientRects().length === 0) {
return false
}
- if (element.style && element.parentNode && element.parentNode.style) {
- const elementStyle = getComputedStyle(element)
- const parentNodeStyle = getComputedStyle(element.parentNode)
+ return getComputedStyle(element).getPropertyValue('visibility') === 'visible'
+}
+
+const isDisabled = element => {
+ if (!element || element.nodeType !== Node.ELEMENT_NODE) {
+ return true
+ }
+
+ if (element.classList.contains('disabled')) {
+ return true
+ }
- return elementStyle.display !== 'none' &&
- parentNodeStyle.display !== 'none' &&
- elementStyle.visibility !== 'hidden'
+ if (typeof element.disabled !== 'undefined') {
+ return element.disabled
}
- return false
+ return element.hasAttribute('disabled') && element.getAttribute('disabled') !== 'false'
}
const findShadowRoot = element => {
@@ -176,9 +185,20 @@ const findShadowRoot = element => {
return findShadowRoot(element.parentNode)
}
-const noop = () => function () {}
+const noop = () => {}
-const reflow = element => element.offsetHeight
+/**
+ * Trick to restart an element's animation
+ *
+ * @param {HTMLElement} element
+ * @return void
+ *
+ * @see https://www.charistheo.io/blog/2021/02/restart-a-css-animation-with-javascript/#restarting-a-css-animation
+ */
+const reflow = element => {
+ // eslint-disable-next-line no-unused-expressions
+ element.offsetHeight
+}
const getjQuery = () => {
const { jQuery } = window
@@ -190,9 +210,18 @@ const getjQuery = () => {
return null
}
+const DOMContentLoadedCallbacks = []
+
const onDOMContentLoaded = callback => {
if (document.readyState === 'loading') {
- document.addEventListener('DOMContentLoaded', callback)
+ // add listener on the first call when the document is in loading state
+ if (!DOMContentLoadedCallbacks.length) {
+ document.addEventListener('DOMContentLoaded', () => {
+ DOMContentLoadedCallbacks.forEach(callback => callback())
+ })
+ }
+
+ DOMContentLoadedCallbacks.push(callback)
} else {
callback()
}
@@ -200,11 +229,12 @@ const onDOMContentLoaded = callback => {
const isRTL = () => document.documentElement.dir === 'rtl'
-const defineJQueryPlugin = (name, plugin) => {
+const defineJQueryPlugin = plugin => {
onDOMContentLoaded(() => {
const $ = getjQuery()
/* istanbul ignore if */
if ($) {
+ const name = plugin.NAME
const JQUERY_NO_CONFLICT = $.fn[name]
$.fn[name] = plugin.jQueryInterface
$.fn[name].Constructor = plugin
@@ -216,21 +246,88 @@ const defineJQueryPlugin = (name, plugin) => {
})
}
+const execute = callback => {
+ if (typeof callback === 'function') {
+ callback()
+ }
+}
+
+const executeAfterTransition = (callback, transitionElement, waitForTransition = true) => {
+ if (!waitForTransition) {
+ execute(callback)
+ return
+ }
+
+ const durationPadding = 5
+ const emulatedDuration = getTransitionDurationFromElement(transitionElement) + durationPadding
+
+ let called = false
+
+ const handler = ({ target }) => {
+ if (target !== transitionElement) {
+ return
+ }
+
+ called = true
+ transitionElement.removeEventListener(TRANSITION_END, handler)
+ execute(callback)
+ }
+
+ transitionElement.addEventListener(TRANSITION_END, handler)
+ setTimeout(() => {
+ if (!called) {
+ triggerTransitionEnd(transitionElement)
+ }
+ }, emulatedDuration)
+}
+
+/**
+ * Return the previous/next element of a list.
+ *
+ * @param {array} list The list of elements
+ * @param activeElement The active element
+ * @param shouldGetNext Choose to get next or previous element
+ * @param isCycleAllowed
+ * @return {Element|elem} The proper element
+ */
+const getNextActiveElement = (list, activeElement, shouldGetNext, isCycleAllowed) => {
+ let index = list.indexOf(activeElement)
+
+ // if the element does not exist in the list return an element depending on the direction and if cycle is allowed
+ if (index === -1) {
+ return list[!shouldGetNext && isCycleAllowed ? list.length - 1 : 0]
+ }
+
+ const listLength = list.length
+
+ index += shouldGetNext ? 1 : -1
+
+ if (isCycleAllowed) {
+ index = (index + listLength) % listLength
+ }
+
+ return list[Math.max(0, Math.min(index, listLength - 1))]
+}
+
export {
+ getElement,
getUID,
getSelectorFromElement,
getElementFromSelector,
getTransitionDurationFromElement,
triggerTransitionEnd,
isElement,
- emulateTransitionEnd,
typeCheckConfig,
isVisible,
+ isDisabled,
findShadowRoot,
noop,
+ getNextActiveElement,
reflow,
getjQuery,
onDOMContentLoaded,
isRTL,
- defineJQueryPlugin
+ defineJQueryPlugin,
+ execute,
+ executeAfterTransition
}
diff --git a/js/src/util/sanitizer.js b/js/src/util/sanitizer.js
index 18ac6f943..d40655918 100644
--- a/js/src/util/sanitizer.js
+++ b/js/src/util/sanitizer.js
@@ -1,6 +1,6 @@
/**
* --------------------------------------------------------------------------
- * Bootstrap (v5.0.0-beta2): util/sanitizer.js
+ * Bootstrap (v5.1.0): 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.
@@ -108,7 +108,7 @@ export function sanitizeHtml(unsafeHtml, allowList, sanitizeFn) {
const elName = el.nodeName.toLowerCase()
if (!allowlistKeys.includes(elName)) {
- el.parentNode.removeChild(el)
+ el.remove()
continue
}
diff --git a/js/src/util/scrollbar.js b/js/src/util/scrollbar.js
new file mode 100644
index 000000000..c90d82907
--- /dev/null
+++ b/js/src/util/scrollbar.js
@@ -0,0 +1,97 @@
+/**
+ * --------------------------------------------------------------------------
+ * Bootstrap (v5.1.0): 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'
+import { isElement } from './index'
+
+const SELECTOR_FIXED_CONTENT = '.fixed-top, .fixed-bottom, .is-fixed, .sticky-top'
+const SELECTOR_STICKY_CONTENT = '.sticky-top'
+
+class ScrollBarHelper {
+ constructor() {
+ this._element = document.body
+ }
+
+ 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)
+ }
+
+ hide() {
+ const width = this.getWidth()
+ this._disableOverFlow()
+ // give padding to element to balance the hidden scrollbar width
+ this._setElementAttributes(this._element, 'paddingRight', calculatedValue => calculatedValue + width)
+ // trick: We adjust positive paddingRight and negative marginRight to sticky-top elements to keep showing fullwidth
+ this._setElementAttributes(SELECTOR_FIXED_CONTENT, 'paddingRight', calculatedValue => calculatedValue + width)
+ this._setElementAttributes(SELECTOR_STICKY_CONTENT, 'marginRight', calculatedValue => calculatedValue - width)
+ }
+
+ _disableOverFlow() {
+ this._saveInitialAttribute(this._element, 'overflow')
+ this._element.style.overflow = 'hidden'
+ }
+
+ _setElementAttributes(selector, styleProp, callback) {
+ const scrollbarWidth = this.getWidth()
+ const manipulationCallBack = element => {
+ if (element !== this._element && window.innerWidth > element.clientWidth + scrollbarWidth) {
+ return
+ }
+
+ this._saveInitialAttribute(element, styleProp)
+ const calculatedValue = window.getComputedStyle(element)[styleProp]
+ element.style[styleProp] = `${callback(Number.parseFloat(calculatedValue))}px`
+ }
+
+ this._applyManipulationCallback(selector, manipulationCallBack)
+ }
+
+ reset() {
+ this._resetElementAttributes(this._element, 'overflow')
+ this._resetElementAttributes(this._element, 'paddingRight')
+ this._resetElementAttributes(SELECTOR_FIXED_CONTENT, 'paddingRight')
+ this._resetElementAttributes(SELECTOR_STICKY_CONTENT, 'marginRight')
+ }
+
+ _saveInitialAttribute(element, styleProp) {
+ const actualValue = element.style[styleProp]
+ if (actualValue) {
+ Manipulator.setDataAttribute(element, styleProp, actualValue)
+ }
+ }
+
+ _resetElementAttributes(selector, styleProp) {
+ const manipulationCallBack = element => {
+ const value = Manipulator.getDataAttribute(element, styleProp)
+ if (typeof value === 'undefined') {
+ element.style.removeProperty(styleProp)
+ } else {
+ Manipulator.removeDataAttribute(element, styleProp)
+ element.style[styleProp] = value
+ }
+ }
+
+ this._applyManipulationCallback(selector, manipulationCallBack)
+ }
+
+ _applyManipulationCallback(selector, callBack) {
+ if (isElement(selector)) {
+ callBack(selector)
+ } else {
+ SelectorEngine.find(selector, this._element).forEach(callBack)
+ }
+ }
+
+ isOverflowing() {
+ return this.getWidth() > 0
+ }
+}
+
+export default ScrollBarHelper