Merge branch 'main' into patrickhlauke-issue26202

This commit is contained in:
Patrick H. Lauke 2020-11-30 18:28:00 +00:00 committed by GitHub
commit 35b6242a85
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
33 changed files with 405 additions and 220 deletions

View file

@ -38,7 +38,7 @@
},
{
"path": "./dist/js/bootstrap.bundle.min.js",
"maxSize": "22 kB"
"maxSize": "22.25 kB"
},
{
"path": "./dist/js/bootstrap.esm.js",

View file

@ -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)
}
}
/**

44
js/src/base-component.js Normal file
View file

@ -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

View file

@ -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)
}
}
/**

View file

@ -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)
}
}
/**

View file

@ -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)
}
}
/**

View file

@ -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)
}
}
/**

View file

@ -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)
}
}
/**

View file

@ -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)
}
}
/**

View file

@ -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)
}
}
/**

View file

@ -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)
}
}
/**

View file

@ -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)
}
}
/**

View file

@ -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)
}
}
/**

View file

@ -170,4 +170,24 @@ describe('Alert', () => {
expect(fixtureEl.querySelector('.alert')).not.toBeNull()
})
})
describe('getInstance', () => {
it('should return alert instance', () => {
fixtureEl.innerHTML = '<div></div>'
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 = '<div></div>'
const div = fixtureEl.querySelector('div')
expect(Alert.getInstance(div)).toEqual(null)
})
})
})

View file

@ -128,4 +128,24 @@ describe('Button', () => {
expect(btnEl.classList.contains('active')).toEqual(false)
})
})
describe('getInstance', () => {
it('should return button instance', () => {
fixtureEl.innerHTML = '<div></div>'
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 = '<div></div>'
const div = fixtureEl.querySelector('div')
expect(Button.getInstance(div)).toEqual(null)
})
})
})

View file

@ -1062,6 +1062,26 @@ describe('Carousel', () => {
})
})
describe('getInstance', () => {
it('should return carousel instance', () => {
fixtureEl.innerHTML = '<div></div>'
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 = '<div></div>'
const div = fixtureEl.querySelector('div')
expect(Carousel.getInstance(div)).toEqual(null)
})
})
describe('jQueryInterface', () => {
it('should create a carousel', () => {
fixtureEl.innerHTML = '<div></div>'

View file

@ -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', () => {

View file

@ -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', () => {

View file

@ -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', () => {

View file

@ -116,6 +116,22 @@ describe('Popover', () => {
popover.show()
})
it('should show a popover with provided custom class', done => {
fixtureEl.innerHTML = '<a href="#" title="Popover" data-bs-content="https://twitter.com/getbootstrap" data-bs-custom-class="custom-class">BS twitter</a>'
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', () => {

View file

@ -634,6 +634,16 @@ describe('ScrollSpy', () => {
})
describe('getInstance', () => {
it('should return scrollspy instance', () => {
fixtureEl.innerHTML = '<div></div>'
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)
})

View file

@ -417,6 +417,7 @@ describe('Tab', () => {
const tab = new Tab(divEl)
expect(Tab.getInstance(divEl)).toEqual(tab)
expect(Tab.getInstance(divEl)).toBeInstanceOf(Tab)
})
})

View file

@ -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', () => {

View file

@ -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 = '<a href="#" rel="tooltip" title="Another tooltip" data-bs-custom-class="custom-class">'
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 = '<a href="#" rel="tooltip" title="Another tooltip">'
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 = '<a href="#" rel="tooltip" title="Another tooltip">'
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 = '<div></div>'
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 = '<div></div>'
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 = '<a href="#" rel="tooltip" title="Another tooltip"></a>'
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 = '<a href="#" rel="tooltip" aria-label="Different label" title="Another tooltip"></a>'
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 = '<a href="#" rel="tooltip" title="Another tooltip">text content</a>'
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 = '<div></div>'

View file

@ -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",

View file

@ -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);
}

View file

@ -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};

View file

@ -877,12 +877,6 @@ Options can be passed via data attributes or JavaScript. For data attributes, ap
<td><code>true</code></td>
<td>Puts the focus on the modal when initialized.</td>
</tr>
<tr>
<td><code>show</code></td>
<td>boolean</td>
<td><code>true</code></td>
<td>Shows the modal when initialized.</td>
</tr>
</tbody>
</table>

View file

@ -262,6 +262,15 @@ Note that for security reasons the `sanitize`, `sanitizeFn`, and `allowList` opt
<td><code>'scrollParent'</code></td>
<td>Overflow constraint boundary of the popover. Accepts the values of <code>'viewport'</code>, <code>'window'</code>, <code>'scrollParent'</code>, or an HTMLElement reference (JavaScript only). For more information refer to Popper's <a href="https://popper.js.org/docs/v1/#modifiers..preventOverflow.boundariesElement">preventOverflow docs</a>.</td>
</tr>
<tr>
<td><code>customClass</code></td>
<td>string | function</td>
<td><code>''</code></td>
<td>
<p>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: <code>'class-1 class-2'</code>.</p>
<p>You can also pass a function that should return a single string containing additional class names.</p>
</td>
</tr>
<tr>
<td><code>sanitize</code></td>
<td>boolean</td>

View file

@ -271,6 +271,15 @@ Note that for security reasons the `sanitize`, `sanitizeFn`, and `allowList` opt
<td><code>'scrollParent'</code></td>
<td>Overflow constraint boundary of the tooltip. Accepts the values of <code>'viewport'</code>, <code>'window'</code>, <code>'scrollParent'</code>, or an HTMLElement reference (JavaScript only). For more information refer to Popper's <a href="https://popper.js.org/docs/v1/#modifiers..preventOverflow.boundariesElement">preventOverflow docs</a>.</td>
</tr>
<tr>
<td><code>customClass</code></td>
<td>string | function</td>
<td><code>''</code></td>
<td>
<p>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: <code>'class-1 class-2'</code>.</p>
<p>You can also pass a function that should return a single string containing additional class names.</p>
</td>
</tr>
<tr>
<td><code>sanitize</code></td>
<td>boolean</td>

View file

@ -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 `<body>`.
- 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 `<body>` (`#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`.

View file

@ -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

View file

@ -81,7 +81,7 @@
<img class="img-fluid mt-3 mx-auto" srcset="/docs/{{ .Site.Params.docs_version }}/assets/img/bootstrap-themes.png,
/docs/{{ .Site.Params.docs_version }}/assets/img/bootstrap-themes@2x.png 2x"
src="/docs/{{ .Site.Params.docs_version }}/assets/img/bootstrap-themes.png"
alt="Bootstrap Icons" width="700" height="500" loading="lazy">
alt="Bootstrap Themes" width="700" height="500" loading="lazy">
</div>
</section>
</div>