diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2023-01-24 12:33:51 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2023-01-24 12:33:51 +0000 |
commit | 3ea39841c8049525e31e9f4d6300f0c60cdb42de (patch) | |
tree | 855de60a8872eafb5911acd303aedcdbfe713a73 /js/src/dom | |
parent | Inital commit. (diff) | |
download | bootstrap-html-4d783e1c546bf6c970705cdad09630440fdea698.tar.xz bootstrap-html-4d783e1c546bf6c970705cdad09630440fdea698.zip |
Adding upstream version 5.2.3+dfsg.upstream/5.2.3+dfsg
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'js/src/dom')
-rw-r--r-- | js/src/dom/data.js | 55 | ||||
-rw-r--r-- | js/src/dom/event-handler.js | 320 | ||||
-rw-r--r-- | js/src/dom/manipulator.js | 71 | ||||
-rw-r--r-- | js/src/dom/selector-engine.js | 83 |
4 files changed, 529 insertions, 0 deletions
diff --git a/js/src/dom/data.js b/js/src/dom/data.js new file mode 100644 index 0000000..2c6a46e --- /dev/null +++ b/js/src/dom/data.js @@ -0,0 +1,55 @@ +/** + * -------------------------------------------------------------------------- + * Bootstrap (v5.2.3): dom/data.js + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + * -------------------------------------------------------------------------- + */ + +/** + * Constants + */ + +const elementMap = new Map() + +export default { + set(element, key, instance) { + if (!elementMap.has(element)) { + elementMap.set(element, new Map()) + } + + const instanceMap = elementMap.get(element) + + // make it clear we only want one instance per element + // can be removed later when multiple key/instances are fine to be used + if (!instanceMap.has(key) && instanceMap.size !== 0) { + // eslint-disable-next-line no-console + console.error(`Bootstrap doesn't allow more than one instance per element. Bound instance: ${Array.from(instanceMap.keys())[0]}.`) + return + } + + instanceMap.set(key, instance) + }, + + get(element, key) { + if (elementMap.has(element)) { + return elementMap.get(element).get(key) || null + } + + return null + }, + + remove(element, key) { + if (!elementMap.has(element)) { + return + } + + const instanceMap = elementMap.get(element) + + instanceMap.delete(key) + + // free up element references if there are no instances left for an element + if (instanceMap.size === 0) { + elementMap.delete(element) + } + } +} diff --git a/js/src/dom/event-handler.js b/js/src/dom/event-handler.js new file mode 100644 index 0000000..9876d77 --- /dev/null +++ b/js/src/dom/event-handler.js @@ -0,0 +1,320 @@ +/** + * -------------------------------------------------------------------------- + * Bootstrap (v5.2.3): dom/event-handler.js + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + * -------------------------------------------------------------------------- + */ + +import { getjQuery } from '../util/index' + +/** + * Constants + */ + +const namespaceRegex = /[^.]*(?=\..*)\.|.*/ +const stripNameRegex = /\..*/ +const stripUidRegex = /::\d+$/ +const eventRegistry = {} // Events storage +let uidEvent = 1 +const customEvents = { + mouseenter: 'mouseover', + mouseleave: 'mouseout' +} + +const nativeEvents = new Set([ + '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 + */ + +function makeEventUid(element, uid) { + return (uid && `${uid}::${uidEvent++}`) || element.uidEvent || uidEvent++ +} + +function getElementEvents(element) { + const uid = makeEventUid(element) + + element.uidEvent = uid + eventRegistry[uid] = eventRegistry[uid] || {} + + return eventRegistry[uid] +} + +function bootstrapHandler(element, fn) { + return function handler(event) { + hydrateObj(event, { delegateTarget: 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 !== this; target = target.parentNode) { + for (const domElement of domElements) { + if (domElement !== target) { + continue + } + + hydrateObj(event, { delegateTarget: target }) + + if (handler.oneOff) { + EventHandler.off(element, event.type, selector, fn) + } + + return fn.apply(target, [event]) + } + } + } +} + +function findHandler(events, callable, delegationSelector = null) { + return Object.values(events) + .find(event => event.callable === callable && event.delegationSelector === delegationSelector) +} + +function normalizeParameters(originalTypeEvent, handler, delegationFunction) { + const isDelegated = typeof handler === 'string' + // todo: tooltip passes `false` instead of selector, so we need to check + const callable = isDelegated ? delegationFunction : (handler || delegationFunction) + let typeEvent = getTypeEvent(originalTypeEvent) + + if (!nativeEvents.has(typeEvent)) { + typeEvent = originalTypeEvent + } + + return [isDelegated, callable, typeEvent] +} + +function addHandler(element, originalTypeEvent, handler, delegationFunction, oneOff) { + if (typeof originalTypeEvent !== 'string' || !element) { + return + } + + let [isDelegated, callable, typeEvent] = normalizeParameters(originalTypeEvent, handler, delegationFunction) + + // in case of mouseenter or mouseleave wrap the handler within a function that checks for its DOM position + // this prevents the handler from being dispatched the same way as mouseover or mouseout does + if (originalTypeEvent in customEvents) { + const wrapFunction = fn => { + return function (event) { + if (!event.relatedTarget || (event.relatedTarget !== event.delegateTarget && !event.delegateTarget.contains(event.relatedTarget))) { + return fn.call(this, event) + } + } + } + + callable = wrapFunction(callable) + } + + const events = getElementEvents(element) + const handlers = events[typeEvent] || (events[typeEvent] = {}) + const previousFunction = findHandler(handlers, callable, isDelegated ? handler : null) + + if (previousFunction) { + previousFunction.oneOff = previousFunction.oneOff && oneOff + + return + } + + const uid = makeEventUid(callable, originalTypeEvent.replace(namespaceRegex, '')) + const fn = isDelegated ? + bootstrapDelegationHandler(element, handler, callable) : + bootstrapHandler(element, callable) + + fn.delegationSelector = isDelegated ? handler : null + fn.callable = callable + fn.oneOff = oneOff + fn.uidEvent = uid + handlers[uid] = fn + + element.addEventListener(typeEvent, fn, isDelegated) +} + +function removeHandler(element, events, typeEvent, handler, delegationSelector) { + const fn = findHandler(events[typeEvent], handler, delegationSelector) + + if (!fn) { + 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 of Object.keys(storeElementEvent)) { + if (handlerKey.includes(namespace)) { + const event = storeElementEvent[handlerKey] + removeHandler(element, events, typeEvent, event.callable, event.delegationSelector) + } + } +} + +function getTypeEvent(event) { + // allow to get the native events from namespaced events ('click.bs.button' --> 'click') + event = event.replace(stripNameRegex, '') + return customEvents[event] || event +} + +const EventHandler = { + on(element, event, handler, delegationFunction) { + addHandler(element, event, handler, delegationFunction, false) + }, + + one(element, event, handler, delegationFunction) { + addHandler(element, event, handler, delegationFunction, true) + }, + + off(element, originalTypeEvent, handler, delegationFunction) { + if (typeof originalTypeEvent !== 'string' || !element) { + return + } + + const [isDelegated, callable, typeEvent] = normalizeParameters(originalTypeEvent, handler, delegationFunction) + const inNamespace = typeEvent !== originalTypeEvent + const events = getElementEvents(element) + const storeElementEvent = events[typeEvent] || {} + const isNamespace = originalTypeEvent.startsWith('.') + + if (typeof callable !== 'undefined') { + // Simplest case: handler is passed, remove that listener ONLY. + if (!Object.keys(storeElementEvent).length) { + return + } + + removeHandler(element, events, typeEvent, callable, isDelegated ? handler : null) + return + } + + if (isNamespace) { + for (const elementEvent of Object.keys(events)) { + removeNamespacedHandlers(element, events, elementEvent, originalTypeEvent.slice(1)) + } + } + + for (const keyHandlers of Object.keys(storeElementEvent)) { + const handlerKey = keyHandlers.replace(stripUidRegex, '') + + if (!inNamespace || originalTypeEvent.includes(handlerKey)) { + const event = storeElementEvent[keyHandlers] + removeHandler(element, events, typeEvent, event.callable, event.delegationSelector) + } + } + }, + + trigger(element, event, args) { + if (typeof event !== 'string' || !element) { + return null + } + + const $ = getjQuery() + const typeEvent = getTypeEvent(event) + const inNamespace = event !== typeEvent + + let jQueryEvent = null + let bubbles = true + let nativeDispatch = true + let defaultPrevented = false + + if (inNamespace && $) { + jQueryEvent = $.Event(event, args) + + $(element).trigger(jQueryEvent) + bubbles = !jQueryEvent.isPropagationStopped() + nativeDispatch = !jQueryEvent.isImmediatePropagationStopped() + defaultPrevented = jQueryEvent.isDefaultPrevented() + } + + let evt = new Event(event, { bubbles, cancelable: true }) + evt = hydrateObj(evt, args) + + if (defaultPrevented) { + evt.preventDefault() + } + + if (nativeDispatch) { + element.dispatchEvent(evt) + } + + if (evt.defaultPrevented && jQueryEvent) { + jQueryEvent.preventDefault() + } + + return evt + } +} + +function hydrateObj(obj, meta) { + for (const [key, value] of Object.entries(meta || {})) { + try { + obj[key] = value + } catch { + Object.defineProperty(obj, key, { + configurable: true, + get() { + return value + } + }) + } + } + + return obj +} + +export default EventHandler diff --git a/js/src/dom/manipulator.js b/js/src/dom/manipulator.js new file mode 100644 index 0000000..38ecfe4 --- /dev/null +++ b/js/src/dom/manipulator.js @@ -0,0 +1,71 @@ +/** + * -------------------------------------------------------------------------- + * Bootstrap (v5.2.3): dom/manipulator.js + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + * -------------------------------------------------------------------------- + */ + +function normalizeData(value) { + if (value === 'true') { + return true + } + + if (value === 'false') { + return false + } + + if (value === Number(value).toString()) { + return Number(value) + } + + if (value === '' || value === 'null') { + return null + } + + if (typeof value !== 'string') { + return value + } + + try { + return JSON.parse(decodeURIComponent(value)) + } catch { + return value + } +} + +function normalizeDataKey(key) { + return key.replace(/[A-Z]/g, chr => `-${chr.toLowerCase()}`) +} + +const Manipulator = { + setDataAttribute(element, key, value) { + element.setAttribute(`data-bs-${normalizeDataKey(key)}`, value) + }, + + removeDataAttribute(element, key) { + element.removeAttribute(`data-bs-${normalizeDataKey(key)}`) + }, + + getDataAttributes(element) { + if (!element) { + return {} + } + + const attributes = {} + const bsKeys = Object.keys(element.dataset).filter(key => key.startsWith('bs') && !key.startsWith('bsConfig')) + + for (const key of bsKeys) { + let pureKey = key.replace(/^bs/, '') + pureKey = pureKey.charAt(0).toLowerCase() + pureKey.slice(1, pureKey.length) + attributes[pureKey] = normalizeData(element.dataset[key]) + } + + return attributes + }, + + getDataAttribute(element, key) { + return normalizeData(element.getAttribute(`data-bs-${normalizeDataKey(key)}`)) + } +} + +export default Manipulator diff --git a/js/src/dom/selector-engine.js b/js/src/dom/selector-engine.js new file mode 100644 index 0000000..1ba104f --- /dev/null +++ b/js/src/dom/selector-engine.js @@ -0,0 +1,83 @@ +/** + * -------------------------------------------------------------------------- + * Bootstrap (v5.2.3): dom/selector-engine.js + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + * -------------------------------------------------------------------------- + */ + +import { isDisabled, isVisible } from '../util/index' + +/** + * Constants + */ + +const SelectorEngine = { + find(selector, element = document.documentElement) { + return [].concat(...Element.prototype.querySelectorAll.call(element, selector)) + }, + + findOne(selector, element = document.documentElement) { + return Element.prototype.querySelector.call(element, selector) + }, + + children(element, selector) { + return [].concat(...element.children).filter(child => child.matches(selector)) + }, + + parents(element, selector) { + const parents = [] + let ancestor = element.parentNode.closest(selector) + + while (ancestor) { + parents.push(ancestor) + ancestor = ancestor.parentNode.closest(selector) + } + + return parents + }, + + prev(element, selector) { + let previous = element.previousElementSibling + + while (previous) { + if (previous.matches(selector)) { + return [previous] + } + + previous = previous.previousElementSibling + } + + return [] + }, + // TODO: this is now unused; remove later along with prev() + next(element, selector) { + let next = element.nextElementSibling + + while (next) { + if (next.matches(selector)) { + return [next] + } + + next = next.nextElementSibling + } + + return [] + }, + + focusableChildren(element) { + const focusables = [ + 'a', + 'button', + 'input', + 'textarea', + 'select', + 'details', + '[tabindex]', + '[contenteditable="true"]' + ].map(selector => `${selector}:not([tabindex^="-"])`).join(',') + + return this.find(focusables, element).filter(el => !isDisabled(el) && isVisible(el)) + } +} + +export default SelectorEngine |