0
0
mirror of https://github.com/twbs/bootstrap.git synced 2024-12-01 13:24:25 +01:00

Extract Component config functionality to a separate class (#33872)

Co-authored-by: XhmikosR <xhmikosr@gmail.com>
This commit is contained in:
GeoSot 2021-12-10 18:18:18 +02:00 committed by GitHub
parent 68f226750d
commit 886b940796
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 283 additions and 240 deletions

View File

@ -6,11 +6,9 @@
*/
import Data from './dom/data'
import {
executeAfterTransition,
getElement
} from './util/index'
import { executeAfterTransition, getElement } from './util/index'
import EventHandler from './dom/event-handler'
import Config from './util/config'
/**
* Constants
@ -22,15 +20,18 @@ const VERSION = '5.1.3'
* Class definition
*/
class BaseComponent {
constructor(element) {
element = getElement(element)
class BaseComponent extends Config {
constructor(element, config) {
super()
element = getElement(element)
if (!element) {
return
}
this._element = element
this._config = this._getConfig(config)
Data.set(this._element, this.constructor.DATA_KEY, this)
}
@ -48,6 +49,13 @@ class BaseComponent {
executeAfterTransition(callback, element, isAnimated)
}
_getConfig(config) {
config = this._mergeConfigObj(config, this._element)
config = this._configAfterMerge(config)
this._typeCheckConfig(config)
return config
}
// Static
static getInstance(element) {
return Data.get(getElement(element), this.DATA_KEY)
@ -61,10 +69,6 @@ class BaseComponent {
return VERSION
}
static get NAME() {
throw new Error('You have to implement the static method "NAME" for each component!')
}
static get DATA_KEY() {
return `bs.${this.NAME}`
}

View File

@ -12,8 +12,7 @@ import {
isRTL,
isVisible,
reflow,
triggerTransitionEnd,
typeCheckConfig
triggerTransitionEnd
} from './util/index'
import EventHandler from './dom/event-handler'
import Manipulator from './dom/manipulator'
@ -95,7 +94,7 @@ const DefaultType = {
class Carousel extends BaseComponent {
constructor(element, config) {
super(element)
super(element, config)
this._items = null
this._interval = null
@ -105,7 +104,6 @@ class Carousel extends BaseComponent {
this.touchTimeout = null
this._swipeHelper = null
this._config = this._getConfig(config)
this._indicatorsElement = SelectorEngine.findOne(SELECTOR_INDICATORS, this._element)
this._addEventListeners()
}
@ -115,6 +113,10 @@ class Carousel extends BaseComponent {
return Default
}
static get DefaultType() {
return DefaultType
}
static get NAME() {
return NAME
}
@ -205,16 +207,6 @@ class Carousel extends BaseComponent {
}
// Private
_getConfig(config) {
config = {
...Default,
...Manipulator.getDataAttributes(this._element),
...(typeof config === 'object' ? config : {})
}
typeCheckConfig(NAME, config, DefaultType)
return config
}
_addEventListeners() {
if (this._config.keyboard) {
EventHandler.on(this._element, EVENT_KEYDOWN, event => this._keydown(event))

View File

@ -10,11 +10,9 @@ import {
getElement,
getElementFromSelector,
getSelectorFromElement,
reflow,
typeCheckConfig
reflow
} 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'
@ -62,10 +60,9 @@ const DefaultType = {
class Collapse extends BaseComponent {
constructor(element, config) {
super(element)
super(element, config)
this._isTransitioning = false
this._config = this._getConfig(config)
this._triggerArray = []
const toggleList = SelectorEngine.find(SELECTOR_DATA_TOGGLE)
@ -96,6 +93,10 @@ class Collapse extends BaseComponent {
return Default
}
static get DefaultType() {
return DefaultType
}
static get NAME() {
return NAME
}
@ -210,15 +211,9 @@ class Collapse extends BaseComponent {
}
// Private
_getConfig(config) {
config = {
...Default,
...Manipulator.getDataAttributes(this._element),
...config
}
_configAfterMerge(config) {
config.toggle = Boolean(config.toggle) // Coerce string values
config.parent = getElement(config.parent)
typeCheckConfig(NAME, config, DefaultType)
return config
}

View File

@ -15,8 +15,7 @@ import {
isElement,
isRTL,
isVisible,
noop,
typeCheckConfig
noop
} from './util/index'
import EventHandler from './dom/event-handler'
import Manipulator from './dom/manipulator'
@ -88,10 +87,9 @@ const DefaultType = {
class Dropdown extends BaseComponent {
constructor(element, config) {
super(element)
super(element, config)
this._popper = null
this._config = this._getConfig(config)
this._menu = this._getMenuElement()
this._inNavbar = this._detectNavbar()
}
@ -205,13 +203,7 @@ class Dropdown extends BaseComponent {
}
_getConfig(config) {
config = {
...this.constructor.Default,
...Manipulator.getDataAttributes(this._element),
...config
}
typeCheckConfig(NAME, config, this.constructor.DefaultType)
config = super._getConfig(config)
if (typeof config.reference === 'object' && !isElement(config.reference) &&
typeof config.reference.getBoundingClientRect !== 'function'

View File

@ -5,16 +5,8 @@
* --------------------------------------------------------------------------
*/
import {
defineJQueryPlugin,
getElementFromSelector,
isRTL,
isVisible,
reflow,
typeCheckConfig
} from './util/index'
import { defineJQueryPlugin, getElementFromSelector, isRTL, isVisible, reflow } from './util/index'
import EventHandler from './dom/event-handler'
import Manipulator from './dom/manipulator'
import SelectorEngine from './dom/selector-engine'
import ScrollBarHelper from './util/scrollbar'
import BaseComponent from './base-component'
@ -70,9 +62,8 @@ const DefaultType = {
class Modal extends BaseComponent {
constructor(element, config) {
super(element)
super(element, config)
this._config = this._getConfig(config)
this._dialog = SelectorEngine.findOne(SELECTOR_DIALOG, this._element)
this._backdrop = this._initializeBackDrop()
this._focustrap = this._initializeFocusTrap()
@ -86,6 +77,10 @@ class Modal extends BaseComponent {
return Default
}
static get DefaultType() {
return DefaultType
}
static get NAME() {
return NAME
}
@ -175,16 +170,6 @@ class Modal extends BaseComponent {
})
}
_getConfig(config) {
config = {
...Default,
...Manipulator.getDataAttributes(this._element),
...(typeof config === 'object' ? config : {})
}
typeCheckConfig(NAME, config, DefaultType)
return config
}
_showElement(relatedTarget) {
// try to append dynamic modal
if (!document.body.contains(this._element)) {

View File

@ -9,14 +9,12 @@ import {
defineJQueryPlugin,
getElementFromSelector,
isDisabled,
isVisible,
typeCheckConfig
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 Manipulator from './dom/manipulator'
import Backdrop from './util/backdrop'
import FocusTrap from './util/focustrap'
import { enableDismissTrigger } from './util/component-functions'
@ -63,9 +61,8 @@ const DefaultType = {
class Offcanvas extends BaseComponent {
constructor(element, config) {
super(element)
super(element, config)
this._config = this._getConfig(config)
this._isShown = false
this._backdrop = this._initializeBackDrop()
this._focustrap = this._initializeFocusTrap()
@ -73,14 +70,18 @@ class Offcanvas extends BaseComponent {
}
// Getters
static get NAME() {
return NAME
}
static get Default() {
return Default
}
static get DefaultType() {
return DefaultType
}
static get NAME() {
return NAME
}
// Public
toggle(relatedTarget) {
return this._isShown ? this.hide() : this.show(relatedTarget)
@ -162,16 +163,6 @@ class Offcanvas extends BaseComponent {
}
// Private
_getConfig(config) {
config = {
...Default,
...Manipulator.getDataAttributes(this._element),
...(typeof config === 'object' ? config : {})
}
typeCheckConfig(NAME, config, DefaultType)
return config
}
_initializeBackDrop() {
return new Backdrop({
className: CLASS_NAME_BACKDROP,

View File

@ -60,6 +60,10 @@ class Popover extends Tooltip {
return Default
}
static get DefaultType() {
return DefaultType
}
static get NAME() {
return NAME
}
@ -68,10 +72,6 @@ class Popover extends Tooltip {
return Event
}
static get DefaultType() {
return DefaultType
}
// Overrides
_isWithContent() {
return this._getTitle() || this._getContent()

View File

@ -8,8 +8,7 @@
import {
defineJQueryPlugin,
getElement,
getSelectorFromElement,
typeCheckConfig
getSelectorFromElement
} from './util/index'
import EventHandler from './dom/event-handler'
import Manipulator from './dom/manipulator'
@ -62,9 +61,8 @@ const DefaultType = {
class ScrollSpy extends BaseComponent {
constructor(element, config) {
super(element)
super(element, config)
this._scrollElement = this._element.tagName === 'BODY' ? window : this._element
this._config = this._getConfig(config)
this._offsets = []
this._targets = []
this._activeTarget = null
@ -81,6 +79,10 @@ class ScrollSpy extends BaseComponent {
return Default
}
static get DefaultType() {
return DefaultType
}
static get NAME() {
return NAME
}
@ -135,17 +137,10 @@ class ScrollSpy extends BaseComponent {
}
// Private
_getConfig(config) {
config = {
...Default,
...Manipulator.getDataAttributes(this._element),
...(typeof config === 'object' && config ? config : {})
}
_configAfterMerge(config) {
config.target = getElement(config.target) || document.documentElement
typeCheckConfig(NAME, config, DefaultType)
return config
}

View File

@ -5,9 +5,8 @@
* --------------------------------------------------------------------------
*/
import { defineJQueryPlugin, reflow, typeCheckConfig } from './util/index'
import { defineJQueryPlugin, reflow } from './util/index'
import EventHandler from './dom/event-handler'
import Manipulator from './dom/manipulator'
import BaseComponent from './base-component'
import { enableDismissTrigger } from './util/component-functions'
@ -51,9 +50,8 @@ const Default = {
class Toast extends BaseComponent {
constructor(element, config) {
super(element)
super(element, config)
this._config = this._getConfig(config)
this._timeout = null
this._hasMouseInteraction = false
this._hasKeyboardInteraction = false
@ -61,14 +59,14 @@ class Toast extends BaseComponent {
}
// Getters
static get DefaultType() {
return DefaultType
}
static get Default() {
return Default
}
static get DefaultType() {
return DefaultType
}
static get NAME() {
return NAME
}
@ -133,17 +131,6 @@ class Toast extends BaseComponent {
}
// Private
_getConfig(config) {
config = {
...Default,
...Manipulator.getDataAttributes(this._element),
...(typeof config === 'object' && config ? config : {})
}
typeCheckConfig(NAME, config, this.constructor.DefaultType)
return config
}
_maybeScheduleHide() {
if (!this._config.autohide) {

View File

@ -12,8 +12,7 @@ import {
getElement,
getUID,
isRTL,
noop,
typeCheckConfig
noop
} from './util/index'
import { DefaultAllowlist } from './util/sanitizer'
import EventHandler from './dom/event-handler'
@ -140,6 +139,10 @@ class Tooltip extends BaseComponent {
return Default
}
static get DefaultType() {
return DefaultType
}
static get NAME() {
return NAME
}
@ -148,10 +151,6 @@ class Tooltip extends BaseComponent {
return Event
}
static get DefaultType() {
return DefaultType
}
// Public
enable() {
this._isEnabled = true
@ -571,11 +570,16 @@ class Tooltip extends BaseComponent {
}
config = {
...this.constructor.Default,
...dataAttributes,
...(typeof config === 'object' && config ? config : {})
}
config = this._mergeConfigObj(config)
config = this._configAfterMerge(config)
this._typeCheckConfig(config)
return config
}
_configAfterMerge(config) {
config.container = config.container === false ? document.body : getElement(config.container)
if (typeof config.delay === 'number') {
@ -595,7 +599,6 @@ class Tooltip extends BaseComponent {
config.content = config.content.toString()
}
typeCheckConfig(NAME, config, this.constructor.DefaultType)
return config
}

View File

@ -6,7 +6,8 @@
*/
import EventHandler from '../dom/event-handler'
import { execute, executeAfterTransition, getElement, reflow, typeCheckConfig } from './index'
import { execute, executeAfterTransition, getElement, reflow } from './index'
import Config from './config'
/**
* Constants
@ -37,13 +38,27 @@ const DefaultType = {
* Class definition
*/
class Backdrop {
class Backdrop extends Config {
constructor(config) {
super()
this._config = this._getConfig(config)
this._isAppended = false
this._element = null
}
// Getters
static get Default() {
return Default
}
static get DefaultType() {
return DefaultType
}
static get NAME() {
return NAME
}
// Public
show(callback) {
if (!this._config.isVisible) {
@ -104,15 +119,9 @@ class Backdrop {
return this._element
}
_getConfig(config) {
config = {
...Default,
...(typeof config === 'object' ? config : {})
}
_configAfterMerge(config) {
// use getElement() with the default "body" to get a fresh Element on each instantiation
config.rootElement = getElement(config.rootElement)
typeCheckConfig(NAME, config, DefaultType)
return config
}

63
js/src/util/config.js Normal file
View File

@ -0,0 +1,63 @@
/**
* --------------------------------------------------------------------------
* Bootstrap (v5.1.3): util/config.js
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
* --------------------------------------------------------------------------
*/
import { isElement, toType } from './index'
import Manipulator from '../dom/manipulator'
/**
* Class definition
*/
class Config {
// Getters
static get Default() {
return {}
}
static get DefaultType() {
return {}
}
static get NAME() {
throw new Error('You have to implement the static method "NAME", for each component!')
}
_getConfig(config) {
config = this._mergeConfigObj(config)
config = this._configAfterMerge(config)
this._typeCheckConfig(config)
return config
}
_configAfterMerge(config) {
return config
}
_mergeConfigObj(config, element) {
return {
...this.constructor.Default,
...(isElement(element) ? Manipulator.getDataAttributes(element) : {}),
...(typeof config === 'object' ? config : {})
}
}
_typeCheckConfig(config, configTypes = this.constructor.DefaultType) {
for (const property of Object.keys(configTypes)) {
const expectedTypes = configTypes[property]
const value = config[property]
const valueType = isElement(value) ? 'element' : toType(value)
if (!new RegExp(expectedTypes).test(valueType)) {
throw new TypeError(
`${this.constructor.NAME.toUpperCase()}: Option "${property}" provided type "${valueType}" but expected type "${expectedTypes}".`
)
}
}
}
}
export default Config

View File

@ -7,7 +7,7 @@
import EventHandler from '../dom/event-handler'
import SelectorEngine from '../dom/selector-engine'
import { typeCheckConfig } from './index'
import Config from './config'
/**
* Constants
@ -37,13 +37,27 @@ const DefaultType = {
* Class definition
*/
class FocusTrap {
class FocusTrap extends Config {
constructor(config) {
super()
this._config = this._getConfig(config)
this._isActive = false
this._lastTabNavDirection = null
}
// Getters
static get Default() {
return Default
}
static get DefaultType() {
return DefaultType
}
static get NAME() {
return NAME
}
// Public
activate() {
const { trapElement, autofocus } = this._config
@ -99,15 +113,6 @@ class FocusTrap {
this._lastTabNavDirection = event.shiftKey ? TAB_NAV_BACKWARD : TAB_NAV_FORWARD
}
_getConfig(config) {
config = {
...Default,
...(typeof config === 'object' ? config : {})
}
typeCheckConfig(NAME, config, DefaultType)
return config
}
}
export default FocusTrap

View File

@ -123,20 +123,6 @@ const getElement = object => {
return null
}
const typeCheckConfig = (componentName, config, configTypes) => {
for (const property of Object.keys(configTypes)) {
const expectedTypes = configTypes[property]
const value = config[property]
const valueType = value && isElement(value) ? 'element' : toType(value)
if (!new RegExp(expectedTypes).test(valueType)) {
throw new TypeError(
`${componentName.toUpperCase()}: Option "${property}" provided type "${valueType}" but expected type "${expectedTypes}".`
)
}
}
}
const isVisible = element => {
if (!isElement(element) || element.getClientRects().length === 0) {
return false
@ -327,5 +313,5 @@ export {
onDOMContentLoaded,
reflow,
triggerTransitionEnd,
typeCheckConfig
toType
}

View File

@ -5,8 +5,9 @@
* --------------------------------------------------------------------------
*/
import Config from './config'
import EventHandler from '../dom/event-handler'
import { execute, typeCheckConfig } from './index'
import { execute } from './index'
/**
* Constants
@ -40,8 +41,9 @@ const DefaultType = {
* Class definition
*/
class Swipe {
class Swipe extends Config {
constructor(element, config) {
super()
this._element = element
if (!element || !Swipe.isSupported()) {
@ -54,6 +56,19 @@ class Swipe {
this._initEvents()
}
// Getters
static get Default() {
return Default
}
static get DefaultType() {
return DefaultType
}
static get NAME() {
return NAME
}
// Public
dispose() {
EventHandler.off(this._element, EVENT_KEY)
@ -118,15 +133,6 @@ class Swipe {
}
}
_getConfig(config) {
config = {
...Default,
...(typeof config === 'object' ? config : {})
}
typeCheckConfig(NAME, config, DefaultType)
return config
}
_eventIsPointerPenTouch(event) {
return this._supportPointerEvents && (event.pointerType === POINTER_TYPE_PEN || event.pointerType === POINTER_TYPE_TOUCH)
}

View File

@ -6,8 +6,9 @@
*/
import { DefaultAllowlist, sanitizeHtml } from './sanitizer'
import { getElement, isElement, typeCheckConfig } from '../util/index'
import { getElement, isElement } from '../util/index'
import SelectorEngine from '../dom/selector-engine'
import Config from './config'
/**
* Constants
@ -44,20 +45,25 @@ const DefaultContentType = {
* Class definition
*/
class TemplateFactory {
class TemplateFactory extends Config {
constructor(config) {
super()
this._config = this._getConfig(config)
}
// Getters
static get NAME() {
return NAME
}
static get Default() {
return Default
}
static get DefaultType() {
return DefaultType
}
static get NAME() {
return NAME
}
// Public
getContent() {
return Object.values(this._config.content)
@ -94,21 +100,14 @@ class TemplateFactory {
}
// Private
_getConfig(config) {
config = {
...Default,
...(typeof config === 'object' ? config : {})
}
typeCheckConfig(NAME, config, DefaultType)
_typeCheckConfig(config) {
super._typeCheckConfig(config)
this._checkContent(config.content)
return config
}
_checkContent(arg) {
for (const [selector, content] of Object.entries(arg)) {
typeCheckConfig(NAME, { selector, entry: content }, DefaultContentType)
super._typeCheckConfig({ selector, entry: content }, DefaultContentType)
}
}

View File

@ -0,0 +1,78 @@
import Config from '../../../src/util/config'
class DummyConfigClass extends Config {
static get NAME() {
return 'dummy'
}
}
describe('Config', () => {
const name = 'dummy'
describe('NAME', () => {
it('should return plugin NAME', () => {
expect(DummyConfigClass.NAME).toEqual(name)
})
})
describe('DefaultType', () => {
it('should return plugin default type', () => {
expect(DummyConfigClass.DefaultType).toEqual(jasmine.any(Object))
})
})
describe('Default', () => {
it('should return plugin defaults', () => {
expect(DummyConfigClass.Default).toEqual(jasmine.any(Object))
})
})
describe('typeCheckConfig', () => {
it('should check type of the config object', () => {
spyOnProperty(DummyConfigClass, 'DefaultType', 'get').and.returnValue({
toggle: 'boolean',
parent: '(string|element)'
})
const config = {
toggle: true,
parent: 777
}
const obj = new DummyConfigClass()
expect(() => {
obj._typeCheckConfig(config)
}).toThrowError(TypeError, obj.constructor.NAME.toUpperCase() + ': Option "parent" provided type "number" but expected type "(string|element)".')
})
it('should return null stringified when null is passed', () => {
spyOnProperty(DummyConfigClass, 'DefaultType', 'get').and.returnValue({
toggle: 'boolean',
parent: '(null|element)'
})
const obj = new DummyConfigClass()
const config = {
toggle: true,
parent: null
}
obj._typeCheckConfig(config)
expect().nothing()
})
it('should return undefined stringified when undefined is passed', () => {
spyOnProperty(DummyConfigClass, 'DefaultType', 'get').and.returnValue({
toggle: 'boolean',
parent: '(undefined|element)'
})
const obj = new DummyConfigClass()
const config = {
toggle: true,
parent: undefined
}
obj._typeCheckConfig(config)
expect().nothing()
})
})
})

View File

@ -223,53 +223,6 @@ describe('Util', () => {
})
})
describe('typeCheckConfig', () => {
const namePlugin = 'collapse'
it('should check type of the config object', () => {
const defaultType = {
toggle: 'boolean',
parent: '(string|element)'
}
const config = {
toggle: true,
parent: 777
}
expect(() => {
Util.typeCheckConfig(namePlugin, config, defaultType)
}).toThrowError(TypeError, 'COLLAPSE: Option "parent" provided type "number" but expected type "(string|element)".')
})
it('should return null stringified when null is passed', () => {
const defaultType = {
toggle: 'boolean',
parent: '(null|element)'
}
const config = {
toggle: true,
parent: null
}
Util.typeCheckConfig(namePlugin, config, defaultType)
expect().nothing()
})
it('should return undefined stringified when undefined is passed', () => {
const defaultType = {
toggle: 'boolean',
parent: '(undefined|element)'
}
const config = {
toggle: true,
parent: undefined
}
Util.typeCheckConfig(namePlugin, config, defaultType)
expect().nothing()
})
})
describe('isVisible', () => {
it('should return false if the element is not defined', () => {
expect(Util.isVisible(null)).toBeFalse()