From 5d5b37d9a1bc160ee118fcaae71edcfe604a7230 Mon Sep 17 00:00:00 2001 From: Johann-S Date: Fri, 22 Feb 2019 23:37:55 +0100 Subject: [PATCH] move util in a util folder with the sanitizer --- .eslintrc.json | 2 +- build/build-plugins.js | 63 ++++---- js/src/alert.js | 17 +- js/src/button.js | 5 +- js/src/carousel.js | 36 +++-- js/src/collapse.js | 50 +++--- js/src/dom/eventHandler.js | 5 +- js/src/dom/polyfill.js | 6 +- js/src/dom/selectorEngine.js | 6 +- js/src/dropdown.js | 30 ++-- js/src/index.js | 2 - js/src/modal.js | 58 +++---- js/src/popover.js | 5 +- js/src/scrollspy.js | 21 ++- js/src/tab.js | 25 +-- js/src/toast.js | 23 +-- js/src/tooltip.js | 47 ++++-- js/src/util.js | 179 --------------------- js/src/util/index.js | 173 ++++++++++++++++++++ js/src/{tools => util}/sanitizer.js | 84 +++++----- js/tests/index.html | 142 ---------------- js/tests/integration/bundle.js | 4 +- js/tests/integration/index.html | 6 +- js/tests/karma.conf.js | 27 ++-- js/tests/unit/.eslintrc.json | 1 + js/tests/unit/modal.js | 5 +- js/tests/unit/tooltip.js | 18 --- js/tests/unit/{util.js => util/index.js} | 2 - js/tests/unit/util/sanitizer.js | 30 ++++ js/tests/visual/toast.html | 6 +- package.json | 1 + site/docs/4.3/assets/js/src/application.js | 26 +-- 32 files changed, 517 insertions(+), 588 deletions(-) delete mode 100644 js/src/util.js create mode 100644 js/src/util/index.js rename js/src/{tools => util}/sanitizer.js (89%) delete mode 100644 js/tests/index.html rename js/tests/unit/{util.js => util/index.js} (98%) create mode 100644 js/tests/unit/util/sanitizer.js diff --git a/.eslintrc.json b/.eslintrc.json index e0b3e5380ac2..8250d274c1fd 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -130,7 +130,7 @@ "func-call-spacing": "error", "func-name-matching": "error", "func-names": "off", - "func-style": ["error", "declaration"], + "func-style": "off", "id-blacklist": "error", "id-length": "off", "id-match": "error", diff --git a/build/build-plugins.js b/build/build-plugins.js index cabd44e45592..1339684963d6 100644 --- a/build/build-plugins.js +++ b/build/build-plugins.js @@ -41,23 +41,25 @@ const bsPlugins = { ScrollSpy: path.resolve(__dirname, '../js/src/scrollspy.js'), Tab: path.resolve(__dirname, '../js/src/tab.js'), Toast: path.resolve(__dirname, '../js/src/toast.js'), - Tooltip: path.resolve(__dirname, '../js/src/tooltip.js'), - Util: path.resolve(__dirname, '../js/src/util.js') + Tooltip: path.resolve(__dirname, '../js/src/tooltip.js') } const rootPath = TEST ? '../js/coverage/dist/' : '../js/dist/' +if (TEST) { + bsPlugins.Util = path.resolve(__dirname, '../js/src/util/index.js') + bsPlugins.Sanitizer = path.resolve(__dirname, '../js/src/util/sanitizer.js') +} + const defaultPluginConfig = { external: [ bsPlugins.Data, bsPlugins.EventHandler, - bsPlugins.SelectorEngine, - bsPlugins.Util + bsPlugins.SelectorEngine ], globals: { [bsPlugins.Data]: 'Data', [bsPlugins.EventHandler]: 'EventHandler', - [bsPlugins.SelectorEngine]: 'SelectorEngine', - [bsPlugins.Util]: 'Util' + [bsPlugins.SelectorEngine]: 'SelectorEngine' } } @@ -65,7 +67,9 @@ function getConfigByPluginKey(pluginKey) { if ( pluginKey === 'Data' || pluginKey === 'Manipulator' || - pluginKey === 'Util' + pluginKey === 'Polyfill' || + pluginKey === 'Util' || + pluginKey === 'Sanitizer' ) { return { external: [], @@ -76,21 +80,10 @@ function getConfigByPluginKey(pluginKey) { if (pluginKey === 'EventHandler' || pluginKey === 'SelectorEngine') { return { external: [ - bsPlugins.Polyfill, - bsPlugins.Util + bsPlugins.Polyfill ], globals: { - [bsPlugins.Polyfill]: 'Polyfill', - [bsPlugins.Util]: 'Util' - } - } - } - - if (pluginKey === 'Polyfill') { - return { - external: [bsPlugins.Util], - globals: { - [bsPlugins.Util]: 'Util' + [bsPlugins.Polyfill]: 'Polyfill' } } } @@ -125,14 +118,12 @@ function getConfigByPluginKey(pluginKey) { external: [ bsPlugins.Data, bsPlugins.SelectorEngine, - bsPlugins.Tooltip, - bsPlugins.Util + bsPlugins.Tooltip ], globals: { [bsPlugins.Data]: 'Data', [bsPlugins.SelectorEngine]: 'SelectorEngine', - [bsPlugins.Tooltip]: 'Tooltip', - [bsPlugins.Util]: 'Util' + [bsPlugins.Tooltip]: 'Tooltip' } } } @@ -142,14 +133,12 @@ function getConfigByPluginKey(pluginKey) { external: [ bsPlugins.Data, bsPlugins.EventHandler, - bsPlugins.Manipulator, - bsPlugins.Util + bsPlugins.Manipulator ], globals: { [bsPlugins.Data]: 'Data', [bsPlugins.EventHandler]: 'EventHandler', - [bsPlugins.Manipulator]: 'Manipulator', - [bsPlugins.Util]: 'Util' + [bsPlugins.Manipulator]: 'Manipulator' } } } @@ -161,14 +150,28 @@ function build(plugin) { const config = getConfigByPluginKey(plugin) const external = config.external const globals = config.globals + let pluginPath = rootPath + + const utilObjects = [ + 'Util', + 'Sanitizer' + ] - const pluginPath = [ + const domObjects = [ 'Data', 'EventHandler', 'Manipulator', 'Polyfill', 'SelectorEngine' - ].includes(plugin) ? `${rootPath}/dom/` : rootPath + ] + + if (utilObjects.includes(plugin)) { + pluginPath = `${rootPath}/util/` + } + + if (domObjects.includes(plugin)) { + pluginPath = `${rootPath}/dom/` + } const pluginFilename = `${plugin.toLowerCase()}.js` diff --git a/js/src/alert.js b/js/src/alert.js index a8d0cc5da99b..9d8f69c46597 100644 --- a/js/src/alert.js +++ b/js/src/alert.js @@ -5,10 +5,16 @@ * -------------------------------------------------------------------------- */ +import { + jQuery as $, + TRANSITION_END, + emulateTransitionEnd, + getSelectorFromElement, + getTransitionDurationFromElement +} from './util/index' import Data from './dom/data' import EventHandler from './dom/eventHandler' import SelectorEngine from './dom/selectorEngine' -import Util from './util' /** * ------------------------------------------------------------------------ @@ -83,7 +89,7 @@ class Alert { // Private _getRootElement(element) { - const selector = Util.getSelectorFromElement(element) + const selector = getSelectorFromElement(element) let parent = false if (selector) { @@ -109,11 +115,11 @@ class Alert { return } - const transitionDuration = Util.getTransitionDurationFromElement(element) + const transitionDuration = getTransitionDurationFromElement(element) EventHandler - .one(element, Util.TRANSITION_END, (event) => this._destroyElement(element, event)) - Util.emulateTransitionEnd(element, transitionDuration) + .one(element, TRANSITION_END, (event) => this._destroyElement(element, event)) + emulateTransitionEnd(element, transitionDuration) } _destroyElement(element) { @@ -170,7 +176,6 @@ EventHandler * add .alert to jQuery only if jQuery is present */ -const $ = Util.jQuery if (typeof $ !== 'undefined') { const JQUERY_NO_CONFLICT = $.fn[NAME] $.fn[NAME] = Alert._jQueryInterface diff --git a/js/src/button.js b/js/src/button.js index 2cb6acabaea4..489fe9de3590 100644 --- a/js/src/button.js +++ b/js/src/button.js @@ -5,10 +5,12 @@ * -------------------------------------------------------------------------- */ +import { + jQuery as $ +} from './util/index' import Data from './dom/data' import EventHandler from './dom/eventHandler' import SelectorEngine from './dom/selectorEngine' -import Util from './util' /** * ------------------------------------------------------------------------ @@ -180,7 +182,6 @@ EventHandler.on(document, Event.BLUR_DATA_API, Selector.DATA_TOGGLE_CARROT, (eve * add .button to jQuery only if jQuery is present */ -const $ = Util.jQuery if (typeof $ !== 'undefined') { const JQUERY_NO_CONFLICT = $.fn[NAME] $.fn[NAME] = Button._jQueryInterface diff --git a/js/src/carousel.js b/js/src/carousel.js index 053659314164..25a683fc192b 100644 --- a/js/src/carousel.js +++ b/js/src/carousel.js @@ -5,11 +5,21 @@ * -------------------------------------------------------------------------- */ +import { + jQuery as $, + TRANSITION_END, + emulateTransitionEnd, + getSelectorFromElement, + getTransitionDurationFromElement, + isVisible, + makeArray, + triggerTransitionEnd, + typeCheckConfig +} from './util/index' import Data from './dom/data' import EventHandler from './dom/eventHandler' import Manipulator from './dom/manipulator' import SelectorEngine from './dom/selectorEngine' -import Util from './util' /** * ------------------------------------------------------------------------ @@ -143,7 +153,7 @@ class Carousel { nextWhenVisible() { // Don't call next when the page isn't visible // or the carousel or its parent isn't visible - if (!document.hidden && Util.isVisible(this._element)) { + if (!document.hidden && isVisible(this._element)) { this.next() } } @@ -160,7 +170,7 @@ class Carousel { } if (SelectorEngine.findOne(Selector.NEXT_PREV, this._element)) { - Util.triggerTransitionEnd(this._element) + triggerTransitionEnd(this._element) this.cycle(true) } @@ -233,7 +243,7 @@ class Carousel { ...Default, ...config } - Util.typeCheckConfig(NAME, config, DefaultType) + typeCheckConfig(NAME, config, DefaultType) return config } @@ -320,7 +330,7 @@ class Carousel { } } - Util.makeArray(SelectorEngine.find(Selector.ITEM_IMG, this._element)).forEach((itemImg) => { + makeArray(SelectorEngine.find(Selector.ITEM_IMG, this._element)).forEach((itemImg) => { EventHandler.on(itemImg, Event.DRAG_START, (e) => e.preventDefault()) }) @@ -356,7 +366,7 @@ class Carousel { _getItemIndex(element) { this._items = element && element.parentNode - ? Util.makeArray(SelectorEngine.find(Selector.ITEM, element.parentNode)) + ? makeArray(SelectorEngine.find(Selector.ITEM, element.parentNode)) : [] return this._items.indexOf(element) @@ -458,9 +468,6 @@ class Carousel { if (this._element.classList.contains(ClassName.SLIDE)) { nextElement.classList.add(orderClassName) - - Util.reflow(nextElement) - activeElement.classList.add(directionalClassName) nextElement.classList.add(directionalClassName) @@ -472,10 +479,10 @@ class Carousel { this._config.interval = this._config.defaultInterval || this._config.interval } - const transitionDuration = Util.getTransitionDurationFromElement(activeElement) + const transitionDuration = getTransitionDurationFromElement(activeElement) EventHandler - .one(activeElement, Util.TRANSITION_END, () => { + .one(activeElement, TRANSITION_END, () => { nextElement.classList.remove(directionalClassName) nextElement.classList.remove(orderClassName) nextElement.classList.add(ClassName.ACTIVE) @@ -496,7 +503,7 @@ class Carousel { }, 0) }) - Util.emulateTransitionEnd(activeElement, transitionDuration) + emulateTransitionEnd(activeElement, transitionDuration) } else { activeElement.classList.remove(ClassName.ACTIVE) nextElement.classList.add(ClassName.ACTIVE) @@ -557,7 +564,7 @@ class Carousel { } static _dataApiClickHandler(event) { - const selector = Util.getSelectorFromElement(this) + const selector = getSelectorFromElement(this) if (!selector) { return @@ -603,7 +610,7 @@ EventHandler .on(document, Event.CLICK_DATA_API, Selector.DATA_SLIDE, Carousel._dataApiClickHandler) EventHandler.on(window, Event.LOAD_DATA_API, () => { - const carousels = Util.makeArray(SelectorEngine.find(Selector.DATA_RIDE)) + const carousels = makeArray(SelectorEngine.find(Selector.DATA_RIDE)) for (let i = 0, len = carousels.length; i < len; i++) { Carousel._carouselInterface(carousels[i], Data.getData(carousels[i], DATA_KEY)) } @@ -616,7 +623,6 @@ EventHandler.on(window, Event.LOAD_DATA_API, () => { * add .carousel to jQuery only if jQuery is present */ -const $ = Util.jQuery if (typeof $ !== 'undefined') { const JQUERY_NO_CONFLICT = $.fn[NAME] $.fn[NAME] = Carousel._jQueryInterface diff --git a/js/src/collapse.js b/js/src/collapse.js index 9c2773754ccb..72e58c5f3b8f 100644 --- a/js/src/collapse.js +++ b/js/src/collapse.js @@ -5,11 +5,20 @@ * -------------------------------------------------------------------------- */ +import { + jQuery as $, + TRANSITION_END, + emulateTransitionEnd, + getSelectorFromElement, + getTransitionDurationFromElement, + isElement, + makeArray, + typeCheckConfig +} from './util/index' import Data from './dom/data' import EventHandler from './dom/eventHandler' import Manipulator from './dom/manipulator' import SelectorEngine from './dom/selectorEngine' -import Util from './util' /** * ------------------------------------------------------------------------ @@ -69,16 +78,16 @@ class Collapse { this._isTransitioning = false this._element = element this._config = this._getConfig(config) - this._triggerArray = Util.makeArray(SelectorEngine.find( + this._triggerArray = makeArray(SelectorEngine.find( `[data-toggle="collapse"][href="#${element.id}"],` + `[data-toggle="collapse"][data-target="#${element.id}"]` )) - const toggleList = Util.makeArray(SelectorEngine.find(Selector.DATA_TOGGLE)) + const toggleList = makeArray(SelectorEngine.find(Selector.DATA_TOGGLE)) for (let i = 0, len = toggleList.length; i < len; i++) { const elem = toggleList[i] - const selector = Util.getSelectorFromElement(elem) - const filterElement = Util.makeArray(SelectorEngine.find(selector)) + const selector = getSelectorFromElement(elem) + const filterElement = makeArray(SelectorEngine.find(selector)) .filter((foundElem) => foundElem === element) if (selector !== null && filterElement.length) { @@ -130,7 +139,7 @@ class Collapse { let activesData if (this._parent) { - actives = Util.makeArray(SelectorEngine.find(Selector.ACTIVES, this._parent)) + actives = makeArray(SelectorEngine.find(Selector.ACTIVES, this._parent)) .filter((elem) => { if (typeof this._config.parent === 'string') { return elem.getAttribute('data-parent') === this._config.parent @@ -201,11 +210,11 @@ class Collapse { const capitalizedDimension = dimension[0].toUpperCase() + dimension.slice(1) const scrollSize = `scroll${capitalizedDimension}` - const transitionDuration = Util.getTransitionDurationFromElement(this._element) + const transitionDuration = getTransitionDurationFromElement(this._element) - EventHandler.one(this._element, Util.TRANSITION_END, complete) + EventHandler.one(this._element, TRANSITION_END, complete) - Util.emulateTransitionEnd(this._element, transitionDuration) + emulateTransitionEnd(this._element, transitionDuration) this._element.style[dimension] = `${this._element[scrollSize]}px` } @@ -224,8 +233,6 @@ class Collapse { this._element.style[dimension] = `${this._element.getBoundingClientRect()[dimension]}px` - Util.reflow(this._element) - this._element.classList.add(ClassName.COLLAPSING) this._element.classList.remove(ClassName.COLLAPSE) this._element.classList.remove(ClassName.SHOW) @@ -234,7 +241,7 @@ class Collapse { if (triggerArrayLength > 0) { for (let i = 0; i < triggerArrayLength; i++) { const trigger = this._triggerArray[i] - const selector = Util.getSelectorFromElement(trigger) + const selector = getSelectorFromElement(trigger) if (selector !== null) { const elem = SelectorEngine.findOne(selector) @@ -257,10 +264,10 @@ class Collapse { } this._element.style[dimension] = '' - const transitionDuration = Util.getTransitionDurationFromElement(this._element) + const transitionDuration = getTransitionDurationFromElement(this._element) - EventHandler.one(this._element, Util.TRANSITION_END, complete) - Util.emulateTransitionEnd(this._element, transitionDuration) + EventHandler.one(this._element, TRANSITION_END, complete) + emulateTransitionEnd(this._element, transitionDuration) } setTransitioning(isTransitioning) { @@ -285,7 +292,7 @@ class Collapse { ...config } config.toggle = Boolean(config.toggle) // Coerce string values - Util.typeCheckConfig(NAME, config, DefaultType) + typeCheckConfig(NAME, config, DefaultType) return config } @@ -297,7 +304,7 @@ class Collapse { _getParent() { let parent - if (Util.isElement(this._config.parent)) { + if (isElement(this._config.parent)) { parent = this._config.parent // it's a jQuery object @@ -311,7 +318,7 @@ class Collapse { const selector = `[data-toggle="collapse"][data-parent="${this._config.parent}"]` - Util.makeArray(SelectorEngine.find(selector, parent)) + makeArray(SelectorEngine.find(selector, parent)) .forEach((element) => { this._addAriaAndCollapsedClass( Collapse._getTargetFromElement(element), @@ -342,7 +349,7 @@ class Collapse { // Static static _getTargetFromElement(element) { - const selector = Util.getSelectorFromElement(element) + const selector = getSelectorFromElement(element) return selector ? SelectorEngine.findOne(selector) : null } @@ -394,8 +401,8 @@ EventHandler.on(document, Event.CLICK_DATA_API, Selector.DATA_TOGGLE, function ( } const triggerData = Manipulator.getDataAttributes(this) - const selector = Util.getSelectorFromElement(this) - const selectorElements = Util.makeArray(SelectorEngine.find(selector)) + const selector = getSelectorFromElement(this) + const selectorElements = makeArray(SelectorEngine.find(selector)) selectorElements.forEach((element) => { const data = Data.getData(element, DATA_KEY) @@ -422,7 +429,6 @@ EventHandler.on(document, Event.CLICK_DATA_API, Selector.DATA_TOGGLE, function ( * add .collapse to jQuery only if jQuery is present */ -const $ = Util.jQuery if (typeof $ !== 'undefined') { const JQUERY_NO_CONFLICT = $.fn[NAME] $.fn[NAME] = Collapse._jQueryInterface diff --git a/js/src/dom/eventHandler.js b/js/src/dom/eventHandler.js index 3b42388b8ebe..d92920cdaac2 100644 --- a/js/src/dom/eventHandler.js +++ b/js/src/dom/eventHandler.js @@ -5,8 +5,10 @@ * -------------------------------------------------------------------------- */ +import { + jQuery as $ +} from '../util/index' import Polyfill from './polyfill' -import Util from '../util' /** * ------------------------------------------------------------------------ @@ -248,7 +250,6 @@ const EventHandler = { const typeEvent = event.replace(stripNameRegex, '') const inNamespace = event !== typeEvent const isNative = nativeEvents.indexOf(typeEvent) > -1 - const $ = Util.jQuery let jQueryEvent let bubbles = true diff --git a/js/src/dom/polyfill.js b/js/src/dom/polyfill.js index cbae0f668e1d..579b7a10ed76 100644 --- a/js/src/dom/polyfill.js +++ b/js/src/dom/polyfill.js @@ -5,7 +5,9 @@ * -------------------------------------------------------------------------- */ -import Util from '../util' +import { + getUID +} from '../util/index' /* istanbul ignore next */ const Polyfill = (() => { @@ -48,7 +50,7 @@ const Polyfill = (() => { const hasId = Boolean(this.id) if (!hasId) { - this.id = Util.getUID('scope') + this.id = getUID('scope') } let nodeList = null diff --git a/js/src/dom/selectorEngine.js b/js/src/dom/selectorEngine.js index 95b5a9fb50ee..151bb54c7553 100644 --- a/js/src/dom/selectorEngine.js +++ b/js/src/dom/selectorEngine.js @@ -6,7 +6,9 @@ */ import Polyfill from './polyfill' -import Util from '../util' +import { + makeArray +} from '../util/index' /** * ------------------------------------------------------------------------ @@ -44,7 +46,7 @@ const SelectorEngine = { return null } - const children = Util.makeArray(element.children) + const children = makeArray(element.children) return children.filter((child) => this.matches(child, selector)) }, diff --git a/js/src/dropdown.js b/js/src/dropdown.js index 46aff8f884d8..f5150a016fbd 100644 --- a/js/src/dropdown.js +++ b/js/src/dropdown.js @@ -5,12 +5,19 @@ * -------------------------------------------------------------------------- */ +import { + jQuery as $, + getSelectorFromElement, + isElement, + makeArray, + noop, + typeCheckConfig +} from './util/index' import Data from './dom/data' import EventHandler from './dom/eventHandler' import Manipulator from './dom/manipulator' import Popper from 'popper.js' import SelectorEngine from './dom/selectorEngine' -import Util from './util' /** * ------------------------------------------------------------------------ @@ -159,7 +166,7 @@ class Dropdown { if (this._config.reference === 'parent') { referenceElement = parent - } else if (Util.isElement(this._config.reference)) { + } else if (isElement(this._config.reference)) { referenceElement = this._config.reference // Check if it's jQuery element @@ -182,9 +189,9 @@ class Dropdown { // only needed because of broken event delegation on iOS // https://www.quirksmode.org/blog/archives/2014/02/mouse_event_bub.html if ('ontouchstart' in document.documentElement && - !Util.makeArray(SelectorEngine.closest(parent, Selector.NAVBAR_NAV)).length) { - Util.makeArray(document.body.children) - .forEach((elem) => EventHandler.on(elem, 'mouseover', null, Util.noop())) + !makeArray(SelectorEngine.closest(parent, Selector.NAVBAR_NAV)).length) { + makeArray(document.body.children) + .forEach((elem) => EventHandler.on(elem, 'mouseover', null, noop())) } this._element.focus() @@ -272,7 +279,7 @@ class Dropdown { ...config } - Util.typeCheckConfig( + typeCheckConfig( NAME, config, this.constructor.DefaultType @@ -389,7 +396,7 @@ class Dropdown { return } - const toggles = Util.makeArray(SelectorEngine.find(Selector.DATA_TOGGLE)) + const toggles = makeArray(SelectorEngine.find(Selector.DATA_TOGGLE)) for (let i = 0, len = toggles.length; i < len; i++) { const parent = Dropdown._getParentFromElement(toggles[i]) const context = Data.getData(toggles[i], DATA_KEY) @@ -425,8 +432,8 @@ class Dropdown { // If this is a touch-enabled device we remove the extra // empty mouseover listeners we added for iOS support if ('ontouchstart' in document.documentElement) { - Util.makeArray(document.body.children) - .forEach((elem) => EventHandler.off(elem, 'mouseover', null, Util.noop())) + makeArray(document.body.children) + .forEach((elem) => EventHandler.off(elem, 'mouseover', null, noop())) } toggles[i].setAttribute('aria-expanded', 'false') @@ -439,7 +446,7 @@ class Dropdown { static _getParentFromElement(element) { let parent - const selector = Util.getSelectorFromElement(element) + const selector = getSelectorFromElement(element) if (selector) { parent = SelectorEngine.findOne(selector) @@ -482,7 +489,7 @@ class Dropdown { return } - const items = Util.makeArray(SelectorEngine.find(Selector.VISIBLE_ITEMS, parent)) + const items = makeArray(SelectorEngine.find(Selector.VISIBLE_ITEMS, parent)) if (!items.length) { return @@ -535,7 +542,6 @@ EventHandler * add .dropdown to jQuery only if jQuery is present */ -const $ = Util.jQuery if (typeof $ !== 'undefined') { const JQUERY_NO_CONFLICT = $.fn[NAME] $.fn[NAME] = Dropdown._jQueryInterface diff --git a/js/src/index.js b/js/src/index.js index 8a0044fe53d3..4d46d84b9526 100644 --- a/js/src/index.js +++ b/js/src/index.js @@ -16,10 +16,8 @@ import ScrollSpy from './scrollspy' import Tab from './tab' import Toast from './toast' import Tooltip from './tooltip' -import Util from './util' export { - Util, Alert, Button, Carousel, diff --git a/js/src/modal.js b/js/src/modal.js index 0897b317ab61..e134e2497f07 100644 --- a/js/src/modal.js +++ b/js/src/modal.js @@ -5,11 +5,20 @@ * -------------------------------------------------------------------------- */ +import { + jQuery as $, + TRANSITION_END, + emulateTransitionEnd, + getSelectorFromElement, + getTransitionDurationFromElement, + isVisible, + makeArray, + typeCheckConfig +} from './util/index' import Data from './dom/data' import EventHandler from './dom/eventHandler' import Manipulator from './dom/manipulator' import SelectorEngine from './dom/selectorEngine' -import Util from './util' /** * ------------------------------------------------------------------------ @@ -184,10 +193,10 @@ class Modal { if (transition) { - const transitionDuration = Util.getTransitionDurationFromElement(this._element) + const transitionDuration = getTransitionDurationFromElement(this._element) - EventHandler.one(this._element, Util.TRANSITION_END, (event) => this._hideModal(event)) - Util.emulateTransitionEnd(this._element, transitionDuration) + EventHandler.one(this._element, TRANSITION_END, (event) => this._hideModal(event)) + emulateTransitionEnd(this._element, transitionDuration) } else { this._hideModal() } @@ -228,7 +237,7 @@ class Modal { ...Default, ...config } - Util.typeCheckConfig(NAME, config, DefaultType) + typeCheckConfig(NAME, config, DefaultType) return config } @@ -251,10 +260,6 @@ class Modal { this._element.scrollTop = 0 } - if (transition) { - Util.reflow(this._element) - } - this._element.classList.add(ClassName.SHOW) if (this._config.focus) { @@ -272,10 +277,10 @@ class Modal { } if (transition) { - const transitionDuration = Util.getTransitionDurationFromElement(this._dialog) + const transitionDuration = getTransitionDurationFromElement(this._dialog) - EventHandler.one(this._dialog, Util.TRANSITION_END, transitionComplete) - Util.emulateTransitionEnd(this._dialog, transitionDuration) + EventHandler.one(this._dialog, TRANSITION_END, transitionComplete) + emulateTransitionEnd(this._dialog, transitionDuration) } else { transitionComplete() } @@ -363,10 +368,6 @@ class Modal { } }) - if (animate) { - Util.reflow(this._backdrop) - } - this._backdrop.classList.add(ClassName.SHOW) if (!callback) { @@ -378,10 +379,10 @@ class Modal { return } - const backdropTransitionDuration = Util.getTransitionDurationFromElement(this._backdrop) + const backdropTransitionDuration = getTransitionDurationFromElement(this._backdrop) - EventHandler.one(this._backdrop, Util.TRANSITION_END, callback) - Util.emulateTransitionEnd(this._backdrop, backdropTransitionDuration) + EventHandler.one(this._backdrop, TRANSITION_END, callback) + emulateTransitionEnd(this._backdrop, backdropTransitionDuration) } else if (!this._isShown && this._backdrop) { this._backdrop.classList.remove(ClassName.SHOW) @@ -393,9 +394,9 @@ class Modal { } if (this._element.classList.contains(ClassName.FADE)) { - const backdropTransitionDuration = Util.getTransitionDurationFromElement(this._backdrop) - EventHandler.one(this._backdrop, Util.TRANSITION_END, callbackRemove) - Util.emulateTransitionEnd(this._backdrop, backdropTransitionDuration) + const backdropTransitionDuration = getTransitionDurationFromElement(this._backdrop) + EventHandler.one(this._backdrop, TRANSITION_END, callbackRemove) + emulateTransitionEnd(this._backdrop, backdropTransitionDuration) } else { callbackRemove() } @@ -439,7 +440,7 @@ class Modal { // while $(DOMNode).css('padding-right') returns the calculated value or 0 if not set // Adjust fixed content padding - Util.makeArray(SelectorEngine.find(Selector.FIXED_CONTENT)) + makeArray(SelectorEngine.find(Selector.FIXED_CONTENT)) .forEach((element) => { const actualPadding = element.style.paddingRight const calculatedPadding = window.getComputedStyle(element)['padding-right'] @@ -448,7 +449,7 @@ class Modal { }) // Adjust sticky content margin - Util.makeArray(SelectorEngine.find(Selector.STICKY_CONTENT)) + makeArray(SelectorEngine.find(Selector.STICKY_CONTENT)) .forEach((element) => { const actualMargin = element.style.marginRight const calculatedMargin = window.getComputedStyle(element)['margin-right'] @@ -469,7 +470,7 @@ class Modal { _resetScrollbar() { // Restore fixed content padding - Util.makeArray(SelectorEngine.find(Selector.FIXED_CONTENT)) + makeArray(SelectorEngine.find(Selector.FIXED_CONTENT)) .forEach((element) => { const padding = Manipulator.getDataAttribute(element, 'padding-right') if (typeof padding !== 'undefined') { @@ -479,7 +480,7 @@ class Modal { }) // Restore sticky content and navbar-toggler margin - Util.makeArray(SelectorEngine.find(`${Selector.STICKY_CONTENT}`)) + makeArray(SelectorEngine.find(`${Selector.STICKY_CONTENT}`)) .forEach((element) => { const margin = Manipulator.getDataAttribute(element, 'margin-right') if (typeof margin !== 'undefined') { @@ -546,7 +547,7 @@ class Modal { EventHandler.on(document, Event.CLICK_DATA_API, Selector.DATA_TOGGLE, function (event) { let target - const selector = Util.getSelectorFromElement(this) + const selector = getSelectorFromElement(this) if (selector) { target = SelectorEngine.findOne(selector) @@ -569,7 +570,7 @@ EventHandler.on(document, Event.CLICK_DATA_API, Selector.DATA_TOGGLE, function ( } EventHandler.one(target, Event.HIDDEN, () => { - if (Util.isVisible(this)) { + if (isVisible(this)) { this.focus() } }) @@ -589,7 +590,6 @@ EventHandler.on(document, Event.CLICK_DATA_API, Selector.DATA_TOGGLE, function ( * ------------------------------------------------------------------------ */ -const $ = Util.jQuery if (typeof $ !== 'undefined') { const JQUERY_NO_CONFLICT = $.fn[NAME] $.fn[NAME] = Modal._jQueryInterface diff --git a/js/src/popover.js b/js/src/popover.js index ce5edcd5c4df..958beaa3d066 100644 --- a/js/src/popover.js +++ b/js/src/popover.js @@ -5,10 +5,12 @@ * -------------------------------------------------------------------------- */ +import { + jQuery as $ +} from './util/index' import Data from './dom/data' import SelectorEngine from './dom/selectorEngine' import Tooltip from './tooltip' -import Util from './util' /** * ------------------------------------------------------------------------ @@ -177,7 +179,6 @@ class Popover extends Tooltip { * ------------------------------------------------------------------------ */ -const $ = Util.jQuery if (typeof $ !== 'undefined') { const JQUERY_NO_CONFLICT = $.fn[NAME] $.fn[NAME] = Popover._jQueryInterface diff --git a/js/src/scrollspy.js b/js/src/scrollspy.js index f5d1008af78d..74b60b7348c1 100644 --- a/js/src/scrollspy.js +++ b/js/src/scrollspy.js @@ -5,11 +5,17 @@ * -------------------------------------------------------------------------- */ +import { + jQuery as $, + getSelectorFromElement, + getUID, + makeArray, + typeCheckConfig +} from './util/index' import Data from './dom/data' import EventHandler from './dom/eventHandler' import Manipulator from './dom/manipulator' import SelectorEngine from './dom/selectorEngine' -import Util from './util' /** * ------------------------------------------------------------------------ @@ -118,12 +124,12 @@ class ScrollSpy { this._scrollHeight = this._getScrollHeight() - const targets = Util.makeArray(SelectorEngine.find(this._selector)) + const targets = makeArray(SelectorEngine.find(this._selector)) targets .map((element) => { let target - const targetSelector = Util.getSelectorFromElement(element) + const targetSelector = getSelectorFromElement(element) if (targetSelector) { target = SelectorEngine.findOne(targetSelector) @@ -174,13 +180,13 @@ class ScrollSpy { if (typeof config.target !== 'string') { let id = config.target.id if (!id) { - id = Util.getUID(NAME) + id = getUID(NAME) config.target.id = id } config.target = `#${id}` } - Util.typeCheckConfig(NAME, config, DefaultType) + typeCheckConfig(NAME, config, DefaultType) return config } @@ -284,7 +290,7 @@ class ScrollSpy { } _clear() { - Util.makeArray(SelectorEngine.find(this._selector)) + makeArray(SelectorEngine.find(this._selector)) .filter((node) => node.classList.contains(ClassName.ACTIVE)) .forEach((node) => node.classList.remove(ClassName.ACTIVE)) } @@ -321,7 +327,7 @@ class ScrollSpy { */ EventHandler.on(window, Event.LOAD_DATA_API, () => { - Util.makeArray(SelectorEngine.find(Selector.DATA_SPY)) + makeArray(SelectorEngine.find(Selector.DATA_SPY)) .forEach((spy) => new ScrollSpy(spy, Manipulator.getDataAttributes(spy))) }) @@ -331,7 +337,6 @@ EventHandler.on(window, Event.LOAD_DATA_API, () => { * ------------------------------------------------------------------------ */ -const $ = Util.jQuery if (typeof $ !== 'undefined') { const JQUERY_NO_CONFLICT = $.fn[NAME] $.fn[NAME] = ScrollSpy._jQueryInterface diff --git a/js/src/tab.js b/js/src/tab.js index dab38c51c9c6..bc3ab268c8d8 100644 --- a/js/src/tab.js +++ b/js/src/tab.js @@ -5,10 +5,17 @@ * -------------------------------------------------------------------------- */ +import { + jQuery as $, + TRANSITION_END, + emulateTransitionEnd, + getSelectorFromElement, + getTransitionDurationFromElement, + makeArray +} from './util/index' import Data from './dom/data' import EventHandler from './dom/eventHandler' import SelectorEngine from './dom/selectorEngine' -import Util from './util' /** * ------------------------------------------------------------------------ @@ -80,11 +87,11 @@ class Tab { let target let previous const listElement = SelectorEngine.closest(this._element, Selector.NAV_LIST_GROUP) - const selector = Util.getSelectorFromElement(this._element) + const selector = getSelectorFromElement(this._element) if (listElement) { const itemSelector = listElement.nodeName === 'UL' || listElement.nodeName === 'OL' ? Selector.ACTIVE_UL : Selector.ACTIVE - previous = Util.makeArray(SelectorEngine.find(itemSelector, listElement)) + previous = makeArray(SelectorEngine.find(itemSelector, listElement)) previous = previous[previous.length - 1] } @@ -153,11 +160,11 @@ class Tab { ) if (active && isTransitioning) { - const transitionDuration = Util.getTransitionDurationFromElement(active) + const transitionDuration = getTransitionDurationFromElement(active) active.classList.remove(ClassName.SHOW) - EventHandler.one(active, Util.TRANSITION_END, complete) - Util.emulateTransitionEnd(active, transitionDuration) + EventHandler.one(active, TRANSITION_END, complete) + emulateTransitionEnd(active, transitionDuration) } else { complete() } @@ -183,8 +190,6 @@ class Tab { element.setAttribute('aria-selected', true) } - Util.reflow(element) - if (element.classList.contains(ClassName.FADE)) { element.classList.add(ClassName.SHOW) } @@ -193,7 +198,7 @@ class Tab { const dropdownElement = SelectorEngine.closest(element, Selector.DROPDOWN) if (dropdownElement) { - Util.makeArray(SelectorEngine.find(Selector.DROPDOWN_TOGGLE)) + makeArray(SelectorEngine.find(Selector.DROPDOWN_TOGGLE)) .forEach((dropdown) => dropdown.classList.add(ClassName.ACTIVE)) } @@ -242,9 +247,9 @@ EventHandler.on(document, Event.CLICK_DATA_API, Selector.DATA_TOGGLE, function ( * ------------------------------------------------------------------------ * jQuery * ------------------------------------------------------------------------ + * add .tab to jQuery only if jQuery is present */ -const $ = Util.jQuery if (typeof $ !== 'undefined') { const JQUERY_NO_CONFLICT = $.fn[NAME] $.fn[NAME] = Tab._jQueryInterface diff --git a/js/src/toast.js b/js/src/toast.js index 6e878e5c8067..fba559197faf 100644 --- a/js/src/toast.js +++ b/js/src/toast.js @@ -5,10 +5,16 @@ * -------------------------------------------------------------------------- */ +import { + jQuery as $, + TRANSITION_END, + emulateTransitionEnd, + getTransitionDurationFromElement, + typeCheckConfig +} from './util/index' import Data from './dom/data' import EventHandler from './dom/eventHandler' import Manipulator from './dom/manipulator' -import Util from './util' /** * ------------------------------------------------------------------------ @@ -104,10 +110,10 @@ class Toast { this._element.classList.remove(ClassName.HIDE) this._element.classList.add(ClassName.SHOWING) if (this._config.animation) { - const transitionDuration = Util.getTransitionDurationFromElement(this._element) + const transitionDuration = getTransitionDurationFromElement(this._element) - EventHandler.one(this._element, Util.TRANSITION_END, complete) - Util.emulateTransitionEnd(this._element, transitionDuration) + EventHandler.one(this._element, TRANSITION_END, complete) + emulateTransitionEnd(this._element, transitionDuration) } else { complete() } @@ -153,7 +159,7 @@ class Toast { ...typeof config === 'object' && config ? config : {} } - Util.typeCheckConfig( + typeCheckConfig( NAME, config, this.constructor.DefaultType @@ -179,10 +185,10 @@ class Toast { this._element.classList.remove(ClassName.SHOW) if (this._config.animation) { - const transitionDuration = Util.getTransitionDurationFromElement(this._element) + const transitionDuration = getTransitionDurationFromElement(this._element) - EventHandler.one(this._element, Util.TRANSITION_END, complete) - Util.emulateTransitionEnd(this._element, transitionDuration) + EventHandler.one(this._element, TRANSITION_END, complete) + emulateTransitionEnd(this._element, transitionDuration) } else { complete() } @@ -221,7 +227,6 @@ class Toast { * add .toast to jQuery only if jQuery is present */ -const $ = Util.jQuery if (typeof $ !== 'undefined') { const JQUERY_NO_CONFLICT = $.fn[NAME] $.fn[NAME] = Toast._jQueryInterface diff --git a/js/src/tooltip.js b/js/src/tooltip.js index 8c2dbe7b2354..13a5c62b7e4f 100644 --- a/js/src/tooltip.js +++ b/js/src/tooltip.js @@ -5,16 +5,27 @@ * -------------------------------------------------------------------------- */ +import { + jQuery as $, + TRANSITION_END, + emulateTransitionEnd, + findShadowRoot, + getTransitionDurationFromElement, + getUID, + isElement, + makeArray, + noop, + typeCheckConfig +} from './util/index' import { DefaultWhitelist, sanitizeHtml -} from './tools/sanitizer' +} from './util/sanitizer' import Data from './dom/data' import EventHandler from './dom/eventHandler' import Manipulator from './dom/manipulator' import Popper from 'popper.js' import SelectorEngine from './dom/selectorEngine' -import Util from './util' /** * ------------------------------------------------------------------------ @@ -257,7 +268,7 @@ class Tooltip { if (this.isWithContent() && this._isEnabled) { const showEvent = EventHandler.trigger(this.element, this.constructor.Event.SHOW) - const shadowRoot = Util.findShadowRoot(this.element) + const shadowRoot = findShadowRoot(this.element) const isInTheDom = shadowRoot !== null ? shadowRoot.contains(this.element) : this.element.ownerDocument.documentElement.contains(this.element) @@ -267,7 +278,7 @@ class Tooltip { } const tip = this.getTipElement() - const tipId = Util.getUID(this.constructor.NAME) + const tipId = getUID(this.constructor.NAME) tip.setAttribute('id', tipId) this.element.setAttribute('aria-describedby', tipId) @@ -323,8 +334,8 @@ class Tooltip { // only needed because of broken event delegation on iOS // https://www.quirksmode.org/blog/archives/2014/02/mouse_event_bub.html if ('ontouchstart' in document.documentElement) { - Util.makeArray(document.body.children).forEach((element) => { - EventHandler.on(element, 'mouseover', Util.noop()) + makeArray(document.body.children).forEach((element) => { + EventHandler.on(element, 'mouseover', noop()) }) } @@ -343,9 +354,9 @@ class Tooltip { } if (this.tip.classList.contains(ClassName.FADE)) { - const transitionDuration = Util.getTransitionDurationFromElement(this.tip) - EventHandler.one(this.tip, Util.TRANSITION_END, complete) - Util.emulateTransitionEnd(this.tip, transitionDuration) + const transitionDuration = getTransitionDurationFromElement(this.tip) + EventHandler.one(this.tip, TRANSITION_END, complete) + emulateTransitionEnd(this.tip, transitionDuration) } else { complete() } @@ -381,8 +392,8 @@ class Tooltip { // If this is a touch-enabled device we remove the extra // empty mouseover listeners we added for iOS support if ('ontouchstart' in document.documentElement) { - Util.makeArray(document.body.children) - .forEach((element) => EventHandler.off(element, 'mouseover', Util.noop)) + makeArray(document.body.children) + .forEach((element) => EventHandler.off(element, 'mouseover', noop)) } this._activeTrigger[Trigger.CLICK] = false @@ -390,9 +401,10 @@ class Tooltip { this._activeTrigger[Trigger.HOVER] = false if (this.tip.classList.contains(ClassName.FADE)) { - const transitionDuration = Util.getTransitionDurationFromElement(tip) - EventHandler.one(tip, Util.TRANSITION_END, complete) - Util.emulateTransitionEnd(tip, transitionDuration) + const transitionDuration = getTransitionDurationFromElement(tip) + + EventHandler.one(tip, TRANSITION_END, complete) + emulateTransitionEnd(tip, transitionDuration) } else { complete() } @@ -507,7 +519,7 @@ class Tooltip { return document.body } - if (Util.isElement(this.config.container)) { + if (isElement(this.config.container)) { return this.config.container } @@ -705,7 +717,7 @@ class Tooltip { config.content = config.content.toString() } - Util.typeCheckConfig( + typeCheckConfig( NAME, config, this.constructor.DefaultType @@ -795,8 +807,9 @@ class Tooltip { * ------------------------------------------------------------------------ * jQuery * ------------------------------------------------------------------------ + * add .tooltip to jQuery only if jQuery is present */ -const $ = Util.jQuery + if (typeof $ !== 'undefined') { const JQUERY_NO_CONFLICT = $.fn[NAME] $.fn[NAME] = Tooltip._jQueryInterface diff --git a/js/src/util.js b/js/src/util.js deleted file mode 100644 index 8b6226f61ccc..000000000000 --- a/js/src/util.js +++ /dev/null @@ -1,179 +0,0 @@ -/** - * -------------------------------------------------------------------------- - * Bootstrap (v4.3.1): util.js - * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) - * -------------------------------------------------------------------------- - */ - -/** - * ------------------------------------------------------------------------ - * Private TransitionEnd Helpers - * ------------------------------------------------------------------------ - */ - -const MAX_UID = 1000000 -const MILLISECONDS_MULTIPLIER = 1000 - -// Shoutout AngusCroll (https://goo.gl/pxwQGp) -function toType(obj) { - return {}.toString.call(obj).match(/\s([a-z]+)/i)[1].toLowerCase() -} - -/** - * -------------------------------------------------------------------------- - * Public Util Api - * -------------------------------------------------------------------------- - */ - -const Util = { - TRANSITION_END: 'transitionend', - - getUID(prefix) { - do { - // eslint-disable-next-line no-bitwise - prefix += ~~(Math.random() * MAX_UID) // "~~" acts like a faster Math.floor() here - } while (document.getElementById(prefix)) - return prefix - }, - - getSelectorFromElement(element) { - let selector = element.getAttribute('data-target') - - if (!selector || selector === '#') { - const hrefAttr = element.getAttribute('href') - selector = hrefAttr && hrefAttr !== '#' ? hrefAttr.trim() : '' - } - - try { - return document.querySelector(selector) ? selector : null - } catch (err) { - return null - } - }, - - getTransitionDurationFromElement(element) { - if (!element) { - return 0 - } - - // Get transition-duration of the element - let transitionDuration = window.getComputedStyle(element).transitionDuration - let transitionDelay = window.getComputedStyle(element).transitionDelay - - const floatTransitionDuration = parseFloat(transitionDuration) - const floatTransitionDelay = parseFloat(transitionDelay) - - // Return 0 if element or transition duration is not found - if (!floatTransitionDuration && !floatTransitionDelay) { - return 0 - } - - // If multiple durations are defined, take the first - transitionDuration = transitionDuration.split(',')[0] - transitionDelay = transitionDelay.split(',')[0] - - return (parseFloat(transitionDuration) + parseFloat(transitionDelay)) * MILLISECONDS_MULTIPLIER - }, - - reflow(element) { - return element.offsetHeight - }, - - triggerTransitionEnd(element) { - element.dispatchEvent(new Event(Util.TRANSITION_END)) - }, - - isElement(obj) { - return (obj[0] || obj).nodeType - }, - - emulateTransitionEnd(element, duration) { - let called = false - const durationPadding = 5 - const emulatedDuration = duration + durationPadding - function listener() { - called = true - element.removeEventListener(Util.TRANSITION_END, listener) - } - - element.addEventListener(Util.TRANSITION_END, listener) - setTimeout(() => { - if (!called) { - Util.triggerTransitionEnd(element) - } - }, emulatedDuration) - }, - - typeCheckConfig(componentName, config, configTypes) { - for (const property in configTypes) { - if (Object.prototype.hasOwnProperty.call(configTypes, property)) { - const expectedTypes = configTypes[property] - const value = config[property] - const valueType = value && Util.isElement(value) - ? 'element' : toType(value) - - if (!new RegExp(expectedTypes).test(valueType)) { - throw new Error( - `${componentName.toUpperCase()}: ` + - `Option "${property}" provided type "${valueType}" ` + - `but expected type "${expectedTypes}".`) - } - } - } - }, - - makeArray(nodeList) { - if (!nodeList) { - return [] - } - - return [].slice.call(nodeList) - }, - - isVisible(element) { - if (!element) { - return false - } - - if (element.style !== null && element.parentNode !== null && typeof element.parentNode.style !== 'undefined') { - return element.style.display !== 'none' && - element.parentNode.style.display !== 'none' && - element.style.visibility !== 'hidden' - } - return false - }, - - findShadowRoot(element) { - if (!document.documentElement.attachShadow) { - return null - } - - // Can find the shadow root otherwise it'll return the document - if (typeof element.getRootNode === 'function') { - const root = element.getRootNode() - return root instanceof ShadowRoot ? root : null - } - - if (element instanceof ShadowRoot) { - return element - } - - // when we don't find a shadow root - if (!element.parentNode) { - return null - } - - return Util.findShadowRoot(element.parentNode) - }, - - noop() { - // eslint-disable-next-line no-empty-function - return function () {} - }, - - get jQuery() { - return window.jQuery - } -} - -export default Util diff --git a/js/src/util/index.js b/js/src/util/index.js new file mode 100644 index 000000000000..61cb31ed10fa --- /dev/null +++ b/js/src/util/index.js @@ -0,0 +1,173 @@ +/** + * -------------------------------------------------------------------------- + * Bootstrap (v4.3.1): util/index.js + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) + * -------------------------------------------------------------------------- + */ + +const MAX_UID = 1000000 +const MILLISECONDS_MULTIPLIER = 1000 +const TRANSITION_END = 'transitionend' +const jQuery = window.jQuery + +// Shoutout AngusCroll (https://goo.gl/pxwQGp) +function toType(obj) { + return {}.toString.call(obj).match(/\s([a-z]+)/i)[1].toLowerCase() +} + +/** + * -------------------------------------------------------------------------- + * Public Util Api + * -------------------------------------------------------------------------- + */ + +const getUID = (prefix) => { + do { + // eslint-disable-next-line no-bitwise + prefix += ~~(Math.random() * MAX_UID) // "~~" acts like a faster Math.floor() here + } while (document.getElementById(prefix)) + return prefix +} + +const getSelectorFromElement = (element) => { + let selector = element.getAttribute('data-target') + + if (!selector || selector === '#') { + const hrefAttr = element.getAttribute('href') + selector = hrefAttr && hrefAttr !== '#' ? hrefAttr.trim() : '' + } + + try { + return document.querySelector(selector) ? selector : null + } catch (err) { + return null + } +} + +const getTransitionDurationFromElement = (element) => { + if (!element) { + return 0 + } + + // Get transition-duration of the element + let transitionDuration = window.getComputedStyle(element).transitionDuration + let transitionDelay = window.getComputedStyle(element).transitionDelay + + const floatTransitionDuration = parseFloat(transitionDuration) + const floatTransitionDelay = parseFloat(transitionDelay) + + // Return 0 if element or transition duration is not found + if (!floatTransitionDuration && !floatTransitionDelay) { + return 0 + } + + // If multiple durations are defined, take the first + transitionDuration = transitionDuration.split(',')[0] + transitionDelay = transitionDelay.split(',')[0] + + return (parseFloat(transitionDuration) + parseFloat(transitionDelay)) * MILLISECONDS_MULTIPLIER +} + +const triggerTransitionEnd = (element) => { + element.dispatchEvent(new Event(TRANSITION_END)) +} + +const isElement = (obj) => (obj[0] || obj).nodeType + +const emulateTransitionEnd = (element, duration) => { + let called = false + const durationPadding = 5 + const emulatedDuration = duration + durationPadding + function listener() { + called = true + element.removeEventListener(TRANSITION_END, listener) + } + + element.addEventListener(TRANSITION_END, listener) + setTimeout(() => { + if (!called) { + triggerTransitionEnd(element) + } + }, emulatedDuration) +} + +const typeCheckConfig = (componentName, config, configTypes) => { + Object.keys(configTypes) + .forEach((property) => { + const expectedTypes = configTypes[property] + const value = config[property] + const valueType = value && isElement(value) + ? 'element' : toType(value) + + if (!new RegExp(expectedTypes).test(valueType)) { + throw new Error( + `${componentName.toUpperCase()}: ` + + `Option "${property}" provided type "${valueType}" ` + + `but expected type "${expectedTypes}".`) + } + }) +} + +const makeArray = (nodeList) => { + if (!nodeList) { + return [] + } + + return [].slice.call(nodeList) +} + +const isVisible = (element) => { + if (!element) { + return false + } + + if (element.style !== null && element.parentNode !== null && typeof element.parentNode.style !== 'undefined') { + return element.style.display !== 'none' && + element.parentNode.style.display !== 'none' && + element.style.visibility !== 'hidden' + } + + return false +} + +const findShadowRoot = (element) => { + if (!document.documentElement.attachShadow) { + return null + } + + // Can find the shadow root otherwise it'll return the document + if (typeof element.getRootNode === 'function') { + const root = element.getRootNode() + return root instanceof ShadowRoot ? root : null + } + + if (element instanceof ShadowRoot) { + return element + } + + // when we don't find a shadow root + if (!element.parentNode) { + return null + } + + return findShadowRoot(element.parentNode) +} + +// eslint-disable-next-line no-empty-function +const noop = () => function () {} + +export { + jQuery, + TRANSITION_END, + getUID, + getSelectorFromElement, + getTransitionDurationFromElement, + triggerTransitionEnd, + isElement, + emulateTransitionEnd, + typeCheckConfig, + makeArray, + isVisible, + findShadowRoot, + noop +} diff --git a/js/src/tools/sanitizer.js b/js/src/util/sanitizer.js similarity index 89% rename from js/src/tools/sanitizer.js rename to js/src/util/sanitizer.js index 3c1608fee8e3..f8bb172a9552 100644 --- a/js/src/tools/sanitizer.js +++ b/js/src/util/sanitizer.js @@ -1,11 +1,13 @@ /** * -------------------------------------------------------------------------- - * Bootstrap (v4.3.1): tools/sanitizer.js + * Bootstrap (v4.3.1): util/sanitizer.js * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) * -------------------------------------------------------------------------- */ -import Util from '../util' +import { + makeArray +} from './index' const uriAttrs = [ 'background', @@ -20,40 +22,6 @@ const uriAttrs = [ const ARIA_ATTRIBUTE_PATTERN = /^aria-[\w-]*$/i -export const DefaultWhitelist = { - // Global attributes allowed on any supplied element below. - '*': ['class', 'dir', 'id', 'lang', 'role', ARIA_ATTRIBUTE_PATTERN], - a: ['target', 'href', 'title', 'rel'], - area: [], - b: [], - br: [], - col: [], - code: [], - div: [], - em: [], - hr: [], - h1: [], - h2: [], - h3: [], - h4: [], - h5: [], - h6: [], - i: [], - img: ['src', 'alt', 'title', 'width', 'height'], - li: [], - ol: [], - p: [], - pre: [], - s: [], - small: [], - span: [], - sub: [], - sup: [], - strong: [], - u: [], - ul: [] -} - /** * A pattern that recognizes a commonly useful subset of URLs that are safe. * @@ -68,7 +36,7 @@ const SAFE_URL_PATTERN = /^(?:(?:https?|mailto|ftp|tel|file):|[^&:/?#]*(?:[/?#]| */ const DATA_URL_PATTERN = /^data:(?:image\/(?:bmp|gif|jpeg|jpg|png|tiff|webp)|video\/(?:mpeg|mp4|ogg|webm)|audio\/(?:mp3|oga|ogg|opus));base64,[a-z0-9+/]+=*$/i -function allowedAttribute(attr, allowedAttributeList) { +const allowedAttribute = (attr, allowedAttributeList) => { const attrName = attr.nodeName.toLowerCase() if (allowedAttributeList.indexOf(attrName) !== -1) { @@ -91,8 +59,42 @@ function allowedAttribute(attr, allowedAttributeList) { return false } +export const DefaultWhitelist = { + // Global attributes allowed on any supplied element below. + '*': ['class', 'dir', 'id', 'lang', 'role', ARIA_ATTRIBUTE_PATTERN], + a: ['target', 'href', 'title', 'rel'], + area: [], + b: [], + br: [], + col: [], + code: [], + div: [], + em: [], + hr: [], + h1: [], + h2: [], + h3: [], + h4: [], + h5: [], + h6: [], + i: [], + img: ['src', 'alt', 'title', 'width', 'height'], + li: [], + ol: [], + p: [], + pre: [], + s: [], + small: [], + span: [], + sub: [], + sup: [], + strong: [], + u: [], + ul: [] +} + export function sanitizeHtml(unsafeHtml, whiteList, sanitizeFn) { - if (unsafeHtml.length === 0) { + if (!unsafeHtml.length) { return unsafeHtml } @@ -103,19 +105,19 @@ export function sanitizeHtml(unsafeHtml, whiteList, sanitizeFn) { const domParser = new window.DOMParser() const createdDocument = domParser.parseFromString(unsafeHtml, 'text/html') const whitelistKeys = Object.keys(whiteList) - const elements = Util.makeArray(createdDocument.body.querySelectorAll('*')) + const elements = makeArray(createdDocument.body.querySelectorAll('*')) for (let i = 0, len = elements.length; i < len; i++) { const el = elements[i] const elName = el.nodeName.toLowerCase() - if (whitelistKeys.indexOf(el.nodeName.toLowerCase()) === -1) { + if (whitelistKeys.indexOf(elName) === -1) { el.parentNode.removeChild(el) continue } - const attributeList = Util.makeArray(el.attributes) + const attributeList = makeArray(el.attributes) const whitelistedAttributes = [].concat(whiteList['*'] || [], whiteList[elName] || []) attributeList.forEach((attr) => { diff --git a/js/tests/index.html b/js/tests/index.html deleted file mode 100644 index 77c29f8f85fd..000000000000 --- a/js/tests/index.html +++ /dev/null @@ -1,142 +0,0 @@ - - - - - - Bootstrap Plugin Test Suite - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-
-
-
- - diff --git a/js/tests/integration/bundle.js b/js/tests/integration/bundle.js index 23caeab73200..f2d3b0d7bec1 100644 --- a/js/tests/integration/bundle.js +++ b/js/tests/integration/bundle.js @@ -2,8 +2,6 @@ import 'popper.js' import bootstrap from '../../../dist/js/bootstrap' window.addEventListener('load', () => { - document.getElementById('resultUID').innerHTML = bootstrap.Util.getUID('bs') - - bootstrap.Util.makeArray(document.querySelectorAll('[data-toggle="tooltip"]')) + Array.from(document.querySelectorAll('[data-toggle="tooltip"]')) .map((tooltipNode) => new bootstrap.Tooltip(tooltipNode)) }) diff --git a/js/tests/integration/index.html b/js/tests/integration/index.html index e5b33a84dcd4..f14330f47ef1 100644 --- a/js/tests/integration/index.html +++ b/js/tests/integration/index.html @@ -13,11 +13,7 @@

Hello, world!

-
-
- Util.getUID: - -
+
diff --git a/js/tests/karma.conf.js b/js/tests/karma.conf.js index 641ac88949cd..704d3c541d22 100644 --- a/js/tests/karma.conf.js +++ b/js/tests/karma.conf.js @@ -11,6 +11,7 @@ const { const jqueryFile = process.env.USE_OLD_JQUERY ? 'https://code.jquery.com/jquery-1.9.1.min.js' : 'node_modules/jquery/dist/jquery.slim.min.js' const bundle = process.env.BUNDLE === 'true' const browserStack = process.env.BROWSER === 'true' +const debug = process.env.DEBUG === 'true' const frameworks = [ 'qunit', @@ -28,11 +29,11 @@ const detectBrowsers = { usePhantomJS: false, postDetection(availableBrowser) { if (typeof process.env.TRAVIS_JOB_ID !== 'undefined' || availableBrowser.includes('Chrome')) { - return ['ChromeHeadless'] + return debug ? ['Chrome'] : ['ChromeHeadless'] } if (availableBrowser.includes('Firefox')) { - return ['FirefoxHeadless'] + return debug ? ['Firefox'] : ['FirefoxHeadless'] } throw new Error('Please install Firefox or Chrome') @@ -76,7 +77,8 @@ if (bundle) { conf.detectBrowsers = detectBrowsers files = files.concat([ jqueryFile, - 'dist/js/bootstrap.js' + 'dist/js/bootstrap.js', + 'js/tests/unit/*.js' ]) } else if (browserStack) { conf.hostname = ip.address() @@ -93,7 +95,8 @@ if (bundle) { reporters.push('BrowserStack') files = files.concat([ 'node_modules/jquery/dist/jquery.slim.min.js', - 'js/coverage/dist/util.js', + 'js/coverage/dist/util/util.js', + 'js/coverage/dist/util/sanitizer.js', 'js/coverage/dist/dom/polyfill.js', 'js/coverage/dist/dom/eventHandler.js', 'js/coverage/dist/dom/selectorEngine.js', @@ -103,7 +106,8 @@ if (bundle) { 'js/coverage/dist/tooltip.js', 'js/coverage/dist/!(util|index|tooltip).js', // include all of our js/dist files except util.js, index.js and tooltip.js 'js/tests/unit/*.js', - 'js/tests/unit/dom/*.js' + 'js/tests/unit/dom/*.js', + 'js/tests/unit/util/*.js' ]) } else { frameworks.push('detectBrowsers') @@ -115,7 +119,8 @@ if (bundle) { ) files = files.concat([ jqueryFile, - 'js/coverage/dist/util.js', + 'js/coverage/dist/util/util.js', + 'js/coverage/dist/util/sanitizer.js', 'js/coverage/dist/dom/polyfill.js', 'js/coverage/dist/dom/eventHandler.js', 'js/coverage/dist/dom/selectorEngine.js', @@ -125,7 +130,8 @@ if (bundle) { 'js/coverage/dist/tooltip.js', 'js/coverage/dist/!(util|index|tooltip).js', // include all of our js/dist files except util.js, index.js and tooltip.js 'js/tests/unit/*.js', - 'js/tests/unit/dom/*.js' + 'js/tests/unit/dom/*.js', + 'js/tests/unit/util/*.js' ]) reporters.push('coverage-istanbul') conf.customLaunchers = customLaunchers @@ -153,9 +159,12 @@ if (bundle) { } } } -} -files.push('js/tests/unit/*.js') + if (debug) { + conf.singleRun = false + conf.autoWatch = true + } +} conf.frameworks = frameworks conf.plugins = plugins diff --git a/js/tests/unit/.eslintrc.json b/js/tests/unit/.eslintrc.json index dfcf1eaa5003..19ab5d998619 100644 --- a/js/tests/unit/.eslintrc.json +++ b/js/tests/unit/.eslintrc.json @@ -8,6 +8,7 @@ "bootstrap": false, "sinon": false, "Util": false, + "Sanitizer": false, "Data": false, "Alert": false, "Button": false, diff --git a/js/tests/unit/modal.js b/js/tests/unit/modal.js index a9a3df838be0..6939c5e5bfb3 100644 --- a/js/tests/unit/modal.js +++ b/js/tests/unit/modal.js @@ -695,13 +695,10 @@ $(function () { ].join('') var $modal = $(modalHTML).appendTo('#qunit-fixture') - var expectedTransitionDuration = 300 - var spy = sinon.spy(Util, 'getTransitionDurationFromElement') $modal.on('shown.bs.modal', function () { - assert.ok(spy.returned(expectedTransitionDuration)) $style.remove() - spy.restore() + assert.ok(true) done() }) .bootstrapModal('show') diff --git a/js/tests/unit/tooltip.js b/js/tests/unit/tooltip.js index 85fafe4b8c8c..c866d36387f4 100644 --- a/js/tests/unit/tooltip.js +++ b/js/tests/unit/tooltip.js @@ -1152,24 +1152,6 @@ $(function () { assert.strictEqual(tooltip.config.template.indexOf('onError'), -1) }) - QUnit.test('should sanitize template by removing tags with XSS', function (assert) { - assert.expect(1) - - var $trigger = $('') - .appendTo('#qunit-fixture') - .bootstrapTooltip({ - template: [ - '
', - ' Click me', - ' Some content', - '
' - ].join('') - }) - - var tooltip = Tooltip._getInstance($trigger[0]) - assert.strictEqual(tooltip.config.template.indexOf('script'), -1) - }) - QUnit.test('should allow custom sanitization rules', function (assert) { assert.expect(2) diff --git a/js/tests/unit/util.js b/js/tests/unit/util/index.js similarity index 98% rename from js/tests/unit/util.js rename to js/tests/unit/util/index.js index db1412a3ba18..2d52ca59a0f5 100644 --- a/js/tests/unit/util.js +++ b/js/tests/unit/util/index.js @@ -1,8 +1,6 @@ $(function () { 'use strict' - window.Util = typeof bootstrap !== 'undefined' ? bootstrap.Util : Util - QUnit.module('util', { afterEach: function () { $('#qunit-fixture').html('') diff --git a/js/tests/unit/util/sanitizer.js b/js/tests/unit/util/sanitizer.js new file mode 100644 index 000000000000..d33f6e93536c --- /dev/null +++ b/js/tests/unit/util/sanitizer.js @@ -0,0 +1,30 @@ +$(function () { + 'use strict' + + QUnit.module('sanitizer', { + afterEach: function () { + $('#qunit-fixture').html('') + } + }) + + QUnit.test('should export a default white list', function (assert) { + assert.expect(1) + + assert.ok(Sanitizer.DefaultWhitelist) + }) + + QUnit.test('should sanitize template by removing tags with XSS', function (assert) { + assert.expect(1) + + var template = [ + '
', + ' Click me', + ' Some content', + '
' + ].join('') + + var result = Sanitizer.sanitizeHtml(template, Sanitizer.DefaultWhitelist, null) + + assert.strictEqual(result.indexOf('script'), -1) + }) +}) diff --git a/js/tests/visual/toast.html b/js/tests/visual/toast.html index a527eab13da4..da5236e3e509 100644 --- a/js/tests/visual/toast.html +++ b/js/tests/visual/toast.html @@ -60,13 +60,13 @@

Toast Bootstrap Visual Test