diff --git a/.bundlewatch.config.json b/.bundlewatch.config.json index 4bfffbab4..525d29f6a 100644 --- a/.bundlewatch.config.json +++ b/.bundlewatch.config.json @@ -38,7 +38,7 @@ }, { "path": "./dist/js/bootstrap.bundle.min.js", - "maxSize": "22 kB" + "maxSize": "22.25 kB" }, { "path": "./dist/js/bootstrap.esm.js", diff --git a/js/src/alert.js b/js/src/alert.js index 724cda017..f1f612232 100644 --- a/js/src/alert.js +++ b/js/src/alert.js @@ -15,6 +15,7 @@ import { } from './util/index' import Data from './dom/data' import EventHandler from './dom/event-handler' +import BaseComponent from './base-component' /** * ------------------------------------------------------------------------ @@ -23,7 +24,6 @@ import EventHandler from './dom/event-handler' */ const NAME = 'alert' -const VERSION = '5.0.0-alpha3' const DATA_KEY = 'bs.alert' const EVENT_KEY = `.${DATA_KEY}` const DATA_API_KEY = '.data-api' @@ -44,19 +44,11 @@ const CLASSNAME_SHOW = 'show' * ------------------------------------------------------------------------ */ -class Alert { - constructor(element) { - this._element = element - - if (this._element) { - Data.setData(element, DATA_KEY, this) - } - } - +class Alert extends BaseComponent { // Getters - static get VERSION() { - return VERSION + static get DATA_KEY() { + return DATA_KEY } // Public @@ -72,11 +64,6 @@ class Alert { this._removeElement(rootElement) } - dispose() { - Data.removeData(this._element, DATA_KEY) - this._element = null - } - // Private _getRootElement(element) { @@ -134,10 +121,6 @@ class Alert { alertInstance.close(this) } } - - static getInstance(element) { - return Data.getData(element, DATA_KEY) - } } /** diff --git a/js/src/base-component.js b/js/src/base-component.js new file mode 100644 index 000000000..776a0052b --- /dev/null +++ b/js/src/base-component.js @@ -0,0 +1,44 @@ +/** + * -------------------------------------------------------------------------- + * Bootstrap (v5.0.0-alpha3): base-component.js + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + * -------------------------------------------------------------------------- + */ + +import Data from './dom/data' + +/** + * ------------------------------------------------------------------------ + * Constants + * ------------------------------------------------------------------------ + */ + +const VERSION = '5.0.0-alpha3' + +class BaseComponent { + constructor(element) { + if (!element) { + return + } + + this._element = element + Data.setData(element, this.constructor.DATA_KEY, this) + } + + dispose() { + Data.removeData(this._element, this.constructor.DATA_KEY) + this._element = null + } + + /** Static */ + + static getInstance(element) { + return Data.getData(element, this.DATA_KEY) + } + + static get VERSION() { + return VERSION + } +} + +export default BaseComponent diff --git a/js/src/button.js b/js/src/button.js index 42376943d..240995564 100644 --- a/js/src/button.js +++ b/js/src/button.js @@ -8,6 +8,7 @@ import { getjQuery, onDOMContentLoaded } from './util/index' import Data from './dom/data' import EventHandler from './dom/event-handler' +import BaseComponent from './base-component' /** * ------------------------------------------------------------------------ @@ -16,7 +17,6 @@ import EventHandler from './dom/event-handler' */ const NAME = 'button' -const VERSION = '5.0.0-alpha3' const DATA_KEY = 'bs.button' const EVENT_KEY = `.${DATA_KEY}` const DATA_API_KEY = '.data-api' @@ -33,16 +33,11 @@ const EVENT_CLICK_DATA_API = `click${EVENT_KEY}${DATA_API_KEY}` * ------------------------------------------------------------------------ */ -class Button { - constructor(element) { - this._element = element - Data.setData(element, DATA_KEY, this) - } - +class Button extends BaseComponent { // Getters - static get VERSION() { - return VERSION + static get DATA_KEY() { + return DATA_KEY } // Public @@ -52,11 +47,6 @@ class Button { this._element.setAttribute('aria-pressed', this._element.classList.toggle(CLASS_NAME_ACTIVE)) } - dispose() { - Data.removeData(this._element, DATA_KEY) - this._element = null - } - // Static static jQueryInterface(config) { @@ -72,10 +62,6 @@ class Button { } }) } - - static getInstance(element) { - return Data.getData(element, DATA_KEY) - } } /** diff --git a/js/src/carousel.js b/js/src/carousel.js index c663efbea..6443ef094 100644 --- a/js/src/carousel.js +++ b/js/src/carousel.js @@ -21,6 +21,7 @@ import Data from './dom/data' import EventHandler from './dom/event-handler' import Manipulator from './dom/manipulator' import SelectorEngine from './dom/selector-engine' +import BaseComponent from './base-component' /** * ------------------------------------------------------------------------ @@ -29,7 +30,6 @@ import SelectorEngine from './dom/selector-engine' */ const NAME = 'carousel' -const VERSION = '5.0.0-alpha3' const DATA_KEY = 'bs.carousel' const EVENT_KEY = `.${DATA_KEY}` const DATA_API_KEY = '.data-api' @@ -104,8 +104,10 @@ const PointerType = { * Class Definition * ------------------------------------------------------------------------ */ -class Carousel { +class Carousel extends BaseComponent { constructor(element, config) { + super(element) + this._items = null this._interval = null this._activeElement = null @@ -116,25 +118,23 @@ class Carousel { this.touchDeltaX = 0 this._config = this._getConfig(config) - this._element = element this._indicatorsElement = SelectorEngine.findOne(SELECTOR_INDICATORS, this._element) this._touchSupported = 'ontouchstart' in document.documentElement || navigator.maxTouchPoints > 0 this._pointerEvent = Boolean(window.PointerEvent) this._addEventListeners() - Data.setData(element, DATA_KEY, this) } // Getters - static get VERSION() { - return VERSION - } - static get Default() { return Default } + static get DATA_KEY() { + return DATA_KEY + } + // Public next() { @@ -218,12 +218,11 @@ class Carousel { } dispose() { + super.dispose() EventHandler.off(this._element, EVENT_KEY) - Data.removeData(this._element, DATA_KEY) this._items = null this._config = null - this._element = null this._interval = null this._isPaused = null this._isSliding = null @@ -590,10 +589,6 @@ class Carousel { event.preventDefault() } - - static getInstance(element) { - return Data.getData(element, DATA_KEY) - } } /** diff --git a/js/src/collapse.js b/js/src/collapse.js index 2ddcb0c04..feff6c917 100644 --- a/js/src/collapse.js +++ b/js/src/collapse.js @@ -21,6 +21,7 @@ import Data from './dom/data' import EventHandler from './dom/event-handler' import Manipulator from './dom/manipulator' import SelectorEngine from './dom/selector-engine' +import BaseComponent from './base-component' /** * ------------------------------------------------------------------------ @@ -29,7 +30,6 @@ import SelectorEngine from './dom/selector-engine' */ const NAME = 'collapse' -const VERSION = '5.0.0-alpha3' const DATA_KEY = 'bs.collapse' const EVENT_KEY = `.${DATA_KEY}` const DATA_API_KEY = '.data-api' @@ -67,10 +67,11 @@ const SELECTOR_DATA_TOGGLE = '[data-bs-toggle="collapse"]' * ------------------------------------------------------------------------ */ -class Collapse { +class Collapse extends BaseComponent { constructor(element, config) { + super(element) + this._isTransitioning = false - this._element = element this._config = this._getConfig(config) this._triggerArray = SelectorEngine.find( `${SELECTOR_DATA_TOGGLE}[href="#${element.id}"],` + @@ -100,20 +101,18 @@ class Collapse { if (this._config.toggle) { this.toggle() } - - Data.setData(element, DATA_KEY, this) } // Getters - static get VERSION() { - return VERSION - } - static get Default() { return Default } + static get DATA_KEY() { + return DATA_KEY + } + // Public toggle() { @@ -266,11 +265,9 @@ class Collapse { } dispose() { - Data.removeData(this._element, DATA_KEY) - + super.dispose() this._config = null this._parent = null - this._element = null this._triggerArray = null this._isTransitioning = null } @@ -368,10 +365,6 @@ class Collapse { Collapse.collapseInterface(this, config) }) } - - static getInstance(element) { - return Data.getData(element, DATA_KEY) - } } /** diff --git a/js/src/dropdown.js b/js/src/dropdown.js index b9f5a4cd3..5b8ae0645 100644 --- a/js/src/dropdown.js +++ b/js/src/dropdown.js @@ -19,6 +19,7 @@ import EventHandler from './dom/event-handler' import Manipulator from './dom/manipulator' import Popper from 'popper.js' import SelectorEngine from './dom/selector-engine' +import BaseComponent from './base-component' /** * ------------------------------------------------------------------------ @@ -27,7 +28,6 @@ import SelectorEngine from './dom/selector-engine' */ const NAME = 'dropdown' -const VERSION = '5.0.0-alpha3' const DATA_KEY = 'bs.dropdown' const EVENT_KEY = `.${DATA_KEY}` const DATA_API_KEY = '.data-api' @@ -96,24 +96,20 @@ const DefaultType = { * ------------------------------------------------------------------------ */ -class Dropdown { +class Dropdown extends BaseComponent { constructor(element, config) { - this._element = element + super(element) + this._popper = null this._config = this._getConfig(config) this._menu = this._getMenuElement() this._inNavbar = this._detectNavbar() this._addEventListeners() - Data.setData(element, DATA_KEY, this) } // Getters - static get VERSION() { - return VERSION - } - static get Default() { return Default } @@ -122,6 +118,10 @@ class Dropdown { return DefaultType } + static get DATA_KEY() { + return DATA_KEY + } + // Public toggle() { @@ -229,9 +229,8 @@ class Dropdown { } dispose() { - Data.removeData(this._element, DATA_KEY) + super.dispose() EventHandler.off(this._element, EVENT_KEY) - this._element = null this._menu = null if (this._popper) { this._popper.destroy() @@ -489,10 +488,6 @@ class Dropdown { items[index].focus() } - - static getInstance(element) { - return Data.getData(element, DATA_KEY) - } } /** diff --git a/js/src/modal.js b/js/src/modal.js index 4309cbd9a..fdca48213 100644 --- a/js/src/modal.js +++ b/js/src/modal.js @@ -20,6 +20,7 @@ import Data from './dom/data' import EventHandler from './dom/event-handler' import Manipulator from './dom/manipulator' import SelectorEngine from './dom/selector-engine' +import BaseComponent from './base-component' /** * ------------------------------------------------------------------------ @@ -28,7 +29,6 @@ import SelectorEngine from './dom/selector-engine' */ const NAME = 'modal' -const VERSION = '5.0.0-alpha3' const DATA_KEY = 'bs.modal' const EVENT_KEY = `.${DATA_KEY}` const DATA_API_KEY = '.data-api' @@ -37,15 +37,13 @@ const ESCAPE_KEY = 'Escape' const Default = { backdrop: true, keyboard: true, - focus: true, - show: true + focus: true } const DefaultType = { backdrop: '(boolean|string)', keyboard: 'boolean', - focus: 'boolean', - show: 'boolean' + focus: 'boolean' } const EVENT_HIDE = `hide${EVENT_KEY}` @@ -81,10 +79,11 @@ const SELECTOR_STICKY_CONTENT = '.sticky-top' * ------------------------------------------------------------------------ */ -class Modal { +class Modal extends BaseComponent { constructor(element, config) { + super(element) + this._config = this._getConfig(config) - this._element = element this._dialog = SelectorEngine.findOne(SELECTOR_DIALOG, element) this._backdrop = null this._isShown = false @@ -92,19 +91,18 @@ class Modal { this._ignoreBackdropClick = false this._isTransitioning = false this._scrollbarWidth = 0 - Data.setData(element, DATA_KEY, this) } // Getters - static get VERSION() { - return VERSION - } - static get Default() { return Default } + static get DATA_KEY() { + return DATA_KEY + } + // Public toggle(relatedTarget) { @@ -201,6 +199,8 @@ class Modal { [window, this._element, this._dialog] .forEach(htmlElement => EventHandler.off(htmlElement, EVENT_KEY)) + super.dispose() + /** * `document` has 2 events `EVENT_FOCUSIN` and `EVENT_CLICK_DATA_API` * Do not move `document` in `htmlElements` array @@ -208,10 +208,7 @@ class Modal { */ EventHandler.off(document, EVENT_FOCUSIN) - Data.removeData(this._element, DATA_KEY) - this._config = null - this._element = null this._dialog = null this._backdrop = null this._isShown = null @@ -558,15 +555,9 @@ class Modal { } data[config](relatedTarget) - } else if (_config.show) { - data.show(relatedTarget) } }) } - - static getInstance(element) { - return Data.getData(element, DATA_KEY) - } } /** diff --git a/js/src/popover.js b/js/src/popover.js index 46b693e69..97db9a3f7 100644 --- a/js/src/popover.js +++ b/js/src/popover.js @@ -17,7 +17,6 @@ import Tooltip from './tooltip' */ const NAME = 'popover' -const VERSION = '5.0.0-alpha3' const DATA_KEY = 'bs.popover' const EVENT_KEY = `.${DATA_KEY}` const CLASS_PREFIX = 'bs-popover' @@ -67,10 +66,6 @@ const SELECTOR_CONTENT = '.popover-body' class Popover extends Tooltip { // Getters - static get VERSION() { - return VERSION - } - static get Default() { return Default } @@ -108,7 +103,7 @@ class Popover extends Tooltip { this.setElementContent(SelectorEngine.findOne(SELECTOR_TITLE, tip), this.getTitle()) let content = this._getContent() if (typeof content === 'function') { - content = content.call(this.element) + content = content.call(this._element) } this.setElementContent(SelectorEngine.findOne(SELECTOR_CONTENT, tip), content) @@ -123,7 +118,7 @@ class Popover extends Tooltip { } _getContent() { - return this.element.getAttribute('data-bs-content') || + return this._element.getAttribute('data-bs-content') || this.config.content } @@ -161,10 +156,6 @@ class Popover extends Tooltip { } }) } - - static getInstance(element) { - return Data.getData(element, DATA_KEY) - } } /** diff --git a/js/src/scrollspy.js b/js/src/scrollspy.js index de456e080..e3e5e76b9 100644 --- a/js/src/scrollspy.js +++ b/js/src/scrollspy.js @@ -17,6 +17,7 @@ import Data from './dom/data' import EventHandler from './dom/event-handler' import Manipulator from './dom/manipulator' import SelectorEngine from './dom/selector-engine' +import BaseComponent from './base-component' /** * ------------------------------------------------------------------------ @@ -25,7 +26,6 @@ import SelectorEngine from './dom/selector-engine' */ const NAME = 'scrollspy' -const VERSION = '5.0.0-alpha3' const DATA_KEY = 'bs.scrollspy' const EVENT_KEY = `.${DATA_KEY}` const DATA_API_KEY = '.data-api' @@ -66,9 +66,9 @@ const METHOD_POSITION = 'position' * ------------------------------------------------------------------------ */ -class ScrollSpy { +class ScrollSpy extends BaseComponent { constructor(element, config) { - this._element = element + super(element) this._scrollElement = element.tagName === 'BODY' ? window : 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}` @@ -81,20 +81,18 @@ class ScrollSpy { this.refresh() this._process() - - Data.setData(element, DATA_KEY, this) } // Getters - static get VERSION() { - return VERSION - } - static get Default() { return Default } + static get DATA_KEY() { + return DATA_KEY + } + // Public refresh() { @@ -141,10 +139,9 @@ class ScrollSpy { } dispose() { - Data.removeData(this._element, DATA_KEY) + super.dispose() EventHandler.off(this._scrollElement, EVENT_KEY) - this._element = null this._scrollElement = null this._config = null this._selector = null @@ -301,10 +298,6 @@ class ScrollSpy { } }) } - - static getInstance(element) { - return Data.getData(element, DATA_KEY) - } } /** diff --git a/js/src/tab.js b/js/src/tab.js index 56269e2fa..0e9755ea0 100644 --- a/js/src/tab.js +++ b/js/src/tab.js @@ -17,6 +17,7 @@ import { import Data from './dom/data' import EventHandler from './dom/event-handler' import SelectorEngine from './dom/selector-engine' +import BaseComponent from './base-component' /** * ------------------------------------------------------------------------ @@ -25,7 +26,6 @@ import SelectorEngine from './dom/selector-engine' */ const NAME = 'tab' -const VERSION = '5.0.0-alpha3' const DATA_KEY = 'bs.tab' const EVENT_KEY = `.${DATA_KEY}` const DATA_API_KEY = '.data-api' @@ -56,17 +56,11 @@ const SELECTOR_DROPDOWN_ACTIVE_CHILD = ':scope > .dropdown-menu .active' * ------------------------------------------------------------------------ */ -class Tab { - constructor(element) { - this._element = element - - Data.setData(this._element, DATA_KEY, this) - } - +class Tab extends BaseComponent { // Getters - static get VERSION() { - return VERSION + static get DATA_KEY() { + return DATA_KEY } // Public @@ -127,11 +121,6 @@ class Tab { } } - dispose() { - Data.removeData(this._element, DATA_KEY) - this._element = null - } - // Private _activate(element, container, callback) { @@ -217,10 +206,6 @@ class Tab { } }) } - - static getInstance(element) { - return Data.getData(element, DATA_KEY) - } } /** diff --git a/js/src/toast.js b/js/src/toast.js index 1e50b0669..30df4606a 100644 --- a/js/src/toast.js +++ b/js/src/toast.js @@ -17,6 +17,7 @@ import { import Data from './dom/data' import EventHandler from './dom/event-handler' import Manipulator from './dom/manipulator' +import BaseComponent from './base-component' /** * ------------------------------------------------------------------------ @@ -25,7 +26,6 @@ import Manipulator from './dom/manipulator' */ const NAME = 'toast' -const VERSION = '5.0.0-alpha3' const DATA_KEY = 'bs.toast' const EVENT_KEY = `.${DATA_KEY}` @@ -60,21 +60,17 @@ const SELECTOR_DATA_DISMISS = '[data-bs-dismiss="toast"]' * ------------------------------------------------------------------------ */ -class Toast { +class Toast extends BaseComponent { constructor(element, config) { - this._element = element + super(element) + this._config = this._getConfig(config) this._timeout = null this._setListeners() - Data.setData(element, DATA_KEY, this) } // Getters - static get VERSION() { - return VERSION - } - static get DefaultType() { return DefaultType } @@ -83,6 +79,10 @@ class Toast { return Default } + static get DATA_KEY() { + return DATA_KEY + } + // Public show() { @@ -159,9 +159,8 @@ class Toast { } EventHandler.off(this._element, EVENT_CLICK_DISMISS) - Data.removeData(this._element, DATA_KEY) - this._element = null + super.dispose() this._config = null } @@ -208,10 +207,6 @@ class Toast { } }) } - - static getInstance(element) { - return Data.getData(element, DATA_KEY) - } } /** diff --git a/js/src/tooltip.js b/js/src/tooltip.js index b2495a3e1..62b338839 100644 --- a/js/src/tooltip.js +++ b/js/src/tooltip.js @@ -26,6 +26,7 @@ import EventHandler from './dom/event-handler' import Manipulator from './dom/manipulator' import Popper from 'popper.js' import SelectorEngine from './dom/selector-engine' +import BaseComponent from './base-component' /** * ------------------------------------------------------------------------ @@ -34,7 +35,6 @@ import SelectorEngine from './dom/selector-engine' */ const NAME = 'tooltip' -const VERSION = '5.0.0-alpha3' const DATA_KEY = 'bs.tooltip' const EVENT_KEY = `.${DATA_KEY}` const CLASS_PREFIX = 'bs-tooltip' @@ -54,6 +54,7 @@ const DefaultType = { container: '(string|element|boolean)', fallbackPlacement: '(string|array)', boundary: '(string|element)', + customClass: '(string|function)', sanitize: 'boolean', sanitizeFn: '(null|function)', allowList: 'object', @@ -83,6 +84,7 @@ const Default = { container: false, fallbackPlacement: 'flip', boundary: 'scrollParent', + customClass: '', sanitize: true, sanitizeFn: null, allowList: DefaultAllowlist, @@ -122,12 +124,14 @@ const TRIGGER_MANUAL = 'manual' * ------------------------------------------------------------------------ */ -class Tooltip { +class Tooltip extends BaseComponent { constructor(element, config) { if (typeof Popper === 'undefined') { throw new TypeError('Bootstrap\'s tooltips require Popper (https://popper.js.org)') } + super(element) + // private this._isEnabled = true this._timeout = 0 @@ -136,20 +140,14 @@ class Tooltip { this._popper = null // Protected - this.element = element this.config = this._getConfig(config) this.tip = null this._setListeners() - Data.setData(element, this.constructor.DATA_KEY, this) } // Getters - static get VERSION() { - return VERSION - } - static get Default() { return Default } @@ -225,10 +223,8 @@ class Tooltip { dispose() { clearTimeout(this._timeout) - Data.removeData(this.element, this.constructor.DATA_KEY) - - 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, this.constructor.EVENT_KEY) + EventHandler.off(this._element.closest(`.${CLASS_NAME_MODAL}`), 'hide.bs.modal', this._hideModalHandler) if (this.tip) { this.tip.parentNode.removeChild(this.tip) @@ -243,22 +239,22 @@ class Tooltip { } this._popper = null - this.element = null this.config = null this.tip = null + super.dispose() } show() { - if (this.element.style.display === 'none') { + if (this._element.style.display === 'none') { throw new Error('Please use show on visible elements') } if (this.isWithContent() && this._isEnabled) { - const showEvent = EventHandler.trigger(this.element, this.constructor.Event.SHOW) - const shadowRoot = findShadowRoot(this.element) + const showEvent = EventHandler.trigger(this._element, this.constructor.Event.SHOW) + const shadowRoot = findShadowRoot(this._element) const isInTheDom = shadowRoot === null ? - this.element.ownerDocument.documentElement.contains(this.element) : - shadowRoot.contains(this.element) + this._element.ownerDocument.documentElement.contains(this._element) : + shadowRoot.contains(this._element) if (showEvent.defaultPrevented || !isInTheDom) { return @@ -268,7 +264,7 @@ class Tooltip { const tipId = getUID(this.constructor.NAME) tip.setAttribute('id', tipId) - this.element.setAttribute('aria-describedby', tipId) + this._element.setAttribute('aria-describedby', tipId) this.setContent() @@ -277,7 +273,7 @@ class Tooltip { } const placement = typeof this.config.placement === 'function' ? - this.config.placement.call(this, tip, this.element) : + this.config.placement.call(this, tip, this._element) : this.config.placement const attachment = this._getAttachment(placement) @@ -286,16 +282,21 @@ class Tooltip { const container = this._getContainer() Data.setData(tip, this.constructor.DATA_KEY, this) - if (!this.element.ownerDocument.documentElement.contains(this.tip)) { + if (!this._element.ownerDocument.documentElement.contains(this.tip)) { container.appendChild(tip) } - EventHandler.trigger(this.element, this.constructor.Event.INSERTED) + EventHandler.trigger(this._element, this.constructor.Event.INSERTED) - this._popper = new Popper(this.element, tip, this._getPopperConfig(attachment)) + this._popper = new Popper(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 + if (customClass) { + tip.classList.add(...customClass.split(' ')) + } + // If this is a touch-enabled device we add extra // empty mouseover listeners to the body's immediate children; // only needed because of broken event delegation on iOS @@ -314,7 +315,7 @@ class Tooltip { const prevHoverState = this._hoverState this._hoverState = null - EventHandler.trigger(this.element, this.constructor.Event.SHOWN) + EventHandler.trigger(this._element, this.constructor.Event.SHOWN) if (prevHoverState === HOVER_STATE_OUT) { this._leave(null, this) @@ -343,12 +344,12 @@ class Tooltip { } this._cleanTipClass() - this.element.removeAttribute('aria-describedby') - EventHandler.trigger(this.element, this.constructor.Event.HIDDEN) + this._element.removeAttribute('aria-describedby') + EventHandler.trigger(this._element, this.constructor.Event.HIDDEN) this._popper.destroy() } - const hideEvent = EventHandler.trigger(this.element, this.constructor.Event.HIDE) + const hideEvent = EventHandler.trigger(this._element, this.constructor.Event.HIDE) if (hideEvent.defaultPrevented) { return } @@ -443,11 +444,11 @@ class Tooltip { } getTitle() { - let title = this.element.getAttribute('data-bs-original-title') + 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.call(this._element) : this.config.title } @@ -496,7 +497,7 @@ class Tooltip { offset.fn = data => { data.offsets = { ...data.offsets, - ...(this.config.offset(data.offsets, this.element) || {}) + ...(this.config.offset(data.offsets, this._element) || {}) } return data @@ -529,7 +530,7 @@ class Tooltip { triggers.forEach(trigger => { if (trigger === 'click') { - EventHandler.on(this.element, + EventHandler.on(this._element, this.constructor.Event.CLICK, this.config.selector, event => this.toggle(event) @@ -542,12 +543,12 @@ class Tooltip { this.constructor.Event.MOUSELEAVE : this.constructor.Event.FOCUSOUT - EventHandler.on(this.element, + EventHandler.on(this._element, eventIn, this.config.selector, event => this._enter(event) ) - EventHandler.on(this.element, + EventHandler.on(this._element, eventOut, this.config.selector, event => this._leave(event) @@ -556,12 +557,12 @@ class Tooltip { }) this._hideModalHandler = () => { - if (this.element) { + if (this._element) { this.hide() } } - EventHandler.on(this.element.closest(`.${CLASS_NAME_MODAL}`), + EventHandler.on(this._element.closest(`.${CLASS_NAME_MODAL}`), 'hide.bs.modal', this._hideModalHandler ) @@ -578,12 +579,16 @@ class Tooltip { } _fixTitle() { - const title = this.element.getAttribute('title') - const originalTitleType = typeof this.element.getAttribute('data-bs-original-title') + const title = this._element.getAttribute('title') + const originalTitleType = typeof this._element.getAttribute('data-bs-original-title') if (title || originalTitleType !== 'string') { - this.element.setAttribute('data-bs-original-title', title || '') - this.element.setAttribute('title', '') + this._element.setAttribute('data-bs-original-title', title || '') + if (title && !this._element.getAttribute('aria-label') && !this._element.textContent) { + this._element.setAttribute('aria-label', title) + } + + this._element.setAttribute('title', '') } } @@ -676,7 +681,7 @@ class Tooltip { } _getConfig(config) { - const dataAttributes = Manipulator.getDataAttributes(this.element) + const dataAttributes = Manipulator.getDataAttributes(this._element) Object.keys(dataAttributes).forEach(dataAttr => { if (DISALLOWED_ATTRIBUTES.has(dataAttr)) { @@ -785,10 +790,6 @@ class Tooltip { } }) } - - static getInstance(element) { - return Data.getData(element, DATA_KEY) - } } /** diff --git a/js/tests/unit/alert.spec.js b/js/tests/unit/alert.spec.js index a746d8501..a1322f1c7 100644 --- a/js/tests/unit/alert.spec.js +++ b/js/tests/unit/alert.spec.js @@ -170,4 +170,24 @@ describe('Alert', () => { expect(fixtureEl.querySelector('.alert')).not.toBeNull() }) }) + + describe('getInstance', () => { + it('should return alert instance', () => { + fixtureEl.innerHTML = '
' + + const div = fixtureEl.querySelector('div') + const alert = new Alert(div) + + expect(Alert.getInstance(div)).toEqual(alert) + expect(Alert.getInstance(div)).toBeInstanceOf(Alert) + }) + + it('should return null when there is no alert instance', () => { + fixtureEl.innerHTML = '
' + + const div = fixtureEl.querySelector('div') + + expect(Alert.getInstance(div)).toEqual(null) + }) + }) }) diff --git a/js/tests/unit/button.spec.js b/js/tests/unit/button.spec.js index 44d7b54b1..51aa73774 100644 --- a/js/tests/unit/button.spec.js +++ b/js/tests/unit/button.spec.js @@ -128,4 +128,24 @@ describe('Button', () => { expect(btnEl.classList.contains('active')).toEqual(false) }) }) + + describe('getInstance', () => { + it('should return button instance', () => { + fixtureEl.innerHTML = '
' + + const div = fixtureEl.querySelector('div') + const button = new Button(div) + + expect(Button.getInstance(div)).toEqual(button) + expect(Button.getInstance(div)).toBeInstanceOf(Button) + }) + + it('should return null when there is no button instance', () => { + fixtureEl.innerHTML = '
' + + const div = fixtureEl.querySelector('div') + + expect(Button.getInstance(div)).toEqual(null) + }) + }) }) diff --git a/js/tests/unit/carousel.spec.js b/js/tests/unit/carousel.spec.js index 8c928c429..07b8fc311 100644 --- a/js/tests/unit/carousel.spec.js +++ b/js/tests/unit/carousel.spec.js @@ -1062,6 +1062,26 @@ describe('Carousel', () => { }) }) + describe('getInstance', () => { + it('should return carousel instance', () => { + fixtureEl.innerHTML = '
' + + const div = fixtureEl.querySelector('div') + const carousel = new Carousel(div) + + expect(Carousel.getInstance(div)).toEqual(carousel) + expect(Carousel.getInstance(div)).toBeInstanceOf(Carousel) + }) + + it('should return null when there is no carousel instance', () => { + fixtureEl.innerHTML = '
' + + const div = fixtureEl.querySelector('div') + + expect(Carousel.getInstance(div)).toEqual(null) + }) + }) + describe('jQueryInterface', () => { it('should create a carousel', () => { fixtureEl.innerHTML = '
' diff --git a/js/tests/unit/collapse.spec.js b/js/tests/unit/collapse.spec.js index c1ead859a..d53ab5964 100644 --- a/js/tests/unit/collapse.spec.js +++ b/js/tests/unit/collapse.spec.js @@ -812,6 +812,7 @@ describe('Collapse', () => { const collapse = new Collapse(div) expect(Collapse.getInstance(div)).toEqual(collapse) + expect(Collapse.getInstance(div)).toBeInstanceOf(Collapse) }) it('should return null when there is no collapse instance', () => { diff --git a/js/tests/unit/dropdown.spec.js b/js/tests/unit/dropdown.spec.js index f5f41636c..145763d20 100644 --- a/js/tests/unit/dropdown.spec.js +++ b/js/tests/unit/dropdown.spec.js @@ -1612,6 +1612,7 @@ describe('Dropdown', () => { const dropdown = new Dropdown(div) expect(Dropdown.getInstance(div)).toEqual(dropdown) + expect(Dropdown.getInstance(div)).toBeInstanceOf(Dropdown) }) it('should return null when there is no dropdown instance', () => { diff --git a/js/tests/unit/modal.spec.js b/js/tests/unit/modal.spec.js index 084f78ad1..f645e9892 100644 --- a/js/tests/unit/modal.spec.js +++ b/js/tests/unit/modal.spec.js @@ -1108,6 +1108,7 @@ describe('Modal', () => { const modal = new Modal(div) expect(Modal.getInstance(div)).toEqual(modal) + expect(Modal.getInstance(div)).toBeInstanceOf(Modal) }) it('should return null when there is no modal instance', () => { diff --git a/js/tests/unit/popover.spec.js b/js/tests/unit/popover.spec.js index df4830595..3c04e7ac1 100644 --- a/js/tests/unit/popover.spec.js +++ b/js/tests/unit/popover.spec.js @@ -116,6 +116,22 @@ describe('Popover', () => { popover.show() }) + + it('should show a popover with provided custom class', done => { + fixtureEl.innerHTML = 'BS twitter' + + const popoverEl = fixtureEl.querySelector('a') + const popover = new Popover(popoverEl) + + popoverEl.addEventListener('shown.bs.popover', () => { + const tip = document.querySelector('.popover') + expect(tip).toBeDefined() + expect(tip.classList.contains('custom-class')).toBeTrue() + done() + }) + + popover.show() + }) }) describe('hide', () => { @@ -237,6 +253,7 @@ describe('Popover', () => { const popover = new Popover(popoverEl) expect(Popover.getInstance(popoverEl)).toEqual(popover) + expect(Popover.getInstance(popoverEl)).toBeInstanceOf(Popover) }) it('should return null when there is no popover instance', () => { diff --git a/js/tests/unit/scrollspy.spec.js b/js/tests/unit/scrollspy.spec.js index 33298300c..45de56fbe 100644 --- a/js/tests/unit/scrollspy.spec.js +++ b/js/tests/unit/scrollspy.spec.js @@ -634,6 +634,16 @@ describe('ScrollSpy', () => { }) describe('getInstance', () => { + it('should return scrollspy instance', () => { + fixtureEl.innerHTML = '
' + + const div = fixtureEl.querySelector('div') + const scrollSpy = new ScrollSpy(div) + + expect(ScrollSpy.getInstance(div)).toEqual(scrollSpy) + expect(ScrollSpy.getInstance(div)).toBeInstanceOf(ScrollSpy) + }) + it('should return null if there is no instance', () => { expect(ScrollSpy.getInstance(fixtureEl)).toEqual(null) }) diff --git a/js/tests/unit/tab.spec.js b/js/tests/unit/tab.spec.js index 324c4a09b..67a85b2e4 100644 --- a/js/tests/unit/tab.spec.js +++ b/js/tests/unit/tab.spec.js @@ -417,6 +417,7 @@ describe('Tab', () => { const tab = new Tab(divEl) expect(Tab.getInstance(divEl)).toEqual(tab) + expect(Tab.getInstance(divEl)).toBeInstanceOf(Tab) }) }) diff --git a/js/tests/unit/toast.spec.js b/js/tests/unit/toast.spec.js index c4cb69a7f..a4ab4f76c 100644 --- a/js/tests/unit/toast.spec.js +++ b/js/tests/unit/toast.spec.js @@ -384,6 +384,7 @@ describe('Toast', () => { const toast = new Toast(div) expect(Toast.getInstance(div)).toEqual(toast) + expect(Toast.getInstance(div)).toBeInstanceOf(Toast) }) it('should return null when there is no toast instance', () => { diff --git a/js/tests/unit/tooltip.spec.js b/js/tests/unit/tooltip.spec.js index c781f587a..b11980751 100644 --- a/js/tests/unit/tooltip.spec.js +++ b/js/tests/unit/tooltip.spec.js @@ -632,6 +632,61 @@ describe('Tooltip', () => { tooltipEl.dispatchEvent(createEvent('mouseover')) }) + + it('should show a tooltip with custom class provided in data attributes', done => { + fixtureEl.innerHTML = '' + + const tooltipEl = fixtureEl.querySelector('a') + const tooltip = new Tooltip(tooltipEl) + + tooltipEl.addEventListener('shown.bs.tooltip', () => { + const tip = document.querySelector('.tooltip') + expect(tip).toBeDefined() + expect(tip.classList.contains('custom-class')).toBeTrue() + done() + }) + + tooltip.show() + }) + + it('should show a tooltip with custom class provided as a string in config', done => { + fixtureEl.innerHTML = '' + + const tooltipEl = fixtureEl.querySelector('a') + const tooltip = new Tooltip(tooltipEl, { + customClass: 'custom-class custom-class-2' + }) + + tooltipEl.addEventListener('shown.bs.tooltip', () => { + const tip = document.querySelector('.tooltip') + expect(tip).toBeDefined() + expect(tip.classList.contains('custom-class')).toBeTrue() + expect(tip.classList.contains('custom-class-2')).toBeTrue() + done() + }) + + tooltip.show() + }) + + it('should show a tooltip with custom class provided as a function in config', done => { + fixtureEl.innerHTML = '' + + const spy = jasmine.createSpy('customClass').and.returnValue('custom-class') + const tooltipEl = fixtureEl.querySelector('a') + const tooltip = new Tooltip(tooltipEl, { + customClass: spy + }) + + tooltipEl.addEventListener('shown.bs.tooltip', () => { + const tip = document.querySelector('.tooltip') + expect(tip).toBeDefined() + expect(spy).toHaveBeenCalled() + expect(tip.classList.contains('custom-class')).toBeTrue() + done() + }) + + tooltip.show() + }) }) describe('hide', () => { @@ -975,6 +1030,79 @@ describe('Tooltip', () => { }) }) + describe('getInstance', () => { + it('should return tooltip instance', () => { + fixtureEl.innerHTML = '
' + + const div = fixtureEl.querySelector('div') + const alert = new Tooltip(div) + + expect(Tooltip.getInstance(div)).toEqual(alert) + expect(Tooltip.getInstance(div)).toBeInstanceOf(Tooltip) + }) + + it('should return null when there is no tooltip instance', () => { + fixtureEl.innerHTML = '
' + + const div = fixtureEl.querySelector('div') + + expect(Tooltip.getInstance(div)).toEqual(null) + }) + }) + + describe('aria-label', () => { + it('should add the aria-label attribute for referencing original title', done => { + fixtureEl.innerHTML = '
' + + const tooltipEl = fixtureEl.querySelector('a') + const tooltip = new Tooltip(tooltipEl) + + tooltipEl.addEventListener('shown.bs.tooltip', () => { + const tooltipShown = document.querySelector('.tooltip') + + expect(tooltipShown).toBeDefined() + expect(tooltipEl.getAttribute('aria-label')).toEqual('Another tooltip') + done() + }) + + tooltip.show() + }) + + it('should not add the aria-label attribute if the attribute already exists', done => { + fixtureEl.innerHTML = '' + + const tooltipEl = fixtureEl.querySelector('a') + const tooltip = new Tooltip(tooltipEl) + + tooltipEl.addEventListener('shown.bs.tooltip', () => { + const tooltipShown = document.querySelector('.tooltip') + + expect(tooltipShown).toBeDefined() + expect(tooltipEl.getAttribute('aria-label')).toEqual('Different label') + done() + }) + + tooltip.show() + }) + + it('should not add the aria-label attribute if the element has text content', done => { + fixtureEl.innerHTML = 'text content' + + const tooltipEl = fixtureEl.querySelector('a') + const tooltip = new Tooltip(tooltipEl) + + tooltipEl.addEventListener('shown.bs.tooltip', () => { + const tooltipShown = document.querySelector('.tooltip') + + expect(tooltipShown).toBeDefined() + expect(tooltipEl.getAttribute('aria-label')).toBeNull() + done() + }) + + tooltip.show() + }) + }) + describe('jQueryInterface', () => { it('should create a tooltip', () => { fixtureEl.innerHTML = '
' diff --git a/package.json b/package.json index 7ef514d1c..b4545406c 100644 --- a/package.json +++ b/package.json @@ -40,10 +40,11 @@ "js-minify-standalone": "terser --compress --mangle --comments \"/^!/\" --source-map \"content=dist/js/bootstrap.js.map,includeSources,url=bootstrap.min.js.map\" --output dist/js/bootstrap.min.js dist/js/bootstrap.js", "js-minify-standalone-esm": "terser --compress --mangle --comments \"/^!/\" --source-map \"content=dist/js/bootstrap.esm.js.map,includeSources,url=bootstrap.esm.min.js.map\" --output dist/js/bootstrap.esm.min.js dist/js/bootstrap.esm.js", "js-minify-bundle": "terser --compress --mangle --comments \"/^!/\" --source-map \"content=dist/js/bootstrap.bundle.js.map,includeSources,url=bootstrap.bundle.min.js.map\" --output dist/js/bootstrap.bundle.min.js dist/js/bootstrap.bundle.js", - "js-test": "npm-run-all --parallel js-test-karma js-test-jquery js-test-integration", + "js-test": "npm-run-all --parallel js-test-karma js-test-jquery js-test-integration-*", "js-debug": "cross-env DEBUG=true npm run js-test-karma", "js-test-karma": "karma start js/tests/karma.conf.js", - "js-test-integration": "rollup --config js/tests/integration/rollup.bundle.js && rollup --config js/tests/integration/rollup.bundle-modularity.js", + "js-test-integration-bundle": "rollup --config js/tests/integration/rollup.bundle.js", + "js-test-integration-modularity": "rollup --config js/tests/integration/rollup.bundle-modularity.js", "js-test-cloud": "cross-env BROWSER=true npm run js-test-karma", "js-test-jquery": "cross-env JQUERY=true npm run js-test-karma", "lint": "npm-run-all --parallel js-lint css-lint lockfile-lint", diff --git a/scss/_button-group.scss b/scss/_button-group.scss index 1571d1ea9..abb363a84 100644 --- a/scss/_button-group.scss +++ b/scss/_button-group.scss @@ -132,7 +132,7 @@ @include border-bottom-radius(0); } - > .btn:not(:first-child), + > .btn ~ .btn, > .btn-group:not(:first-child) > .btn { @include border-top-radius(0); } diff --git a/scss/_tables.scss b/scss/_tables.scss index 1b2db3008..506c60be0 100644 --- a/scss/_tables.scss +++ b/scss/_tables.scss @@ -4,7 +4,6 @@ .table { --#{$variable-prefix}table-bg: #{$table-bg}; - --#{$variable-prefix}table-accent-bg: transparent; --#{$variable-prefix}table-striped-color: #{$table-striped-color}; --#{$variable-prefix}table-striped-bg: #{$table-striped-bg}; --#{$variable-prefix}table-active-color: #{$table-active-color}; diff --git a/site/content/docs/5.0/components/modal.md b/site/content/docs/5.0/components/modal.md index 5bd116c8e..36dc3b548 100644 --- a/site/content/docs/5.0/components/modal.md +++ b/site/content/docs/5.0/components/modal.md @@ -877,12 +877,6 @@ Options can be passed via data attributes or JavaScript. For data attributes, ap true Puts the focus on the modal when initialized. - - show - boolean - true - Shows the modal when initialized. - diff --git a/site/content/docs/5.0/components/popovers.md b/site/content/docs/5.0/components/popovers.md index c1e1f9017..86efe909e 100644 --- a/site/content/docs/5.0/components/popovers.md +++ b/site/content/docs/5.0/components/popovers.md @@ -262,6 +262,15 @@ Note that for security reasons the `sanitize`, `sanitizeFn`, and `allowList` opt 'scrollParent' Overflow constraint boundary of the popover. Accepts the values of 'viewport', 'window', 'scrollParent', or an HTMLElement reference (JavaScript only). For more information refer to Popper's preventOverflow docs. + + customClass + string | function + '' + +

Add classes to the popover when it is shown. Note that these classes will be added in addition to any classes specified in the template. To add multiple classes, separate them with spaces: 'class-1 class-2'.

+

You can also pass a function that should return a single string containing additional class names.

+ + sanitize boolean diff --git a/site/content/docs/5.0/components/tooltips.md b/site/content/docs/5.0/components/tooltips.md index 58c1bf9db..a4e76bc06 100644 --- a/site/content/docs/5.0/components/tooltips.md +++ b/site/content/docs/5.0/components/tooltips.md @@ -271,6 +271,15 @@ Note that for security reasons the `sanitize`, `sanitizeFn`, and `allowList` opt 'scrollParent' Overflow constraint boundary of the tooltip. Accepts the values of 'viewport', 'window', 'scrollParent', or an HTMLElement reference (JavaScript only). For more information refer to Popper's preventOverflow docs. + + customClass + string | function + '' + +

Add classes to the tooltip when it is shown. Note that these classes will be added in addition to any classes specified in the template. To add multiple classes, separate them with spaces: 'class-1 class-2'.

+

You can also pass a function that should return a single string containing additional class names.

+ + sanitize boolean diff --git a/site/content/docs/5.0/content/typography.md b/site/content/docs/5.0/content/typography.md index b8e4f558a..d93702eea 100644 --- a/site/content/docs/5.0/content/typography.md +++ b/site/content/docs/5.0/content/typography.md @@ -13,7 +13,7 @@ Bootstrap sets basic global display, typography, and link styles. When more cont - Use a [native font stack]({{< docsref "/content/reboot#native-font-stack" >}}) that selects the best `font-family` for each OS and device. - For a more inclusive and accessible type scale, we use the browser's default root `font-size` (typically 16px) so visitors can customize their browser defaults as needed. - Use the `$font-family-base`, `$font-size-base`, and `$line-height-base` attributes as our typographic base applied to the ``. -- Set the global link color via `$link-color` and apply link underlines only on `:hover`. +- Set the global link color via `$link-color`. - Use `$body-bg` to set a `background-color` on the `` (`#fff` by default). These styles can be found within `_reboot.scss`, and the global variables are defined in `_variables.scss`. Make sure to set `$font-size-base` in `rem`. diff --git a/site/content/docs/5.0/migration.md b/site/content/docs/5.0/migration.md index dab1bd7df..296bdd427 100644 --- a/site/content/docs/5.0/migration.md +++ b/site/content/docs/5.0/migration.md @@ -267,6 +267,7 @@ Changes to Reboot, typography, tables, and more. - Removed individual `$display-*-weight` variables for a single `$display-font-weight`. - Added two new `.display-*` heading styles, `.display-5` and `.display-6`. - Resized existing display headings for a slightly more consistent set of `font-size`s. +- Links are underlined by default (not just on hover), unless they're part of specific components. ### Forms diff --git a/site/layouts/partials/home/masthead-followup.html b/site/layouts/partials/home/masthead-followup.html index d14febd4e..a2dbdc76b 100644 --- a/site/layouts/partials/home/masthead-followup.html +++ b/site/layouts/partials/home/masthead-followup.html @@ -81,7 +81,7 @@ Bootstrap Icons + alt="Bootstrap Themes" width="700" height="500" loading="lazy">