From db46bfc03f3a22752ef6bd91ae577d893872a216 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Sat, 24 Jun 2023 14:44:40 +0200 Subject: Merging upstream version 5.3.0+dfsg. Signed-off-by: Daniel Baumann --- js/src/alert.js | 10 ++--- js/src/base-component.js | 12 +++--- js/src/button.js | 8 ++-- js/src/carousel.js | 19 +++++---- js/src/collapse.js | 23 +++++------ js/src/dom/data.js | 2 +- js/src/dom/event-handler.js | 19 ++++----- js/src/dom/manipulator.js | 2 +- js/src/dom/selector-engine.js | 53 +++++++++++++++++++++--- js/src/dropdown.js | 21 +++++----- js/src/modal.js | 27 ++++++------- js/src/offcanvas.js | 27 ++++++------- js/src/popover.js | 6 +-- js/src/scrollspy.js | 14 +++---- js/src/tab.js | 22 +++++----- js/src/toast.js | 10 ++--- js/src/tooltip.js | 36 ++++++++--------- js/src/util/backdrop.js | 8 ++-- js/src/util/component-functions.js | 9 +++-- js/src/util/config.js | 9 ++--- js/src/util/focustrap.js | 8 ++-- js/src/util/index.js | 68 +++++++++---------------------- js/src/util/sanitizer.js | 82 ++++++++++++++++++-------------------- js/src/util/scrollbar.js | 8 ++-- js/src/util/swipe.js | 8 ++-- js/src/util/template-factory.js | 12 +++--- 26 files changed, 260 insertions(+), 263 deletions(-) (limited to 'js/src') diff --git a/js/src/alert.js b/js/src/alert.js index 59de828..88232bc 100644 --- a/js/src/alert.js +++ b/js/src/alert.js @@ -1,14 +1,14 @@ /** * -------------------------------------------------------------------------- - * Bootstrap (v5.2.3): alert.js + * Bootstrap alert.js * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) * -------------------------------------------------------------------------- */ -import { defineJQueryPlugin } from './util/index' -import EventHandler from './dom/event-handler' -import BaseComponent from './base-component' -import { enableDismissTrigger } from './util/component-functions' +import BaseComponent from './base-component.js' +import EventHandler from './dom/event-handler.js' +import { enableDismissTrigger } from './util/component-functions.js' +import { defineJQueryPlugin } from './util/index.js' /** * Constants diff --git a/js/src/base-component.js b/js/src/base-component.js index 0c1a259..d13c7ab 100644 --- a/js/src/base-component.js +++ b/js/src/base-component.js @@ -1,20 +1,20 @@ /** * -------------------------------------------------------------------------- - * Bootstrap (v5.2.3): base-component.js + * Bootstrap 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' -import Config from './util/config' +import Data from './dom/data.js' +import EventHandler from './dom/event-handler.js' +import Config from './util/config.js' +import { executeAfterTransition, getElement } from './util/index.js' /** * Constants */ -const VERSION = '5.2.3' +const VERSION = '5.3.0' /** * Class definition diff --git a/js/src/button.js b/js/src/button.js index 03e7604..a797f50 100644 --- a/js/src/button.js +++ b/js/src/button.js @@ -1,13 +1,13 @@ /** * -------------------------------------------------------------------------- - * Bootstrap (v5.2.3): button.js + * Bootstrap button.js * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) * -------------------------------------------------------------------------- */ -import { defineJQueryPlugin } from './util/index' -import EventHandler from './dom/event-handler' -import BaseComponent from './base-component' +import BaseComponent from './base-component.js' +import EventHandler from './dom/event-handler.js' +import { defineJQueryPlugin } from './util/index.js' /** * Constants diff --git a/js/src/carousel.js b/js/src/carousel.js index 24bbe39..68d11a3 100644 --- a/js/src/carousel.js +++ b/js/src/carousel.js @@ -1,24 +1,23 @@ /** * -------------------------------------------------------------------------- - * Bootstrap (v5.2.3): carousel.js + * Bootstrap carousel.js * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) * -------------------------------------------------------------------------- */ +import BaseComponent from './base-component.js' +import EventHandler from './dom/event-handler.js' +import Manipulator from './dom/manipulator.js' +import SelectorEngine from './dom/selector-engine.js' import { defineJQueryPlugin, - getElementFromSelector, getNextActiveElement, isRTL, isVisible, reflow, triggerTransitionEnd -} from './util/index' -import EventHandler from './dom/event-handler' -import Manipulator from './dom/manipulator' -import SelectorEngine from './dom/selector-engine' -import Swipe from './util/swipe' -import BaseComponent from './base-component' +} from './util/index.js' +import Swipe from './util/swipe.js' /** * Constants @@ -330,7 +329,7 @@ class Carousel extends BaseComponent { if (!activeElement || !nextElement) { // Some weirdness is happening, so we bail - // todo: change tests that use empty divs to avoid this check + // TODO: change tests that use empty divs to avoid this check return } @@ -431,7 +430,7 @@ class Carousel extends BaseComponent { */ EventHandler.on(document, EVENT_CLICK_DATA_API, SELECTOR_DATA_SLIDE, function (event) { - const target = getElementFromSelector(this) + const target = SelectorEngine.getElementFromSelector(this) if (!target || !target.classList.contains(CLASS_NAME_CAROUSEL)) { return diff --git a/js/src/collapse.js b/js/src/collapse.js index 204d180..9f0c60c 100644 --- a/js/src/collapse.js +++ b/js/src/collapse.js @@ -1,20 +1,18 @@ /** * -------------------------------------------------------------------------- - * Bootstrap (v5.2.3): collapse.js + * Bootstrap collapse.js * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) * -------------------------------------------------------------------------- */ +import BaseComponent from './base-component.js' +import EventHandler from './dom/event-handler.js' +import SelectorEngine from './dom/selector-engine.js' import { defineJQueryPlugin, getElement, - getElementFromSelector, - getSelectorFromElement, reflow -} from './util/index' -import EventHandler from './dom/event-handler' -import SelectorEngine from './dom/selector-engine' -import BaseComponent from './base-component' +} from './util/index.js' /** * Constants @@ -68,7 +66,7 @@ class Collapse extends BaseComponent { const toggleList = SelectorEngine.find(SELECTOR_DATA_TOGGLE) for (const elem of toggleList) { - const selector = getSelectorFromElement(elem) + const selector = SelectorEngine.getSelectorFromElement(elem) const filterElement = SelectorEngine.find(selector) .filter(foundElement => foundElement === this._element) @@ -185,7 +183,7 @@ class Collapse extends BaseComponent { this._element.classList.remove(CLASS_NAME_COLLAPSE, CLASS_NAME_SHOW) for (const trigger of this._triggerArray) { - const element = getElementFromSelector(trigger) + const element = SelectorEngine.getElementFromSelector(trigger) if (element && !this._isShown(element)) { this._addAriaAndCollapsedClass([trigger], false) @@ -229,7 +227,7 @@ class Collapse extends BaseComponent { const children = this._getFirstLevelChildren(SELECTOR_DATA_TOGGLE) for (const element of children) { - const selected = getElementFromSelector(element) + const selected = SelectorEngine.getElementFromSelector(element) if (selected) { this._addAriaAndCollapsedClass([element], this._isShown(selected)) @@ -285,10 +283,7 @@ EventHandler.on(document, EVENT_CLICK_DATA_API, SELECTOR_DATA_TOGGLE, function ( event.preventDefault() } - const selector = getSelectorFromElement(this) - const selectorElements = SelectorEngine.find(selector) - - for (const element of selectorElements) { + for (const element of SelectorEngine.getMultipleElementsFromSelector(this)) { Collapse.getOrCreateInstance(element, { toggle: false }).toggle() } }) diff --git a/js/src/dom/data.js b/js/src/dom/data.js index 2c6a46e..407f67e 100644 --- a/js/src/dom/data.js +++ b/js/src/dom/data.js @@ -1,6 +1,6 @@ /** * -------------------------------------------------------------------------- - * Bootstrap (v5.2.3): dom/data.js + * Bootstrap dom/data.js * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) * -------------------------------------------------------------------------- */ diff --git a/js/src/dom/event-handler.js b/js/src/dom/event-handler.js index 9876d77..561d875 100644 --- a/js/src/dom/event-handler.js +++ b/js/src/dom/event-handler.js @@ -1,11 +1,11 @@ /** * -------------------------------------------------------------------------- - * Bootstrap (v5.2.3): dom/event-handler.js + * Bootstrap dom/event-handler.js * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) * -------------------------------------------------------------------------- */ -import { getjQuery } from '../util/index' +import { getjQuery } from '../util/index.js' /** * Constants @@ -128,7 +128,7 @@ function findHandler(events, callable, delegationSelector = null) { function normalizeParameters(originalTypeEvent, handler, delegationFunction) { const isDelegated = typeof handler === 'string' - // todo: tooltip passes `false` instead of selector, so we need to check + // TODO: tooltip passes `false` instead of selector, so we need to check const callable = isDelegated ? delegationFunction : (handler || delegationFunction) let typeEvent = getTypeEvent(originalTypeEvent) @@ -198,9 +198,8 @@ function removeHandler(element, events, typeEvent, handler, delegationSelector) function removeNamespacedHandlers(element, events, typeEvent, namespace) { const storeElementEvent = events[typeEvent] || {} - for (const handlerKey of Object.keys(storeElementEvent)) { + for (const [handlerKey, event] of Object.entries(storeElementEvent)) { if (handlerKey.includes(namespace)) { - const event = storeElementEvent[handlerKey] removeHandler(element, events, typeEvent, event.callable, event.delegationSelector) } } @@ -248,11 +247,10 @@ const EventHandler = { } } - for (const keyHandlers of Object.keys(storeElementEvent)) { + for (const [keyHandlers, event] of Object.entries(storeElementEvent)) { const handlerKey = keyHandlers.replace(stripUidRegex, '') if (!inNamespace || originalTypeEvent.includes(handlerKey)) { - const event = storeElementEvent[keyHandlers] removeHandler(element, events, typeEvent, event.callable, event.delegationSelector) } } @@ -281,8 +279,7 @@ const EventHandler = { defaultPrevented = jQueryEvent.isDefaultPrevented() } - let evt = new Event(event, { bubbles, cancelable: true }) - evt = hydrateObj(evt, args) + const evt = hydrateObj(new Event(event, { bubbles, cancelable: true }), args) if (defaultPrevented) { evt.preventDefault() @@ -300,8 +297,8 @@ const EventHandler = { } } -function hydrateObj(obj, meta) { - for (const [key, value] of Object.entries(meta || {})) { +function hydrateObj(obj, meta = {}) { + for (const [key, value] of Object.entries(meta)) { try { obj[key] = value } catch { diff --git a/js/src/dom/manipulator.js b/js/src/dom/manipulator.js index 38ecfe4..dd86a9f 100644 --- a/js/src/dom/manipulator.js +++ b/js/src/dom/manipulator.js @@ -1,6 +1,6 @@ /** * -------------------------------------------------------------------------- - * Bootstrap (v5.2.3): dom/manipulator.js + * Bootstrap dom/manipulator.js * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) * -------------------------------------------------------------------------- */ diff --git a/js/src/dom/selector-engine.js b/js/src/dom/selector-engine.js index 1ba104f..3cecf6f 100644 --- a/js/src/dom/selector-engine.js +++ b/js/src/dom/selector-engine.js @@ -1,15 +1,36 @@ /** * -------------------------------------------------------------------------- - * Bootstrap (v5.2.3): dom/selector-engine.js + * Bootstrap dom/selector-engine.js * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) * -------------------------------------------------------------------------- */ -import { isDisabled, isVisible } from '../util/index' +import { isDisabled, isVisible, parseSelector } from '../util/index.js' -/** - * Constants - */ +const getSelector = element => { + let selector = element.getAttribute('data-bs-target') + + if (!selector || selector === '#') { + let hrefAttribute = element.getAttribute('href') + + // The only valid content that could double as a selector are IDs or classes, + // so everything starting with `#` or `.`. If a "real" URL is used as the selector, + // `document.querySelector` will rightfully complain it is invalid. + // See https://github.com/twbs/bootstrap/issues/32273 + if (!hrefAttribute || (!hrefAttribute.includes('#') && !hrefAttribute.startsWith('.'))) { + return null + } + + // Just in case some CMS puts out a full URL with the anchor appended + if (hrefAttribute.includes('#') && !hrefAttribute.startsWith('#')) { + hrefAttribute = `#${hrefAttribute.split('#')[1]}` + } + + selector = hrefAttribute && hrefAttribute !== '#' ? hrefAttribute.trim() : null + } + + return parseSelector(selector) +} const SelectorEngine = { find(selector, element = document.documentElement) { @@ -77,6 +98,28 @@ const SelectorEngine = { ].map(selector => `${selector}:not([tabindex^="-"])`).join(',') return this.find(focusables, element).filter(el => !isDisabled(el) && isVisible(el)) + }, + + getSelectorFromElement(element) { + const selector = getSelector(element) + + if (selector) { + return SelectorEngine.findOne(selector) ? selector : null + } + + return null + }, + + getElementFromSelector(element) { + const selector = getSelector(element) + + return selector ? SelectorEngine.findOne(selector) : null + }, + + getMultipleElementsFromSelector(element) { + const selector = getSelector(element) + + return selector ? SelectorEngine.find(selector) : [] } } diff --git a/js/src/dropdown.js b/js/src/dropdown.js index 9596baa..af5fd16 100644 --- a/js/src/dropdown.js +++ b/js/src/dropdown.js @@ -1,13 +1,18 @@ /** * -------------------------------------------------------------------------- - * Bootstrap (v5.2.3): dropdown.js + * Bootstrap dropdown.js * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) * -------------------------------------------------------------------------- */ import * as Popper from '@popperjs/core' +import BaseComponent from './base-component.js' +import EventHandler from './dom/event-handler.js' +import Manipulator from './dom/manipulator.js' +import SelectorEngine from './dom/selector-engine.js' import { defineJQueryPlugin, + execute, getElement, getNextActiveElement, isDisabled, @@ -15,11 +20,7 @@ import { isRTL, isVisible, noop -} from './util/index' -import EventHandler from './dom/event-handler' -import Manipulator from './dom/manipulator' -import SelectorEngine from './dom/selector-engine' -import BaseComponent from './base-component' +} from './util/index.js' /** * Constants @@ -95,7 +96,7 @@ class Dropdown extends BaseComponent { this._popper = null this._parent = this._element.parentNode // dropdown wrapper - // todo: v6 revert #37011 & change markup https://getbootstrap.com/docs/5.2/forms/input-group/ + // TODO: v6 revert #37011 & change markup https://getbootstrap.com/docs/5.3/forms/input-group/ this._menu = SelectorEngine.next(this._element, SELECTOR_MENU)[0] || SelectorEngine.prev(this._element, SELECTOR_MENU)[0] || SelectorEngine.findOne(SELECTOR_MENU, this._parent) @@ -310,7 +311,7 @@ class Dropdown extends BaseComponent { // Disable Popper if we have a static display or Dropdown is in Navbar if (this._inNavbar || this._config.display === 'static') { - Manipulator.setDataAttribute(this._menu, 'popper', 'static') // todo:v6 remove + Manipulator.setDataAttribute(this._menu, 'popper', 'static') // TODO: v6 remove defaultBsPopperConfig.modifiers = [{ name: 'applyStyles', enabled: false @@ -319,7 +320,7 @@ class Dropdown extends BaseComponent { return { ...defaultBsPopperConfig, - ...(typeof this._config.popperConfig === 'function' ? this._config.popperConfig(defaultBsPopperConfig) : this._config.popperConfig) + ...execute(this._config.popperConfig, [defaultBsPopperConfig]) } } @@ -408,7 +409,7 @@ class Dropdown extends BaseComponent { event.preventDefault() - // todo: v6 revert #37011 & change markup https://getbootstrap.com/docs/5.2/forms/input-group/ + // TODO: v6 revert #37011 & change markup https://getbootstrap.com/docs/5.3/forms/input-group/ const getToggleButton = this.matches(SELECTOR_DATA_TOGGLE) ? this : (SelectorEngine.prev(this, SELECTOR_DATA_TOGGLE)[0] || diff --git a/js/src/modal.js b/js/src/modal.js index 26c7e8c..b44cbb9 100644 --- a/js/src/modal.js +++ b/js/src/modal.js @@ -1,18 +1,18 @@ /** * -------------------------------------------------------------------------- - * Bootstrap (v5.2.3): modal.js + * Bootstrap modal.js * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) * -------------------------------------------------------------------------- */ -import { defineJQueryPlugin, getElementFromSelector, isRTL, isVisible, reflow } from './util/index' -import EventHandler from './dom/event-handler' -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' +import BaseComponent from './base-component.js' +import EventHandler from './dom/event-handler.js' +import SelectorEngine from './dom/selector-engine.js' +import Backdrop from './util/backdrop.js' +import { enableDismissTrigger } from './util/component-functions.js' +import FocusTrap from './util/focustrap.js' +import { defineJQueryPlugin, isRTL, isVisible, reflow } from './util/index.js' +import ScrollBarHelper from './util/scrollbar.js' /** * Constants @@ -139,12 +139,12 @@ class Modal extends BaseComponent { } dispose() { - for (const htmlElement of [window, this._dialog]) { - EventHandler.off(htmlElement, EVENT_KEY) - } + EventHandler.off(window, EVENT_KEY) + EventHandler.off(this._dialog, EVENT_KEY) this._backdrop.dispose() this._focustrap.deactivate() + super.dispose() } @@ -208,7 +208,6 @@ class Modal extends BaseComponent { } if (this._config.keyboard) { - event.preventDefault() this.hide() return } @@ -336,7 +335,7 @@ class Modal extends BaseComponent { */ EventHandler.on(document, EVENT_CLICK_DATA_API, SELECTOR_DATA_TOGGLE, function (event) { - const target = getElementFromSelector(this) + const target = SelectorEngine.getElementFromSelector(this) if (['A', 'AREA'].includes(this.tagName)) { event.preventDefault() diff --git a/js/src/offcanvas.js b/js/src/offcanvas.js index 7dd06fd..8d1feb1 100644 --- a/js/src/offcanvas.js +++ b/js/src/offcanvas.js @@ -1,23 +1,22 @@ /** * -------------------------------------------------------------------------- - * Bootstrap (v5.2.3): offcanvas.js + * Bootstrap offcanvas.js * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) * -------------------------------------------------------------------------- */ +import BaseComponent from './base-component.js' +import EventHandler from './dom/event-handler.js' +import SelectorEngine from './dom/selector-engine.js' +import Backdrop from './util/backdrop.js' +import { enableDismissTrigger } from './util/component-functions.js' +import FocusTrap from './util/focustrap.js' import { defineJQueryPlugin, - getElementFromSelector, isDisabled, isVisible -} 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 Backdrop from './util/backdrop' -import FocusTrap from './util/focustrap' -import { enableDismissTrigger } from './util/component-functions' +} from './util/index.js' +import ScrollBarHelper from './util/scrollbar.js' /** * Constants @@ -199,12 +198,12 @@ class Offcanvas extends BaseComponent { return } - if (!this._config.keyboard) { - EventHandler.trigger(this._element, EVENT_HIDE_PREVENTED) + if (this._config.keyboard) { + this.hide() return } - this.hide() + EventHandler.trigger(this._element, EVENT_HIDE_PREVENTED) }) } @@ -231,7 +230,7 @@ class Offcanvas extends BaseComponent { */ EventHandler.on(document, EVENT_CLICK_DATA_API, SELECTOR_DATA_TOGGLE, function (event) { - const target = getElementFromSelector(this) + const target = SelectorEngine.getElementFromSelector(this) if (['A', 'AREA'].includes(this.tagName)) { event.preventDefault() diff --git a/js/src/popover.js b/js/src/popover.js index 1b09dd4..612c521 100644 --- a/js/src/popover.js +++ b/js/src/popover.js @@ -1,12 +1,12 @@ /** * -------------------------------------------------------------------------- - * Bootstrap (v5.2.3): popover.js + * Bootstrap popover.js * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) * -------------------------------------------------------------------------- */ -import { defineJQueryPlugin } from './util/index' -import Tooltip from './tooltip' +import Tooltip from './tooltip.js' +import { defineJQueryPlugin } from './util/index.js' /** * Constants diff --git a/js/src/scrollspy.js b/js/src/scrollspy.js index 01aba99..69de715 100644 --- a/js/src/scrollspy.js +++ b/js/src/scrollspy.js @@ -1,14 +1,14 @@ /** * -------------------------------------------------------------------------- - * Bootstrap (v5.2.3): scrollspy.js + * Bootstrap scrollspy.js * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) * -------------------------------------------------------------------------- */ -import { defineJQueryPlugin, getElement, isDisabled, isVisible } from './util/index' -import EventHandler from './dom/event-handler' -import SelectorEngine from './dom/selector-engine' -import BaseComponent from './base-component' +import BaseComponent from './base-component.js' +import EventHandler from './dom/event-handler.js' +import SelectorEngine from './dom/selector-engine.js' +import { defineJQueryPlugin, getElement, isDisabled, isVisible } from './util/index.js' /** * Constants @@ -208,11 +208,11 @@ class ScrollSpy extends BaseComponent { continue } - const observableSection = SelectorEngine.findOne(anchor.hash, this._element) + const observableSection = SelectorEngine.findOne(decodeURI(anchor.hash), this._element) // ensure that the observableSection exists & is visible if (isVisible(observableSection)) { - this._targetLinks.set(anchor.hash, anchor) + this._targetLinks.set(decodeURI(anchor.hash), anchor) this._observableSections.set(anchor.hash, observableSection) } } diff --git a/js/src/tab.js b/js/src/tab.js index 8dc4644..d9993d5 100644 --- a/js/src/tab.js +++ b/js/src/tab.js @@ -1,14 +1,14 @@ /** * -------------------------------------------------------------------------- - * Bootstrap (v5.2.3): tab.js + * Bootstrap tab.js * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) * -------------------------------------------------------------------------- */ -import { defineJQueryPlugin, getElementFromSelector, getNextActiveElement, isDisabled } from './util/index' -import EventHandler from './dom/event-handler' -import SelectorEngine from './dom/selector-engine' -import BaseComponent from './base-component' +import BaseComponent from './base-component.js' +import EventHandler from './dom/event-handler.js' +import SelectorEngine from './dom/selector-engine.js' +import { defineJQueryPlugin, getNextActiveElement, isDisabled } from './util/index.js' /** * Constants @@ -43,7 +43,7 @@ const NOT_SELECTOR_DROPDOWN_TOGGLE = ':not(.dropdown-toggle)' const SELECTOR_TAB_PANEL = '.list-group, .nav, [role="tablist"]' const SELECTOR_OUTER = '.nav-item, .list-group-item' const SELECTOR_INNER = `.nav-link${NOT_SELECTOR_DROPDOWN_TOGGLE}, .list-group-item${NOT_SELECTOR_DROPDOWN_TOGGLE}, [role="tab"]${NOT_SELECTOR_DROPDOWN_TOGGLE}` -const SELECTOR_DATA_TOGGLE = '[data-bs-toggle="tab"], [data-bs-toggle="pill"], [data-bs-toggle="list"]' // todo:v6: could be only `tab` +const SELECTOR_DATA_TOGGLE = '[data-bs-toggle="tab"], [data-bs-toggle="pill"], [data-bs-toggle="list"]' // TODO: could only be `tab` in v6 const SELECTOR_INNER_ELEM = `${SELECTOR_INNER}, ${SELECTOR_DATA_TOGGLE}` const SELECTOR_DATA_TOGGLE_ACTIVE = `.${CLASS_NAME_ACTIVE}[data-bs-toggle="tab"], .${CLASS_NAME_ACTIVE}[data-bs-toggle="pill"], .${CLASS_NAME_ACTIVE}[data-bs-toggle="list"]` @@ -59,7 +59,7 @@ class Tab extends BaseComponent { if (!this._parent) { return - // todo: should Throw exception on v6 + // TODO: should throw exception in v6 // throw new TypeError(`${element.outerHTML} has not a valid parent ${SELECTOR_INNER_ELEM}`) } @@ -106,7 +106,7 @@ class Tab extends BaseComponent { element.classList.add(CLASS_NAME_ACTIVE) - this._activate(getElementFromSelector(element)) // Search and activate/show the proper section + this._activate(SelectorEngine.getElementFromSelector(element)) // Search and activate/show the proper section const complete = () => { if (element.getAttribute('role') !== 'tab') { @@ -133,7 +133,7 @@ class Tab extends BaseComponent { element.classList.remove(CLASS_NAME_ACTIVE) element.blur() - this._deactivate(getElementFromSelector(element)) // Search and deactivate the shown section too + this._deactivate(SelectorEngine.getElementFromSelector(element)) // Search and deactivate the shown section too const complete = () => { if (element.getAttribute('role') !== 'tab') { @@ -203,7 +203,7 @@ class Tab extends BaseComponent { } _setInitialAttributesOnTargetPanel(child) { - const target = getElementFromSelector(child) + const target = SelectorEngine.getElementFromSelector(child) if (!target) { return @@ -212,7 +212,7 @@ class Tab extends BaseComponent { this._setAttributeIfNotExists(target, 'role', 'tabpanel') if (child.id) { - this._setAttributeIfNotExists(target, 'aria-labelledby', `#${child.id}`) + this._setAttributeIfNotExists(target, 'aria-labelledby', `${child.id}`) } } diff --git a/js/src/toast.js b/js/src/toast.js index a7fe775..d5d9c0e 100644 --- a/js/src/toast.js +++ b/js/src/toast.js @@ -1,14 +1,14 @@ /** * -------------------------------------------------------------------------- - * Bootstrap (v5.2.3): toast.js + * Bootstrap toast.js * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) * -------------------------------------------------------------------------- */ -import { defineJQueryPlugin, reflow } from './util/index' -import EventHandler from './dom/event-handler' -import BaseComponent from './base-component' -import { enableDismissTrigger } from './util/component-functions' +import BaseComponent from './base-component.js' +import EventHandler from './dom/event-handler.js' +import { enableDismissTrigger } from './util/component-functions.js' +import { defineJQueryPlugin, reflow } from './util/index.js' /** * Constants diff --git a/js/src/tooltip.js b/js/src/tooltip.js index 748a0e1..1252811 100644 --- a/js/src/tooltip.js +++ b/js/src/tooltip.js @@ -1,17 +1,17 @@ /** * -------------------------------------------------------------------------- - * Bootstrap (v5.2.3): tooltip.js + * Bootstrap tooltip.js * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) * -------------------------------------------------------------------------- */ import * as Popper from '@popperjs/core' -import { defineJQueryPlugin, findShadowRoot, getElement, getUID, isRTL, noop } from './util/index' -import { DefaultAllowlist } from './util/sanitizer' -import EventHandler from './dom/event-handler' -import Manipulator from './dom/manipulator' -import BaseComponent from './base-component' -import TemplateFactory from './util/template-factory' +import BaseComponent from './base-component.js' +import EventHandler from './dom/event-handler.js' +import Manipulator from './dom/manipulator.js' +import { defineJQueryPlugin, execute, findShadowRoot, getElement, getUID, isRTL, noop } from './util/index.js' +import { DefaultAllowlist } from './util/sanitizer.js' +import TemplateFactory from './util/template-factory.js' /** * Constants @@ -62,7 +62,7 @@ const Default = { delay: 0, fallbackPlacements: ['top', 'right', 'bottom', 'left'], html: false, - offset: [0, 0], + offset: [0, 6], placement: 'top', popperConfig: null, sanitize: true, @@ -197,7 +197,7 @@ class Tooltip extends BaseComponent { return } - // todo v6 remove this OR make it optional + // TODO: v6 remove this or make it optional this._disposePopper() const tip = this._getTipElement() @@ -302,13 +302,13 @@ class Tooltip extends BaseComponent { _createTipElement(content) { const tip = this._getTemplateFactory(content).toHtml() - // todo: remove this check on v6 + // TODO: remove this check in v6 if (!tip) { return null } tip.classList.remove(CLASS_NAME_FADE, CLASS_NAME_SHOW) - // todo: on v6 the following can be achieved with CSS only + // TODO: v6 the following can be achieved with CSS only tip.classList.add(`bs-${this.constructor.NAME}-auto`) const tipId = getUID(this.constructor.NAME).toString() @@ -370,9 +370,7 @@ class Tooltip extends BaseComponent { } _createPopper(tip) { - const placement = typeof this._config.placement === 'function' ? - this._config.placement.call(this, tip, this._element) : - this._config.placement + const placement = execute(this._config.placement, [this, tip, this._element]) const attachment = AttachmentMap[placement.toUpperCase()] return Popper.createPopper(this._element, tip, this._getPopperConfig(attachment)) } @@ -392,7 +390,7 @@ class Tooltip extends BaseComponent { } _resolvePossibleFunction(arg) { - return typeof arg === 'function' ? arg.call(this._element) : arg + return execute(arg, [this._element]) } _getPopperConfig(attachment) { @@ -438,7 +436,7 @@ class Tooltip extends BaseComponent { return { ...defaultBsPopperConfig, - ...(typeof this._config.popperConfig === 'function' ? this._config.popperConfig(defaultBsPopperConfig) : this._config.popperConfig) + ...execute(this._config.popperConfig, [defaultBsPopperConfig]) } } @@ -579,9 +577,9 @@ class Tooltip extends BaseComponent { _getDelegateConfig() { const config = {} - for (const key in this._config) { - if (this.constructor.Default[key] !== this._config[key]) { - config[key] = this._config[key] + for (const [key, value] of Object.entries(this._config)) { + if (this.constructor.Default[key] !== value) { + config[key] = value } } diff --git a/js/src/util/backdrop.js b/js/src/util/backdrop.js index 78279e0..0d478e9 100644 --- a/js/src/util/backdrop.js +++ b/js/src/util/backdrop.js @@ -1,13 +1,13 @@ /** * -------------------------------------------------------------------------- - * Bootstrap (v5.2.3): util/backdrop.js + * Bootstrap util/backdrop.js * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) * -------------------------------------------------------------------------- */ -import EventHandler from '../dom/event-handler' -import { execute, executeAfterTransition, getElement, reflow } from './index' -import Config from './config' +import EventHandler from '../dom/event-handler.js' +import Config from './config.js' +import { execute, executeAfterTransition, getElement, reflow } from './index.js' /** * Constants diff --git a/js/src/util/component-functions.js b/js/src/util/component-functions.js index c2f99cc..4be828f 100644 --- a/js/src/util/component-functions.js +++ b/js/src/util/component-functions.js @@ -1,12 +1,13 @@ /** * -------------------------------------------------------------------------- - * Bootstrap (v5.2.3): util/component-functions.js + * Bootstrap 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' +import EventHandler from '../dom/event-handler.js' +import SelectorEngine from '../dom/selector-engine.js' +import { isDisabled } from './index.js' const enableDismissTrigger = (component, method = 'hide') => { const clickEvent = `click.dismiss${component.EVENT_KEY}` @@ -21,7 +22,7 @@ const enableDismissTrigger = (component, method = 'hide') => { return } - const target = getElementFromSelector(this) || this.closest(`.${name}`) + const target = SelectorEngine.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 diff --git a/js/src/util/config.js b/js/src/util/config.js index 1205905..a2b4bfb 100644 --- a/js/src/util/config.js +++ b/js/src/util/config.js @@ -1,12 +1,12 @@ /** * -------------------------------------------------------------------------- - * Bootstrap (v5.2.3): util/config.js + * Bootstrap util/config.js * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) * -------------------------------------------------------------------------- */ -import { isElement, toType } from './index' -import Manipulator from '../dom/manipulator' +import Manipulator from '../dom/manipulator.js' +import { isElement, toType } from './index.js' /** * Class definition @@ -49,8 +49,7 @@ class Config { } _typeCheckConfig(config, configTypes = this.constructor.DefaultType) { - for (const property of Object.keys(configTypes)) { - const expectedTypes = configTypes[property] + for (const [property, expectedTypes] of Object.entries(configTypes)) { const value = config[property] const valueType = isElement(value) ? 'element' : toType(value) diff --git a/js/src/util/focustrap.js b/js/src/util/focustrap.js index ef69166..158f3d1 100644 --- a/js/src/util/focustrap.js +++ b/js/src/util/focustrap.js @@ -1,13 +1,13 @@ /** * -------------------------------------------------------------------------- - * Bootstrap (v5.2.3): util/focustrap.js + * Bootstrap util/focustrap.js * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) * -------------------------------------------------------------------------- */ -import EventHandler from '../dom/event-handler' -import SelectorEngine from '../dom/selector-engine' -import Config from './config' +import EventHandler from '../dom/event-handler.js' +import SelectorEngine from '../dom/selector-engine.js' +import Config from './config.js' /** * Constants diff --git a/js/src/util/index.js b/js/src/util/index.js index 297e571..68b8d89 100644 --- a/js/src/util/index.js +++ b/js/src/util/index.js @@ -1,6 +1,6 @@ /** * -------------------------------------------------------------------------- - * Bootstrap (v5.2.3): util/index.js + * Bootstrap util/index.js * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) * -------------------------------------------------------------------------- */ @@ -9,6 +9,20 @@ const MAX_UID = 1_000_000 const MILLISECONDS_MULTIPLIER = 1000 const TRANSITION_END = 'transitionend' +/** + * Properly escape IDs selectors to handle weird IDs + * @param {string} selector + * @returns {string} + */ +const parseSelector = selector => { + if (selector && window.CSS && window.CSS.escape) { + // document.querySelector needs escaping to handle IDs (html5+) containing for instance / + selector = selector.replace(/#([^\s"#']+)/g, (match, id) => `#${CSS.escape(id)}`) + } + + return selector +} + // Shout-out Angus Croll (https://goo.gl/pxwQGp) const toType = object => { if (object === null || object === undefined) { @@ -30,47 +44,6 @@ const getUID = prefix => { return prefix } -const getSelector = element => { - let selector = element.getAttribute('data-bs-target') - - if (!selector || selector === '#') { - let hrefAttribute = element.getAttribute('href') - - // The only valid content that could double as a selector are IDs or classes, - // so everything starting with `#` or `.`. If a "real" URL is used as the selector, - // `document.querySelector` will rightfully complain it is invalid. - // See https://github.com/twbs/bootstrap/issues/32273 - if (!hrefAttribute || (!hrefAttribute.includes('#') && !hrefAttribute.startsWith('.'))) { - return null - } - - // Just in case some CMS puts out a full URL with the anchor appended - if (hrefAttribute.includes('#') && !hrefAttribute.startsWith('#')) { - hrefAttribute = `#${hrefAttribute.split('#')[1]}` - } - - selector = hrefAttribute && hrefAttribute !== '#' ? hrefAttribute.trim() : null - } - - return selector -} - -const getSelectorFromElement = element => { - const selector = getSelector(element) - - if (selector) { - return document.querySelector(selector) ? selector : null - } - - return null -} - -const getElementFromSelector = element => { - const selector = getSelector(element) - - return selector ? document.querySelector(selector) : null -} - const getTransitionDurationFromElement = element => { if (!element) { return 0 @@ -117,7 +90,7 @@ const getElement = object => { } if (typeof object === 'string' && object.length > 0) { - return document.querySelector(object) + return document.querySelector(parseSelector(object)) } return null @@ -249,10 +222,8 @@ const defineJQueryPlugin = plugin => { }) } -const execute = callback => { - if (typeof callback === 'function') { - callback() - } +const execute = (possibleCallback, args = [], defaultValue = possibleCallback) => { + return typeof possibleCallback === 'function' ? possibleCallback(...args) : defaultValue } const executeAfterTransition = (callback, transitionElement, waitForTransition = true) => { @@ -318,10 +289,8 @@ export { executeAfterTransition, findShadowRoot, getElement, - getElementFromSelector, getjQuery, getNextActiveElement, - getSelectorFromElement, getTransitionDurationFromElement, getUID, isDisabled, @@ -330,6 +299,7 @@ export { isVisible, noop, onDOMContentLoaded, + parseSelector, reflow, triggerTransitionEnd, toType diff --git a/js/src/util/sanitizer.js b/js/src/util/sanitizer.js index 5328691..d2b0808 100644 --- a/js/src/util/sanitizer.js +++ b/js/src/util/sanitizer.js @@ -1,53 +1,13 @@ /** * -------------------------------------------------------------------------- - * Bootstrap (v5.2.3): util/sanitizer.js + * Bootstrap util/sanitizer.js * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) * -------------------------------------------------------------------------- */ -const uriAttributes = new Set([ - 'background', - 'cite', - 'href', - 'itemtype', - 'longdesc', - 'poster', - 'src', - 'xlink:href' -]) - +// js-docs-start allow-list const ARIA_ATTRIBUTE_PATTERN = /^aria-[\w-]*$/i -/** - * A pattern that recognizes a commonly useful subset of URLs that are safe. - * - * Shout-out to Angular https://github.com/angular/angular/blob/12.2.x/packages/core/src/sanitization/url_sanitizer.ts - */ -const SAFE_URL_PATTERN = /^(?:(?:https?|mailto|ftp|tel|file|sms):|[^#&/:?]*(?:[#/?]|$))/i - -/** - * A pattern that matches safe data URLs. Only matches image, video and audio types. - * - * Shout-out to Angular https://github.com/angular/angular/blob/12.2.x/packages/core/src/sanitization/url_sanitizer.ts - */ -const DATA_URL_PATTERN = /^data:(?:image\/(?:bmp|gif|jpeg|jpg|png|tiff|webp)|video\/(?:mpeg|mp4|ogg|webm)|audio\/(?:mp3|oga|ogg|opus));base64,[\d+/a-z]+=*$/i - -const allowedAttribute = (attribute, allowedAttributeList) => { - const attributeName = attribute.nodeName.toLowerCase() - - if (allowedAttributeList.includes(attributeName)) { - if (uriAttributes.has(attributeName)) { - return Boolean(SAFE_URL_PATTERN.test(attribute.nodeValue) || DATA_URL_PATTERN.test(attribute.nodeValue)) - } - - return true - } - - // Check if a regular expression validates the attribute. - return allowedAttributeList.filter(attributeRegex => attributeRegex instanceof RegExp) - .some(regex => regex.test(attributeName)) -} - export const DefaultAllowlist = { // Global attributes allowed on any supplied element below. '*': ['class', 'dir', 'id', 'lang', 'role', ARIA_ATTRIBUTE_PATTERN], @@ -81,6 +41,43 @@ export const DefaultAllowlist = { u: [], ul: [] } +// js-docs-end allow-list + +const uriAttributes = new Set([ + 'background', + 'cite', + 'href', + 'itemtype', + 'longdesc', + 'poster', + 'src', + 'xlink:href' +]) + +/** + * A pattern that recognizes URLs that are safe wrt. XSS in URL navigation + * contexts. + * + * Shout-out to Angular https://github.com/angular/angular/blob/15.2.8/packages/core/src/sanitization/url_sanitizer.ts#L38 + */ +// eslint-disable-next-line unicorn/better-regex +const SAFE_URL_PATTERN = /^(?!javascript:)(?:[a-z0-9+.-]+:|[^&:/?#]*(?:[/?#]|$))/i + +const allowedAttribute = (attribute, allowedAttributeList) => { + const attributeName = attribute.nodeName.toLowerCase() + + if (allowedAttributeList.includes(attributeName)) { + if (uriAttributes.has(attributeName)) { + return Boolean(SAFE_URL_PATTERN.test(attribute.nodeValue)) + } + + return true + } + + // Check if a regular expression validates the attribute. + return allowedAttributeList.filter(attributeRegex => attributeRegex instanceof RegExp) + .some(regex => regex.test(attributeName)) +} export function sanitizeHtml(unsafeHtml, allowList, sanitizeFunction) { if (!unsafeHtml.length) { @@ -100,7 +97,6 @@ export function sanitizeHtml(unsafeHtml, allowList, sanitizeFunction) { if (!Object.keys(allowList).includes(elementName)) { element.remove() - continue } diff --git a/js/src/util/scrollbar.js b/js/src/util/scrollbar.js index 5cac7b6..413f178 100644 --- a/js/src/util/scrollbar.js +++ b/js/src/util/scrollbar.js @@ -1,13 +1,13 @@ /** * -------------------------------------------------------------------------- - * Bootstrap (v5.2.3): util/scrollBar.js + * Bootstrap 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' +import Manipulator from '../dom/manipulator.js' +import SelectorEngine from '../dom/selector-engine.js' +import { isElement } from './index.js' /** * Constants diff --git a/js/src/util/swipe.js b/js/src/util/swipe.js index 7126360..d2f7087 100644 --- a/js/src/util/swipe.js +++ b/js/src/util/swipe.js @@ -1,13 +1,13 @@ /** * -------------------------------------------------------------------------- - * Bootstrap (v5.2.3): util/swipe.js + * Bootstrap util/swipe.js * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) * -------------------------------------------------------------------------- */ -import Config from './config' -import EventHandler from '../dom/event-handler' -import { execute } from './index' +import EventHandler from '../dom/event-handler.js' +import Config from './config.js' +import { execute } from './index.js' /** * Constants diff --git a/js/src/util/template-factory.js b/js/src/util/template-factory.js index cf402fa..f73589b 100644 --- a/js/src/util/template-factory.js +++ b/js/src/util/template-factory.js @@ -1,14 +1,14 @@ /** * -------------------------------------------------------------------------- - * Bootstrap (v5.2.3): util/template-factory.js + * Bootstrap util/template-factory.js * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) * -------------------------------------------------------------------------- */ -import { DefaultAllowlist, sanitizeHtml } from './sanitizer' -import { getElement, isElement } from '../util/index' -import SelectorEngine from '../dom/selector-engine' -import Config from './config' +import SelectorEngine from '../dom/selector-engine.js' +import Config from './config.js' +import { DefaultAllowlist, sanitizeHtml } from './sanitizer.js' +import { execute, getElement, isElement } from './index.js' /** * Constants @@ -143,7 +143,7 @@ class TemplateFactory extends Config { } _resolvePossibleFunction(arg) { - return typeof arg === 'function' ? arg(this) : arg + return execute(arg, [this]) } _putElementInTemplate(element, templateElement) { -- cgit v1.2.3