diff --git a/build/rollup.config.js b/build/rollup.config.js index b3a7929ec4..e2819f8c04 100644 --- a/build/rollup.config.js +++ b/build/rollup.config.js @@ -8,7 +8,7 @@ const banner = require('./banner.js') const BUNDLE = process.env.BUNDLE === 'true' let fileDest = 'bootstrap.js' -const external = ['jquery', 'popper.js'] +const external = ['popper.js'] const plugins = [ babel({ exclude: 'node_modules/**', // Only transpile our source code diff --git a/js/src/collapse.js b/js/src/collapse.js index dae60e1227..838eea4d1c 100644 --- a/js/src/collapse.js +++ b/js/src/collapse.js @@ -311,13 +311,13 @@ class Collapse { const selector = `[data-toggle="collapse"][data-parent="${this._config.parent}"]` - const elements = Util.makeArray(SelectorEngine.find(selector, parent)) - elements.forEach((element) => { - this._addAriaAndCollapsedClass( - Collapse._getTargetFromElement(element), - [element] - ) - }) + Util.makeArray(SelectorEngine.find(selector, parent)) + .forEach((element) => { + this._addAriaAndCollapsedClass( + Collapse._getTargetFromElement(element), + [element] + ) + }) return parent } diff --git a/js/src/dom/data.js b/js/src/dom/data.js index 2dfaad91a4..9aa3f13b14 100644 --- a/js/src/dom/data.js +++ b/js/src/dom/data.js @@ -5,64 +5,62 @@ * -------------------------------------------------------------------------- */ -const Data = (() => { - /** - * ------------------------------------------------------------------------ - * Constants - * ------------------------------------------------------------------------ - */ - - const mapData = (() => { - const storeData = {} - let id = 1 - return { - set(element, key, data) { - if (typeof element.key === 'undefined') { - element.key = { - key, - id - } - id++ - } - - storeData[element.key.id] = data - }, - get(element, key) { - if (!element || typeof element.key === 'undefined') { - return null - } - - const keyProperties = element.key - if (keyProperties.key === key) { - return storeData[keyProperties.id] - } - return null - }, - delete(element, key) { - if (typeof element.key === 'undefined') { - return - } - - const keyProperties = element.key - if (keyProperties.key === key) { - delete storeData[keyProperties.id] - delete element.key - } - } - } - })() +/** + * ------------------------------------------------------------------------ + * Constants + * ------------------------------------------------------------------------ + */ +const mapData = (() => { + const storeData = {} + let id = 1 return { - setData(instance, key, data) { - mapData.set(instance, key, data) + set(element, key, data) { + if (typeof element.key === 'undefined') { + element.key = { + key, + id + } + id++ + } + + storeData[element.key.id] = data }, - getData(instance, key) { - return mapData.get(instance, key) + get(element, key) { + if (!element || typeof element.key === 'undefined') { + return null + } + + const keyProperties = element.key + if (keyProperties.key === key) { + return storeData[keyProperties.id] + } + return null }, - removeData(instance, key) { - mapData.delete(instance, key) + delete(element, key) { + if (typeof element.key === 'undefined') { + return + } + + const keyProperties = element.key + if (keyProperties.key === key) { + delete storeData[keyProperties.id] + delete element.key + } } } })() +const Data = { + setData(instance, key, data) { + mapData.set(instance, key, data) + }, + getData(instance, key) { + return mapData.get(instance, key) + }, + removeData(instance, key) { + mapData.delete(instance, key) + } +} + export default Data diff --git a/js/src/dom/eventHandler.js b/js/src/dom/eventHandler.js index 259f575ed9..84570f874c 100644 --- a/js/src/dom/eventHandler.js +++ b/js/src/dom/eventHandler.js @@ -8,312 +8,310 @@ import Util from '../util' * -------------------------------------------------------------------------- */ -const EventHandler = (() => { - /** - * ------------------------------------------------------------------------ - * Constants - * ------------------------------------------------------------------------ - */ +/** + * ------------------------------------------------------------------------ + * Constants + * ------------------------------------------------------------------------ + */ - const namespaceRegex = /[^.]*(?=\..*)\.|.*/ - const stripNameRegex = /\..*/ - const keyEventRegex = /^key/ - const stripUidRegex = /::\d+$/ - const eventRegistry = {} // Events storage - let uidEvent = 1 - const customEvents = { - mouseenter: 'mouseover', - mouseleave: 'mouseout' - } - const nativeEvents = [ - 'click', 'dblclick', 'mouseup', 'mousedown', 'contextmenu', - 'mousewheel', 'DOMMouseScroll', - 'mouseover', 'mouseout', 'mousemove', 'selectstart', 'selectend', - 'keydown', 'keypress', 'keyup', - 'orientationchange', - 'touchstart', 'touchmove', 'touchend', 'touchcancel', - 'pointerdown', 'pointermove', 'pointerup', 'pointerleave', 'pointercancel', - 'gesturestart', 'gesturechange', 'gestureend', - 'focus', 'blur', 'change', 'reset', 'select', 'submit', 'focusin', 'focusout', - 'load', 'unload', 'beforeunload', 'resize', 'move', 'DOMContentLoaded', 'readystatechange', - 'error', 'abort', 'scroll' - ] +const namespaceRegex = /[^.]*(?=\..*)\.|.*/ +const stripNameRegex = /\..*/ +const keyEventRegex = /^key/ +const stripUidRegex = /::\d+$/ +const eventRegistry = {} // Events storage +let uidEvent = 1 +const customEvents = { + mouseenter: 'mouseover', + mouseleave: 'mouseout' +} +const nativeEvents = [ + 'click', 'dblclick', 'mouseup', 'mousedown', 'contextmenu', + 'mousewheel', 'DOMMouseScroll', + 'mouseover', 'mouseout', 'mousemove', 'selectstart', 'selectend', + 'keydown', 'keypress', 'keyup', + 'orientationchange', + 'touchstart', 'touchmove', 'touchend', 'touchcancel', + 'pointerdown', 'pointermove', 'pointerup', 'pointerleave', 'pointercancel', + 'gesturestart', 'gesturechange', 'gestureend', + 'focus', 'blur', 'change', 'reset', 'select', 'submit', 'focusin', 'focusout', + 'load', 'unload', 'beforeunload', 'resize', 'move', 'DOMContentLoaded', 'readystatechange', + 'error', 'abort', 'scroll' +] - /** - * ------------------------------------------------------------------------ - * Private methods - * ------------------------------------------------------------------------ - */ +/** + * ------------------------------------------------------------------------ + * Private methods + * ------------------------------------------------------------------------ + */ - function getUidEvent(element, uid) { - return uid && `${uid}::${uidEvent++}` || element.uidEvent || uidEvent++ +function getUidEvent(element, uid) { + return uid && `${uid}::${uidEvent++}` || element.uidEvent || uidEvent++ +} + +function getEvent(element) { + const uid = getUidEvent(element) + element.uidEvent = uid + + return eventRegistry[uid] = eventRegistry[uid] || {} +} + +function fixEvent(event, element) { + // Add which for key events + if (event.which === null && keyEventRegex.test(event.type)) { + event.which = event.charCode !== null ? event.charCode : event.keyCode } - function getEvent(element) { - const uid = getUidEvent(element) - element.uidEvent = uid + event.delegateTarget = element +} - return eventRegistry[uid] = eventRegistry[uid] || {} - } - - function fixEvent(event, element) { - // Add which for key events - if (event.which === null && keyEventRegex.test(event.type)) { - event.which = event.charCode !== null ? event.charCode : event.keyCode +function bootstrapHandler(element, fn) { + return function handler(event) { + fixEvent(event, element) + if (handler.oneOff) { + EventHandler.off(element, event.type, fn) } - event.delegateTarget = element + return fn.apply(element, [event]) } +} - function bootstrapHandler(element, fn) { - return function handler(event) { - fixEvent(event, element) - if (handler.oneOff) { - EventHandler.off(element, event.type, fn) - } - - return fn.apply(element, [event]) - } - } - - function bootstrapDelegationHandler(element, selector, fn) { - return function handler(event) { - const domElements = element.querySelectorAll(selector) - for (let target = event.target; target && target !== this; target = target.parentNode) { - for (let i = domElements.length; i--;) { - if (domElements[i] === target) { - fixEvent(event, target) - if (handler.oneOff) { - EventHandler.off(element, event.type, fn) - } - - return fn.apply(target, [event]) +function bootstrapDelegationHandler(element, selector, fn) { + return function handler(event) { + const domElements = element.querySelectorAll(selector) + for (let target = event.target; target && target !== this; target = target.parentNode) { + for (let i = domElements.length; i--;) { + if (domElements[i] === target) { + fixEvent(event, target) + if (handler.oneOff) { + EventHandler.off(element, event.type, fn) } + + return fn.apply(target, [event]) } } - - // To please ESLint - return null - } - } - - function findHandler(events, handler, delegationSelector = null) { - for (const uid in events) { - if (!Object.prototype.hasOwnProperty.call(events, uid)) { - continue - } - - const event = events[uid] - if (event.originalHandler === handler && event.delegationSelector === delegationSelector) { - return events[uid] - } } + // To please ESLint return null } +} - function normalizeParams(originalTypeEvent, handler, delegationFn) { - const delegation = typeof handler === 'string' - const originalHandler = delegation ? delegationFn : handler - - // allow to get the native events from namespaced events ('click.bs.button' --> 'click') - let typeEvent = originalTypeEvent.replace(stripNameRegex, '') - - const custom = customEvents[typeEvent] - if (custom) { - typeEvent = custom +function findHandler(events, handler, delegationSelector = null) { + for (const uid in events) { + if (!Object.prototype.hasOwnProperty.call(events, uid)) { + continue } - const isNative = nativeEvents.indexOf(typeEvent) > -1 - if (!isNative) { - typeEvent = originalTypeEvent + const event = events[uid] + if (event.originalHandler === handler && event.delegationSelector === delegationSelector) { + return events[uid] } - - return [delegation, originalHandler, typeEvent] } - function addHandler(element, originalTypeEvent, handler, delegationFn, oneOff) { + return null +} + +function normalizeParams(originalTypeEvent, handler, delegationFn) { + const delegation = typeof handler === 'string' + const originalHandler = delegation ? delegationFn : handler + + // allow to get the native events from namespaced events ('click.bs.button' --> 'click') + let typeEvent = originalTypeEvent.replace(stripNameRegex, '') + + const custom = customEvents[typeEvent] + if (custom) { + typeEvent = custom + } + + const isNative = nativeEvents.indexOf(typeEvent) > -1 + if (!isNative) { + typeEvent = originalTypeEvent + } + + return [delegation, originalHandler, typeEvent] +} + +function addHandler(element, originalTypeEvent, handler, delegationFn, oneOff) { + if (typeof originalTypeEvent !== 'string' || (typeof element === 'undefined' || element === null)) { + return + } + + if (!handler) { + handler = delegationFn + delegationFn = null + } + + const [delegation, originalHandler, typeEvent] = normalizeParams(originalTypeEvent, handler, delegationFn) + + const events = getEvent(element) + const handlers = events[typeEvent] || (events[typeEvent] = {}) + const previousFn = findHandler(handlers, originalHandler, delegation ? handler : null) + + if (previousFn) { + previousFn.oneOff = previousFn.oneOff && oneOff + return + } + + const uid = getUidEvent(originalHandler, originalTypeEvent.replace(namespaceRegex, '')) + const fn = !delegation ? bootstrapHandler(element, handler) : bootstrapDelegationHandler(element, handler, delegationFn) + + fn.delegationSelector = delegation ? handler : null + fn.originalHandler = originalHandler + fn.oneOff = oneOff + fn.uidEvent = uid + handlers[uid] = fn + + element.addEventListener(typeEvent, fn, delegation) +} + +function removeHandler(element, events, typeEvent, handler, delegationSelector) { + const fn = findHandler(events[typeEvent], handler, delegationSelector) + if (fn === null) { + return + } + + element.removeEventListener(typeEvent, fn, Boolean(delegationSelector)) + delete events[typeEvent][fn.uidEvent] +} + +function removeNamespacedHandlers(element, events, typeEvent, namespace) { + const storeElementEvent = events[typeEvent] || {} + for (const handlerKey in storeElementEvent) { + if (!Object.prototype.hasOwnProperty.call(storeElementEvent, handlerKey)) { + continue + } + + if (handlerKey.indexOf(namespace) > -1) { + const event = storeElementEvent[handlerKey] + removeHandler(element, events, typeEvent, event.originalHandler, event.delegationSelector) + } + } +} + +const EventHandler = { + on(element, event, handler, delegationFn) { + addHandler(element, event, handler, delegationFn, false) + }, + + one(element, event, handler, delegationFn) { + addHandler(element, event, handler, delegationFn, true) + }, + + off(element, originalTypeEvent, handler, delegationFn) { if (typeof originalTypeEvent !== 'string' || (typeof element === 'undefined' || element === null)) { return } - if (!handler) { - handler = delegationFn - delegationFn = null - } - const [delegation, originalHandler, typeEvent] = normalizeParams(originalTypeEvent, handler, delegationFn) - const events = getEvent(element) - const handlers = events[typeEvent] || (events[typeEvent] = {}) - const previousFn = findHandler(handlers, originalHandler, delegation ? handler : null) + const inNamespace = typeEvent !== originalTypeEvent + const events = getEvent(element) - if (previousFn) { - previousFn.oneOff = previousFn.oneOff && oneOff - return - } - - const uid = getUidEvent(originalHandler, originalTypeEvent.replace(namespaceRegex, '')) - const fn = !delegation ? bootstrapHandler(element, handler) : bootstrapDelegationHandler(element, handler, delegationFn) - - fn.delegationSelector = delegation ? handler : null - fn.originalHandler = originalHandler - fn.oneOff = oneOff - fn.uidEvent = uid - handlers[uid] = fn - - element.addEventListener(typeEvent, fn, delegation) - } - - function removeHandler(element, events, typeEvent, handler, delegationSelector) { - const fn = findHandler(events[typeEvent], handler, delegationSelector) - if (fn === null) { - return - } - - element.removeEventListener(typeEvent, fn, Boolean(delegationSelector)) - delete events[typeEvent][fn.uidEvent] - } - - function removeNamespacedHandlers(element, events, typeEvent, namespace) { - const storeElementEvent = events[typeEvent] || {} - for (const handlerKey in storeElementEvent) { - if (!Object.prototype.hasOwnProperty.call(storeElementEvent, handlerKey)) { - continue - } - - if (handlerKey.indexOf(namespace) > -1) { - const event = storeElementEvent[handlerKey] - removeHandler(element, events, typeEvent, event.originalHandler, event.delegationSelector) - } - } - } - - return { - on(element, event, handler, delegationFn) { - addHandler(element, event, handler, delegationFn, false) - }, - - one(element, event, handler, delegationFn) { - addHandler(element, event, handler, delegationFn, true) - }, - - off(element, originalTypeEvent, handler, delegationFn) { - if (typeof originalTypeEvent !== 'string' || (typeof element === 'undefined' || element === null)) { + if (typeof originalHandler !== 'undefined') { + // Simplest case: handler is passed, remove that listener ONLY. + if (!events || !events[typeEvent]) { return } - const [delegation, originalHandler, typeEvent] = normalizeParams(originalTypeEvent, handler, delegationFn) + removeHandler(element, events, typeEvent, originalHandler, delegation ? handler : null) + return + } - const inNamespace = typeEvent !== originalTypeEvent - const events = getEvent(element) - - if (typeof originalHandler !== 'undefined') { - // Simplest case: handler is passed, remove that listener ONLY. - if (!events || !events[typeEvent]) { - return - } - - removeHandler(element, events, typeEvent, originalHandler, delegation ? handler : null) - return - } - - const isNamespace = originalTypeEvent.charAt(0) === '.' - if (isNamespace) { - for (const elementEvent in events) { - if (!Object.prototype.hasOwnProperty.call(events, elementEvent)) { - continue - } - - removeNamespacedHandlers(element, events, elementEvent, originalTypeEvent.substr(1)) - } - } - - const storeElementEvent = events[typeEvent] || {} - for (const keyHandlers in storeElementEvent) { - if (!Object.prototype.hasOwnProperty.call(storeElementEvent, keyHandlers)) { + const isNamespace = originalTypeEvent.charAt(0) === '.' + if (isNamespace) { + for (const elementEvent in events) { + if (!Object.prototype.hasOwnProperty.call(events, elementEvent)) { continue } - const handlerKey = keyHandlers.replace(stripUidRegex, '') - if (!inNamespace || originalTypeEvent.indexOf(handlerKey) > -1) { - const event = storeElementEvent[keyHandlers] - removeHandler(element, events, typeEvent, event.originalHandler, event.delegationSelector) - } + removeNamespacedHandlers(element, events, elementEvent, originalTypeEvent.substr(1)) } - }, + } - trigger(element, event, args) { - if (typeof event !== 'string' || - (typeof element === 'undefined' || element === null)) { - return null + const storeElementEvent = events[typeEvent] || {} + for (const keyHandlers in storeElementEvent) { + if (!Object.prototype.hasOwnProperty.call(storeElementEvent, keyHandlers)) { + continue } - const typeEvent = event.replace(stripNameRegex, '') - const inNamespace = event !== typeEvent - const isNative = nativeEvents.indexOf(typeEvent) > -1 - - const $ = Util.jQuery - let jQueryEvent - - let bubbles = true - let nativeDispatch = true - let defaultPrevented = false - - if (inNamespace && typeof $ !== 'undefined') { - jQueryEvent = $.Event(event, args) - - $(element).trigger(jQueryEvent) - bubbles = !jQueryEvent.isPropagationStopped() - nativeDispatch = !jQueryEvent.isImmediatePropagationStopped() - defaultPrevented = jQueryEvent.isDefaultPrevented() + const handlerKey = keyHandlers.replace(stripUidRegex, '') + if (!inNamespace || originalTypeEvent.indexOf(handlerKey) > -1) { + const event = storeElementEvent[keyHandlers] + removeHandler(element, events, typeEvent, event.originalHandler, event.delegationSelector) } + } + }, - let evt = null - if (isNative) { - evt = document.createEvent('HTMLEvents') - evt.initEvent(typeEvent, bubbles, true) - } else { - evt = new CustomEvent(event, { - bubbles, - cancelable: true + trigger(element, event, args) { + if (typeof event !== 'string' || + (typeof element === 'undefined' || element === null)) { + return null + } + + const typeEvent = event.replace(stripNameRegex, '') + const inNamespace = event !== typeEvent + const isNative = nativeEvents.indexOf(typeEvent) > -1 + + const $ = Util.jQuery + let jQueryEvent + + let bubbles = true + let nativeDispatch = true + let defaultPrevented = false + + if (inNamespace && typeof $ !== 'undefined') { + jQueryEvent = $.Event(event, args) + + $(element).trigger(jQueryEvent) + bubbles = !jQueryEvent.isPropagationStopped() + nativeDispatch = !jQueryEvent.isImmediatePropagationStopped() + defaultPrevented = jQueryEvent.isDefaultPrevented() + } + + let evt = null + if (isNative) { + evt = document.createEvent('HTMLEvents') + evt.initEvent(typeEvent, bubbles, true) + } else { + evt = new CustomEvent(event, { + bubbles, + cancelable: true + }) + } + + // merge custom informations in our event + if (typeof args !== 'undefined') { + Object.keys(args) + .forEach((key) => { + Object.defineProperty(evt, key, { + get() { + return args[key] + } + }) + }) + } + + if (defaultPrevented) { + evt.preventDefault() + + if (!Polyfill.defaultPreventedPreservedOnDispatch) { + Object.defineProperty(evt, 'defaultPrevented', { + get: () => true }) } - - // merge custom informations in our event - if (typeof args !== 'undefined') { - Object.keys(args) - .forEach((key) => { - Object.defineProperty(evt, key, { - get() { - return args[key] - } - }) - }) - } - - if (defaultPrevented) { - evt.preventDefault() - - if (!Polyfill.defaultPreventedPreservedOnDispatch) { - Object.defineProperty(evt, 'defaultPrevented', { - get: () => true - }) - } - } - - if (nativeDispatch) { - element.dispatchEvent(evt) - } - - if (evt.defaultPrevented && typeof jQueryEvent !== 'undefined') { - jQueryEvent.preventDefault() - } - - return evt } + + if (nativeDispatch) { + element.dispatchEvent(evt) + } + + if (evt.defaultPrevented && typeof jQueryEvent !== 'undefined') { + jQueryEvent.preventDefault() + } + + return evt } -})() +} /* istanbul ignore next */ // focusin and focusout polyfill diff --git a/js/src/dom/polyfill.js b/js/src/dom/polyfill.js index 45defb76e7..ff7ba1d179 100644 --- a/js/src/dom/polyfill.js +++ b/js/src/dom/polyfill.js @@ -77,8 +77,9 @@ const Polyfill = (() => { } // matches polyfill (see: https://mzl.la/2ikXneG) - if (!Element.prototype.matches) { - Element.prototype.matches = + let matches = Element.prototype.matches + if (!matches) { + matches = Element.prototype.msMatchesSelector || Element.prototype.webkitMatchesSelector } @@ -86,15 +87,16 @@ const Polyfill = (() => { // closest polyfill (see: https://mzl.la/2vXggaI) let closest if (!Element.prototype.closest) { + const nodeText = 3 closest = (element, selector) => { let ancestor = element do { - if (ancestor.matches(selector)) { + if (matches.call(ancestor, selector)) { return ancestor } ancestor = ancestor.parentElement - } while (ancestor !== null && ancestor.nodeType === Node.ELEMENT_NODE) + } while (ancestor !== null && ancestor.nodeType === Node.ELEMENT_NODE && ancestor.nodeType !== nodeText) return null } @@ -186,6 +188,7 @@ const Polyfill = (() => { defaultPreventedPreservedOnDispatch, focusIn: typeof window.onfocusin === 'undefined', closest, + matches, find, findOne } diff --git a/js/src/dom/selectorEngine.js b/js/src/dom/selectorEngine.js index c2eec95a7e..ee2f004850 100644 --- a/js/src/dom/selectorEngine.js +++ b/js/src/dom/selectorEngine.js @@ -8,93 +8,93 @@ import Util from '../util' * -------------------------------------------------------------------------- */ -const SelectorEngine = (() => { - /** - * ------------------------------------------------------------------------ - * Constants - * ------------------------------------------------------------------------ - */ +/** + * ------------------------------------------------------------------------ + * Constants + * ------------------------------------------------------------------------ + */ - const closest = Polyfill.closest - const find = Polyfill.find - const findOne = Polyfill.findOne +const closest = Polyfill.closest +const matchesFn = Polyfill.matches +const find = Polyfill.find +const findOne = Polyfill.findOne +const nodeText = 3 - return { - matches(element, selector) { - return element.matches(selector) - }, +const SelectorEngine = { + matches(element, selector) { + return matchesFn.call(element, selector) + }, - find(selector, element = document.documentElement) { - if (typeof selector !== 'string') { - return null - } - - return find.call(element, selector) - }, - - findOne(selector, element = document.documentElement) { - if (typeof selector !== 'string') { - return null - } - - return findOne.call(element, selector) - }, - - children(element, selector) { - if (typeof selector !== 'string') { - return null - } - - const children = Util.makeArray(element.children) - return children.filter((child) => this.matches(child, selector)) - }, - - parents(element, selector) { - if (typeof selector !== 'string') { - return null - } - - const parents = [] - - let ancestor = element.parentNode - while (ancestor && ancestor.nodeType === Node.ELEMENT_NODE) { - if (ancestor.matches(selector)) { - parents.push(ancestor) - } - - ancestor = ancestor.parentNode - } - - return parents - }, - - closest(element, selector) { - if (typeof selector !== 'string') { - return null - } - - return closest(element, selector) - }, - - prev(element, selector) { - if (typeof selector !== 'string') { - return null - } - - const siblings = [] - - let previous = element.previousSibling - while (previous) { - if (previous.matches(selector)) { - siblings.push(previous) - } - - previous = previous.previousSibling - } - - return siblings + find(selector, element = document.documentElement) { + if (typeof selector !== 'string') { + return null } + + return find.call(element, selector) + }, + + findOne(selector, element = document.documentElement) { + if (typeof selector !== 'string') { + return null + } + + return findOne.call(element, selector) + }, + + children(element, selector) { + if (typeof selector !== 'string') { + return null + } + + const children = Util.makeArray(element.children) + return children.filter((child) => this.matches(child, selector)) + }, + + parents(element, selector) { + if (typeof selector !== 'string') { + return null + } + + const parents = [] + + let ancestor = element.parentNode + while (ancestor && ancestor.nodeType === Node.ELEMENT_NODE && ancestor.nodeType !== nodeText) { + if (this.matches(ancestor, selector)) { + parents.push(ancestor) + } + + ancestor = ancestor.parentNode + } + + return parents + }, + + closest(element, selector) { + if (typeof selector !== 'string') { + return null + } + + return closest(element, selector) + }, + + prev(element, selector) { + if (typeof selector !== 'string') { + return null + } + + const siblings = [] + + let previous = element.previousSibling + while (previous && previous.nodeType === Node.ELEMENT_NODE && previous.nodeType !== nodeText) { + if (this.matches(previous, selector)) { + siblings.push(previous) + } + + previous = previous.previousSibling + } + + return siblings } -})() +} export default SelectorEngine diff --git a/js/src/dropdown.js b/js/src/dropdown.js index 9a22a69916..8f3f8dd895 100644 --- a/js/src/dropdown.js +++ b/js/src/dropdown.js @@ -313,7 +313,7 @@ class Dropdown { } _detectNavbar() { - return Util.makeArray(SelectorEngine.closest(this._element, '.navbar')).length > 0 + return Boolean(SelectorEngine.closest(this._element, '.navbar')) } _getOffset() { @@ -367,7 +367,6 @@ class Dropdown { if (!data) { data = new Dropdown(element, _config) - Data.setData(element, DATA_KEY, data) } if (typeof config === 'string') { diff --git a/js/src/modal.js b/js/src/modal.js index 8e56fc5b29..e2d61621d3 100644 --- a/js/src/modal.js +++ b/js/src/modal.js @@ -394,9 +394,8 @@ 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(backdropTransitionDuration) + Util.emulateTransitionEnd(this._backdrop, backdropTransitionDuration) } else { callbackRemove() } diff --git a/js/src/tooltip.js b/js/src/tooltip.js index 93880bb8e4..43c11aa1d9 100644 --- a/js/src/tooltip.js +++ b/js/src/tooltip.js @@ -127,7 +127,7 @@ class Tooltip { * Popper - https://popper.js.org */ if (typeof Popper === 'undefined') { - throw new TypeError('Bootstrap\'s tooltips require Popper.js (https://popper.js.org/)') + throw new TypeError('Bootstrap\'s tooltips require Popper.js (https://popper.js.org)') } // private @@ -201,7 +201,7 @@ class Tooltip { if (!context) { context = new this.constructor( - event.currentTarget, + event.delegateTarget, this._getDelegateConfig() ) Data.setData(event.delegateTarget, dataKey, context) @@ -344,7 +344,6 @@ 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) } else { @@ -383,7 +382,7 @@ class Tooltip { // 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())) + .forEach((element) => EventHandler.off(element, 'mouseover', Util.noop)) } this._activeTrigger[Trigger.CLICK] = false @@ -392,7 +391,6 @@ class Tooltip { if (this.tip.classList.contains(ClassName.FADE)) { const transitionDuration = Util.getTransitionDurationFromElement(tip) - EventHandler.one(tip, Util.TRANSITION_END, complete) Util.emulateTransitionEnd(tip, transitionDuration) } else { @@ -754,11 +752,9 @@ class Tooltip { _fixTransition() { const tip = this.getTipElement() const initConfigAnimation = this.config.animation - if (tip.getAttribute('x-placement') !== null) { return } - tip.classList.remove(ClassName.FADE) this.config.animation = false this.hide() diff --git a/js/src/util.js b/js/src/util.js index caa2e63485..7183aeb6ba 100644 --- a/js/src/util.js +++ b/js/src/util.js @@ -11,7 +11,6 @@ * ------------------------------------------------------------------------ */ -const TRANSITION_END = 'transitionend' const MAX_UID = 1000000 const MILLISECONDS_MULTIPLIER = 1000 @@ -20,9 +19,14 @@ function toType(obj) { return {}.toString.call(obj).match(/\s([a-z]+)/i)[1].toLowerCase() } -const Util = { +/** + * -------------------------------------------------------------------------- + * Public Util Api + * -------------------------------------------------------------------------- + */ - TRANSITION_END: 'bsTransitionEnd', +const Util = { + TRANSITION_END: 'transitionend', getUID(prefix) { do { @@ -79,19 +83,25 @@ const Util = { element.dispatchEvent(new Event(Util.TRANSITION_END)) }, - // TODO: Remove in v5 - supportsTransitionEnd() { - return Boolean(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(() => { - Util.triggerTransitionEnd(element) - }, duration) + if (!called) { + Util.triggerTransitionEnd(element) + } + }, emulatedDuration) }, typeCheckConfig(componentName, config, configTypes) { @@ -117,13 +127,7 @@ const Util = { return [] } - const strRepresentation = Object.prototype.toString.call(nodeList) - if (strRepresentation === '[object NodeList]' || - strRepresentation === '[object HTMLCollection]' || strRepresentation === '[object Array]') { - return Array.prototype.slice.call(nodeList) - } - - return [nodeList] + return [].slice.call(nodeList) }, isVisible(element) { diff --git a/js/tests/unit/util.js b/js/tests/unit/util.js index fa930dcac2..db1412a3ba 100644 --- a/js/tests/unit/util.js +++ b/js/tests/unit/util.js @@ -119,11 +119,6 @@ $(function () { assert.ok(id !== id2, id + ' !== ' + id2) }) - QUnit.test('Util.supportsTransitionEnd should return true', function (assert) { - assert.expect(1) - assert.ok(Util.supportsTransitionEnd()) - }) - QUnit.test('Util.findShadowRoot should find the shadow DOM root', function (assert) { // Only for newer browsers if (!document.documentElement.attachShadow) { @@ -178,4 +173,21 @@ $(function () { window.$ = jQuery }) + + QUnit.test('Util.emulateTransitionEnd should emulate transition end', function (assert) { + assert.expect(1) + var $div = $('
').appendTo($('#qunit-fixture')) + + var spy = sinon.spy($div[0], 'removeEventListener') + + Util.emulateTransitionEnd($div[0], 7) + + assert.ok(spy.notCalled) + }) + + QUnit.test('Util.makeArray should return empty array on null', function (assert) { + assert.expect(1) + + assert.ok(Util.makeArray(null).length === 0) + }) }) diff --git a/site/docs/4.3/assets/js/src/application.js b/site/docs/4.3/assets/js/src/application.js index d039273d5f..3293fddab0 100644 --- a/site/docs/4.3/assets/js/src/application.js +++ b/site/docs/4.3/assets/js/src/application.js @@ -17,13 +17,14 @@ document.addEventListener('DOMContentLoaded', function () { // Tooltip and popover demos - var tooltipDemoList = document.querySelectorAll('.tooltip-demo') + var tooltipDemoList = [].slice.call(document.querySelectorAll('.tooltip-demo')) tooltipDemoList.forEach(function (tooltip) { new bootstrap.Tooltip(tooltip, { selector: '[data-toggle="tooltip"]' }) }) - var popoverList = document.querySelectorAll('[data-toggle="popover"]') + + var popoverList = [].slice.call(document.querySelectorAll('[data-toggle="popover"]')) popoverList.forEach(function (popover) { new bootstrap.Popover(popover) }) @@ -35,24 +36,24 @@ .toast('show') // Demos within modals - var tooltipTestList = document.querySelectorAll('.tooltip-test') + var tooltipTestList = [].slice.call(document.querySelectorAll('.tooltip-test')) tooltipTestList.forEach(function (tooltip) { new bootstrap.Tooltip(tooltip) }) - var popoverTestList = document.querySelectorAll('.popover-test') + var popoverTestList = [].slice.call(document.querySelectorAll('.popover-test')) popoverTestList.forEach(function (popover) { new bootstrap.Popover(popover) }) // Indeterminate checkbox example - var indeterminateCheckboxList = document.querySelectorAll('.bd-example-indeterminate [type="checkbox"]') + var indeterminateCheckboxList = [].slice.call(document.querySelectorAll('.bd-example-indeterminate [type="checkbox"]')) indeterminateCheckboxList.forEach(function (checkbox) { checkbox.indeterminate = true }) // Disable empty links in docs examples - var emptyLinkList = document.querySelectorAll('.bd-content [href="#"]') + var emptyLinkList = [].slice.call(document.querySelectorAll('.bd-content [href="#"]')) emptyLinkList.forEach(function (link) { link.addEventListener('click', function (e) { e.preventDefault() @@ -77,7 +78,7 @@ } // Activate animated progress bar - var animatedProgressBarList = document.querySelectorAll('.bd-toggle-animated-progress > .progress-bar-striped') + var animatedProgressBarList = [].slice.call(document.querySelectorAll('.bd-toggle-animated-progress > .progress-bar-striped')) animatedProgressBarList.forEach(function (progressBar) { progressBar.addEventListener('click', function () { if (progressBar.classList.contains('progress-bar-animated')) { @@ -89,9 +90,9 @@ }) // Insert copy to clipboard button before .highlight - var hightList = [].slice.call(document.querySelectorAll('figure.highlight, div.highlight')) var btnHtml = '
' - hightList.forEach(function (element) { + var highList = [].slice.call(document.querySelectorAll('figure.highlight, div.highlight')) + highList.forEach(function (element) { element.insertAdjacentHTML('beforebegin', btnHtml) })