From 3ea39841c8049525e31e9f4d6300f0c60cdb42de Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Tue, 24 Jan 2023 13:33:51 +0100 Subject: Adding upstream version 5.2.3+dfsg. Signed-off-by: Daniel Baumann --- js/src/util/backdrop.js | 149 ++++++++++++++++ js/src/util/component-functions.js | 34 ++++ js/src/util/config.js | 66 ++++++++ js/src/util/focustrap.js | 115 +++++++++++++ js/src/util/index.js | 336 +++++++++++++++++++++++++++++++++++++ js/src/util/sanitizer.js | 118 +++++++++++++ js/src/util/scrollbar.js | 114 +++++++++++++ js/src/util/swipe.js | 146 ++++++++++++++++ js/src/util/template-factory.js | 160 ++++++++++++++++++ 9 files changed, 1238 insertions(+) create mode 100644 js/src/util/backdrop.js create mode 100644 js/src/util/component-functions.js create mode 100644 js/src/util/config.js create mode 100644 js/src/util/focustrap.js create mode 100644 js/src/util/index.js create mode 100644 js/src/util/sanitizer.js create mode 100644 js/src/util/scrollbar.js create mode 100644 js/src/util/swipe.js create mode 100644 js/src/util/template-factory.js (limited to 'js/src/util') diff --git a/js/src/util/backdrop.js b/js/src/util/backdrop.js new file mode 100644 index 0000000..78279e0 --- /dev/null +++ b/js/src/util/backdrop.js @@ -0,0 +1,149 @@ +/** + * -------------------------------------------------------------------------- + * Bootstrap (v5.2.3): util/backdrop.js + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + * -------------------------------------------------------------------------- + */ + +import EventHandler from '../dom/event-handler' +import { execute, executeAfterTransition, getElement, reflow } from './index' +import Config from './config' + +/** + * Constants + */ + +const NAME = 'backdrop' +const CLASS_NAME_FADE = 'fade' +const CLASS_NAME_SHOW = 'show' +const EVENT_MOUSEDOWN = `mousedown.bs.${NAME}` + +const Default = { + className: 'modal-backdrop', + clickCallback: null, + isAnimated: false, + isVisible: true, // if false, we use the backdrop helper without adding any element to the dom + rootElement: 'body' // give the choice to place backdrop under different elements +} + +const DefaultType = { + className: 'string', + clickCallback: '(function|null)', + isAnimated: 'boolean', + isVisible: 'boolean', + rootElement: '(element|string)' +} + +/** + * Class definition + */ + +class Backdrop extends Config { + constructor(config) { + super() + this._config = this._getConfig(config) + this._isAppended = false + this._element = null + } + + // Getters + static get Default() { + return Default + } + + static get DefaultType() { + return DefaultType + } + + static get NAME() { + return NAME + } + + // Public + show(callback) { + if (!this._config.isVisible) { + execute(callback) + return + } + + this._append() + + const element = this._getElement() + if (this._config.isAnimated) { + reflow(element) + } + + element.classList.add(CLASS_NAME_SHOW) + + this._emulateAnimation(() => { + execute(callback) + }) + } + + hide(callback) { + if (!this._config.isVisible) { + execute(callback) + return + } + + this._getElement().classList.remove(CLASS_NAME_SHOW) + + this._emulateAnimation(() => { + this.dispose() + execute(callback) + }) + } + + dispose() { + if (!this._isAppended) { + return + } + + EventHandler.off(this._element, EVENT_MOUSEDOWN) + + this._element.remove() + this._isAppended = false + } + + // Private + _getElement() { + if (!this._element) { + const backdrop = document.createElement('div') + backdrop.className = this._config.className + if (this._config.isAnimated) { + backdrop.classList.add(CLASS_NAME_FADE) + } + + this._element = backdrop + } + + return this._element + } + + _configAfterMerge(config) { + // use getElement() with the default "body" to get a fresh Element on each instantiation + config.rootElement = getElement(config.rootElement) + return config + } + + _append() { + if (this._isAppended) { + return + } + + const element = this._getElement() + this._config.rootElement.append(element) + + EventHandler.on(element, EVENT_MOUSEDOWN, () => { + execute(this._config.clickCallback) + }) + + this._isAppended = true + } + + _emulateAnimation(callback) { + executeAfterTransition(callback, this._getElement(), this._config.isAnimated) + } +} + +export default Backdrop diff --git a/js/src/util/component-functions.js b/js/src/util/component-functions.js new file mode 100644 index 0000000..c2f99cc --- /dev/null +++ b/js/src/util/component-functions.js @@ -0,0 +1,34 @@ +/** + * -------------------------------------------------------------------------- + * Bootstrap (v5.2.3): util/component-functions.js + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + * -------------------------------------------------------------------------- + */ + +import EventHandler from '../dom/event-handler' +import { getElementFromSelector, isDisabled } from './index' + +const enableDismissTrigger = (component, method = 'hide') => { + const clickEvent = `click.dismiss${component.EVENT_KEY}` + const name = component.NAME + + EventHandler.on(document, clickEvent, `[data-bs-dismiss="${name}"]`, function (event) { + if (['A', 'AREA'].includes(this.tagName)) { + event.preventDefault() + } + + if (isDisabled(this)) { + return + } + + const target = getElementFromSelector(this) || this.closest(`.${name}`) + const instance = component.getOrCreateInstance(target) + + // Method argument is left, for Alert and only, as it doesn't implement the 'hide' method + instance[method]() + }) +} + +export { + enableDismissTrigger +} diff --git a/js/src/util/config.js b/js/src/util/config.js new file mode 100644 index 0000000..1205905 --- /dev/null +++ b/js/src/util/config.js @@ -0,0 +1,66 @@ +/** + * -------------------------------------------------------------------------- + * Bootstrap (v5.2.3): util/config.js + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + * -------------------------------------------------------------------------- + */ + +import { isElement, toType } from './index' +import Manipulator from '../dom/manipulator' + +/** + * Class definition + */ + +class Config { + // Getters + static get Default() { + return {} + } + + static get DefaultType() { + return {} + } + + static get NAME() { + throw new Error('You have to implement the static method "NAME", for each component!') + } + + _getConfig(config) { + config = this._mergeConfigObj(config) + config = this._configAfterMerge(config) + this._typeCheckConfig(config) + return config + } + + _configAfterMerge(config) { + return config + } + + _mergeConfigObj(config, element) { + const jsonConfig = isElement(element) ? Manipulator.getDataAttribute(element, 'config') : {} // try to parse + + return { + ...this.constructor.Default, + ...(typeof jsonConfig === 'object' ? jsonConfig : {}), + ...(isElement(element) ? Manipulator.getDataAttributes(element) : {}), + ...(typeof config === 'object' ? config : {}) + } + } + + _typeCheckConfig(config, configTypes = this.constructor.DefaultType) { + for (const property of Object.keys(configTypes)) { + const expectedTypes = configTypes[property] + const value = config[property] + const valueType = isElement(value) ? 'element' : toType(value) + + if (!new RegExp(expectedTypes).test(valueType)) { + throw new TypeError( + `${this.constructor.NAME.toUpperCase()}: Option "${property}" provided type "${valueType}" but expected type "${expectedTypes}".` + ) + } + } + } +} + +export default Config diff --git a/js/src/util/focustrap.js b/js/src/util/focustrap.js new file mode 100644 index 0000000..ef69166 --- /dev/null +++ b/js/src/util/focustrap.js @@ -0,0 +1,115 @@ +/** + * -------------------------------------------------------------------------- + * Bootstrap (v5.2.3): util/focustrap.js + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + * -------------------------------------------------------------------------- + */ + +import EventHandler from '../dom/event-handler' +import SelectorEngine from '../dom/selector-engine' +import Config from './config' + +/** + * Constants + */ + +const NAME = 'focustrap' +const DATA_KEY = 'bs.focustrap' +const EVENT_KEY = `.${DATA_KEY}` +const EVENT_FOCUSIN = `focusin${EVENT_KEY}` +const EVENT_KEYDOWN_TAB = `keydown.tab${EVENT_KEY}` + +const TAB_KEY = 'Tab' +const TAB_NAV_FORWARD = 'forward' +const TAB_NAV_BACKWARD = 'backward' + +const Default = { + autofocus: true, + trapElement: null // The element to trap focus inside of +} + +const DefaultType = { + autofocus: 'boolean', + trapElement: 'element' +} + +/** + * Class definition + */ + +class FocusTrap extends Config { + constructor(config) { + super() + this._config = this._getConfig(config) + this._isActive = false + this._lastTabNavDirection = null + } + + // Getters + static get Default() { + return Default + } + + static get DefaultType() { + return DefaultType + } + + static get NAME() { + return NAME + } + + // Public + activate() { + if (this._isActive) { + return + } + + if (this._config.autofocus) { + this._config.trapElement.focus() + } + + EventHandler.off(document, EVENT_KEY) // guard against infinite focus loop + EventHandler.on(document, EVENT_FOCUSIN, event => this._handleFocusin(event)) + EventHandler.on(document, EVENT_KEYDOWN_TAB, event => this._handleKeydown(event)) + + this._isActive = true + } + + deactivate() { + if (!this._isActive) { + return + } + + this._isActive = false + EventHandler.off(document, EVENT_KEY) + } + + // Private + _handleFocusin(event) { + const { trapElement } = this._config + + if (event.target === document || event.target === trapElement || trapElement.contains(event.target)) { + return + } + + const elements = SelectorEngine.focusableChildren(trapElement) + + if (elements.length === 0) { + trapElement.focus() + } else if (this._lastTabNavDirection === TAB_NAV_BACKWARD) { + elements[elements.length - 1].focus() + } else { + elements[0].focus() + } + } + + _handleKeydown(event) { + if (event.key !== TAB_KEY) { + return + } + + this._lastTabNavDirection = event.shiftKey ? TAB_NAV_BACKWARD : TAB_NAV_FORWARD + } +} + +export default FocusTrap diff --git a/js/src/util/index.js b/js/src/util/index.js new file mode 100644 index 0000000..297e571 --- /dev/null +++ b/js/src/util/index.js @@ -0,0 +1,336 @@ +/** + * -------------------------------------------------------------------------- + * Bootstrap (v5.2.3): util/index.js + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + * -------------------------------------------------------------------------- + */ + +const MAX_UID = 1_000_000 +const MILLISECONDS_MULTIPLIER = 1000 +const TRANSITION_END = 'transitionend' + +// Shout-out Angus Croll (https://goo.gl/pxwQGp) +const toType = object => { + if (object === null || object === undefined) { + return `${object}` + } + + return Object.prototype.toString.call(object).match(/\s([a-z]+)/i)[1].toLowerCase() +} + +/** + * Public Util API + */ + +const getUID = prefix => { + do { + prefix += Math.floor(Math.random() * MAX_UID) + } while (document.getElementById(prefix)) + + return prefix +} + +const getSelector = element => { + let selector = element.getAttribute('data-bs-target') + + if (!selector || selector === '#') { + let hrefAttribute = element.getAttribute('href') + + // The only valid content that could double as a selector are IDs or classes, + // so everything starting with `#` or `.`. If a "real" URL is used as the selector, + // `document.querySelector` will rightfully complain it is invalid. + // See https://github.com/twbs/bootstrap/issues/32273 + if (!hrefAttribute || (!hrefAttribute.includes('#') && !hrefAttribute.startsWith('.'))) { + return null + } + + // Just in case some CMS puts out a full URL with the anchor appended + if (hrefAttribute.includes('#') && !hrefAttribute.startsWith('#')) { + hrefAttribute = `#${hrefAttribute.split('#')[1]}` + } + + selector = hrefAttribute && hrefAttribute !== '#' ? hrefAttribute.trim() : null + } + + return selector +} + +const getSelectorFromElement = element => { + const selector = getSelector(element) + + if (selector) { + return document.querySelector(selector) ? selector : null + } + + return null +} + +const getElementFromSelector = element => { + const selector = getSelector(element) + + return selector ? document.querySelector(selector) : null +} + +const getTransitionDurationFromElement = element => { + if (!element) { + return 0 + } + + // Get transition-duration of the element + let { transitionDuration, transitionDelay } = window.getComputedStyle(element) + + const floatTransitionDuration = Number.parseFloat(transitionDuration) + const floatTransitionDelay = Number.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 (Number.parseFloat(transitionDuration) + Number.parseFloat(transitionDelay)) * MILLISECONDS_MULTIPLIER +} + +const triggerTransitionEnd = element => { + element.dispatchEvent(new Event(TRANSITION_END)) +} + +const isElement = object => { + if (!object || typeof object !== 'object') { + return false + } + + if (typeof object.jquery !== 'undefined') { + object = object[0] + } + + return typeof object.nodeType !== 'undefined' +} + +const getElement = object => { + // it's a jQuery object or a node element + if (isElement(object)) { + return object.jquery ? object[0] : object + } + + if (typeof object === 'string' && object.length > 0) { + return document.querySelector(object) + } + + return null +} + +const isVisible = element => { + if (!isElement(element) || element.getClientRects().length === 0) { + return false + } + + const elementIsVisible = getComputedStyle(element).getPropertyValue('visibility') === 'visible' + // Handle `details` element as its content may falsie appear visible when it is closed + const closedDetails = element.closest('details:not([open])') + + if (!closedDetails) { + return elementIsVisible + } + + if (closedDetails !== element) { + const summary = element.closest('summary') + if (summary && summary.parentNode !== closedDetails) { + return false + } + + if (summary === null) { + return false + } + } + + return elementIsVisible +} + +const isDisabled = element => { + if (!element || element.nodeType !== Node.ELEMENT_NODE) { + return true + } + + if (element.classList.contains('disabled')) { + return true + } + + if (typeof element.disabled !== 'undefined') { + return element.disabled + } + + return element.hasAttribute('disabled') && element.getAttribute('disabled') !== '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) +} + +const noop = () => {} + +/** + * Trick to restart an element's animation + * + * @param {HTMLElement} element + * @return void + * + * @see https://www.charistheo.io/blog/2021/02/restart-a-css-animation-with-javascript/#restarting-a-css-animation + */ +const reflow = element => { + element.offsetHeight // eslint-disable-line no-unused-expressions +} + +const getjQuery = () => { + if (window.jQuery && !document.body.hasAttribute('data-bs-no-jquery')) { + return window.jQuery + } + + return null +} + +const DOMContentLoadedCallbacks = [] + +const onDOMContentLoaded = callback => { + if (document.readyState === 'loading') { + // add listener on the first call when the document is in loading state + if (!DOMContentLoadedCallbacks.length) { + document.addEventListener('DOMContentLoaded', () => { + for (const callback of DOMContentLoadedCallbacks) { + callback() + } + }) + } + + DOMContentLoadedCallbacks.push(callback) + } else { + callback() + } +} + +const isRTL = () => document.documentElement.dir === 'rtl' + +const defineJQueryPlugin = plugin => { + onDOMContentLoaded(() => { + const $ = getjQuery() + /* istanbul ignore if */ + if ($) { + const name = plugin.NAME + const JQUERY_NO_CONFLICT = $.fn[name] + $.fn[name] = plugin.jQueryInterface + $.fn[name].Constructor = plugin + $.fn[name].noConflict = () => { + $.fn[name] = JQUERY_NO_CONFLICT + return plugin.jQueryInterface + } + } + }) +} + +const execute = callback => { + if (typeof callback === 'function') { + callback() + } +} + +const executeAfterTransition = (callback, transitionElement, waitForTransition = true) => { + if (!waitForTransition) { + execute(callback) + return + } + + const durationPadding = 5 + const emulatedDuration = getTransitionDurationFromElement(transitionElement) + durationPadding + + let called = false + + const handler = ({ target }) => { + if (target !== transitionElement) { + return + } + + called = true + transitionElement.removeEventListener(TRANSITION_END, handler) + execute(callback) + } + + transitionElement.addEventListener(TRANSITION_END, handler) + setTimeout(() => { + if (!called) { + triggerTransitionEnd(transitionElement) + } + }, emulatedDuration) +} + +/** + * Return the previous/next element of a list. + * + * @param {array} list The list of elements + * @param activeElement The active element + * @param shouldGetNext Choose to get next or previous element + * @param isCycleAllowed + * @return {Element|elem} The proper element + */ +const getNextActiveElement = (list, activeElement, shouldGetNext, isCycleAllowed) => { + const listLength = list.length + let index = list.indexOf(activeElement) + + // if the element does not exist in the list return an element + // depending on the direction and if cycle is allowed + if (index === -1) { + return !shouldGetNext && isCycleAllowed ? list[listLength - 1] : list[0] + } + + index += shouldGetNext ? 1 : -1 + + if (isCycleAllowed) { + index = (index + listLength) % listLength + } + + return list[Math.max(0, Math.min(index, listLength - 1))] +} + +export { + defineJQueryPlugin, + execute, + executeAfterTransition, + findShadowRoot, + getElement, + getElementFromSelector, + getjQuery, + getNextActiveElement, + getSelectorFromElement, + getTransitionDurationFromElement, + getUID, + isDisabled, + isElement, + isRTL, + isVisible, + noop, + onDOMContentLoaded, + reflow, + triggerTransitionEnd, + toType +} diff --git a/js/src/util/sanitizer.js b/js/src/util/sanitizer.js new file mode 100644 index 0000000..5328691 --- /dev/null +++ b/js/src/util/sanitizer.js @@ -0,0 +1,118 @@ +/** + * -------------------------------------------------------------------------- + * Bootstrap (v5.2.3): util/sanitizer.js + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + * -------------------------------------------------------------------------- + */ + +const uriAttributes = new Set([ + 'background', + 'cite', + 'href', + 'itemtype', + 'longdesc', + 'poster', + 'src', + 'xlink:href' +]) + +const ARIA_ATTRIBUTE_PATTERN = /^aria-[\w-]*$/i + +/** + * A pattern that recognizes a commonly useful subset of URLs that are safe. + * + * Shout-out to Angular https://github.com/angular/angular/blob/12.2.x/packages/core/src/sanitization/url_sanitizer.ts + */ +const SAFE_URL_PATTERN = /^(?:(?:https?|mailto|ftp|tel|file|sms):|[^#&/:?]*(?:[#/?]|$))/i + +/** + * A pattern that matches safe data URLs. Only matches image, video and audio types. + * + * Shout-out to Angular https://github.com/angular/angular/blob/12.2.x/packages/core/src/sanitization/url_sanitizer.ts + */ +const DATA_URL_PATTERN = /^data:(?:image\/(?:bmp|gif|jpeg|jpg|png|tiff|webp)|video\/(?:mpeg|mp4|ogg|webm)|audio\/(?:mp3|oga|ogg|opus));base64,[\d+/a-z]+=*$/i + +const allowedAttribute = (attribute, allowedAttributeList) => { + const attributeName = attribute.nodeName.toLowerCase() + + if (allowedAttributeList.includes(attributeName)) { + if (uriAttributes.has(attributeName)) { + return Boolean(SAFE_URL_PATTERN.test(attribute.nodeValue) || DATA_URL_PATTERN.test(attribute.nodeValue)) + } + + return true + } + + // Check if a regular expression validates the attribute. + return allowedAttributeList.filter(attributeRegex => attributeRegex instanceof RegExp) + .some(regex => regex.test(attributeName)) +} + +export const DefaultAllowlist = { + // 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', 'srcset', 'alt', 'title', 'width', 'height'], + li: [], + ol: [], + p: [], + pre: [], + s: [], + small: [], + span: [], + sub: [], + sup: [], + strong: [], + u: [], + ul: [] +} + +export function sanitizeHtml(unsafeHtml, allowList, sanitizeFunction) { + if (!unsafeHtml.length) { + return unsafeHtml + } + + if (sanitizeFunction && typeof sanitizeFunction === 'function') { + return sanitizeFunction(unsafeHtml) + } + + const domParser = new window.DOMParser() + const createdDocument = domParser.parseFromString(unsafeHtml, 'text/html') + const elements = [].concat(...createdDocument.body.querySelectorAll('*')) + + for (const element of elements) { + const elementName = element.nodeName.toLowerCase() + + if (!Object.keys(allowList).includes(elementName)) { + element.remove() + + continue + } + + const attributeList = [].concat(...element.attributes) + const allowedAttributes = [].concat(allowList['*'] || [], allowList[elementName] || []) + + for (const attribute of attributeList) { + if (!allowedAttribute(attribute, allowedAttributes)) { + element.removeAttribute(attribute.nodeName) + } + } + } + + return createdDocument.body.innerHTML +} diff --git a/js/src/util/scrollbar.js b/js/src/util/scrollbar.js new file mode 100644 index 0000000..5cac7b6 --- /dev/null +++ b/js/src/util/scrollbar.js @@ -0,0 +1,114 @@ +/** + * -------------------------------------------------------------------------- + * Bootstrap (v5.2.3): util/scrollBar.js + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + * -------------------------------------------------------------------------- + */ + +import SelectorEngine from '../dom/selector-engine' +import Manipulator from '../dom/manipulator' +import { isElement } from './index' + +/** + * Constants + */ + +const SELECTOR_FIXED_CONTENT = '.fixed-top, .fixed-bottom, .is-fixed, .sticky-top' +const SELECTOR_STICKY_CONTENT = '.sticky-top' +const PROPERTY_PADDING = 'padding-right' +const PROPERTY_MARGIN = 'margin-right' + +/** + * Class definition + */ + +class ScrollBarHelper { + constructor() { + this._element = document.body + } + + // Public + getWidth() { + // https://developer.mozilla.org/en-US/docs/Web/API/Window/innerWidth#usage_notes + const documentWidth = document.documentElement.clientWidth + return Math.abs(window.innerWidth - documentWidth) + } + + hide() { + const width = this.getWidth() + this._disableOverFlow() + // give padding to element to balance the hidden scrollbar width + this._setElementAttributes(this._element, PROPERTY_PADDING, calculatedValue => calculatedValue + width) + // trick: We adjust positive paddingRight and negative marginRight to sticky-top elements to keep showing fullwidth + this._setElementAttributes(SELECTOR_FIXED_CONTENT, PROPERTY_PADDING, calculatedValue => calculatedValue + width) + this._setElementAttributes(SELECTOR_STICKY_CONTENT, PROPERTY_MARGIN, calculatedValue => calculatedValue - width) + } + + reset() { + this._resetElementAttributes(this._element, 'overflow') + this._resetElementAttributes(this._element, PROPERTY_PADDING) + this._resetElementAttributes(SELECTOR_FIXED_CONTENT, PROPERTY_PADDING) + this._resetElementAttributes(SELECTOR_STICKY_CONTENT, PROPERTY_MARGIN) + } + + isOverflowing() { + return this.getWidth() > 0 + } + + // Private + _disableOverFlow() { + this._saveInitialAttribute(this._element, 'overflow') + this._element.style.overflow = 'hidden' + } + + _setElementAttributes(selector, styleProperty, callback) { + const scrollbarWidth = this.getWidth() + const manipulationCallBack = element => { + if (element !== this._element && window.innerWidth > element.clientWidth + scrollbarWidth) { + return + } + + this._saveInitialAttribute(element, styleProperty) + const calculatedValue = window.getComputedStyle(element).getPropertyValue(styleProperty) + element.style.setProperty(styleProperty, `${callback(Number.parseFloat(calculatedValue))}px`) + } + + this._applyManipulationCallback(selector, manipulationCallBack) + } + + _saveInitialAttribute(element, styleProperty) { + const actualValue = element.style.getPropertyValue(styleProperty) + if (actualValue) { + Manipulator.setDataAttribute(element, styleProperty, actualValue) + } + } + + _resetElementAttributes(selector, styleProperty) { + const manipulationCallBack = element => { + const value = Manipulator.getDataAttribute(element, styleProperty) + // We only want to remove the property if the value is `null`; the value can also be zero + if (value === null) { + element.style.removeProperty(styleProperty) + return + } + + Manipulator.removeDataAttribute(element, styleProperty) + element.style.setProperty(styleProperty, value) + } + + this._applyManipulationCallback(selector, manipulationCallBack) + } + + _applyManipulationCallback(selector, callBack) { + if (isElement(selector)) { + callBack(selector) + return + } + + for (const sel of SelectorEngine.find(selector, this._element)) { + callBack(sel) + } + } +} + +export default ScrollBarHelper diff --git a/js/src/util/swipe.js b/js/src/util/swipe.js new file mode 100644 index 0000000..7126360 --- /dev/null +++ b/js/src/util/swipe.js @@ -0,0 +1,146 @@ +/** + * -------------------------------------------------------------------------- + * Bootstrap (v5.2.3): util/swipe.js + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + * -------------------------------------------------------------------------- + */ + +import Config from './config' +import EventHandler from '../dom/event-handler' +import { execute } from './index' + +/** + * Constants + */ + +const NAME = 'swipe' +const EVENT_KEY = '.bs.swipe' +const EVENT_TOUCHSTART = `touchstart${EVENT_KEY}` +const EVENT_TOUCHMOVE = `touchmove${EVENT_KEY}` +const EVENT_TOUCHEND = `touchend${EVENT_KEY}` +const EVENT_POINTERDOWN = `pointerdown${EVENT_KEY}` +const EVENT_POINTERUP = `pointerup${EVENT_KEY}` +const POINTER_TYPE_TOUCH = 'touch' +const POINTER_TYPE_PEN = 'pen' +const CLASS_NAME_POINTER_EVENT = 'pointer-event' +const SWIPE_THRESHOLD = 40 + +const Default = { + endCallback: null, + leftCallback: null, + rightCallback: null +} + +const DefaultType = { + endCallback: '(function|null)', + leftCallback: '(function|null)', + rightCallback: '(function|null)' +} + +/** + * Class definition + */ + +class Swipe extends Config { + constructor(element, config) { + super() + this._element = element + + if (!element || !Swipe.isSupported()) { + return + } + + this._config = this._getConfig(config) + this._deltaX = 0 + this._supportPointerEvents = Boolean(window.PointerEvent) + this._initEvents() + } + + // Getters + static get Default() { + return Default + } + + static get DefaultType() { + return DefaultType + } + + static get NAME() { + return NAME + } + + // Public + dispose() { + EventHandler.off(this._element, EVENT_KEY) + } + + // Private + _start(event) { + if (!this._supportPointerEvents) { + this._deltaX = event.touches[0].clientX + + return + } + + if (this._eventIsPointerPenTouch(event)) { + this._deltaX = event.clientX + } + } + + _end(event) { + if (this._eventIsPointerPenTouch(event)) { + this._deltaX = event.clientX - this._deltaX + } + + this._handleSwipe() + execute(this._config.endCallback) + } + + _move(event) { + this._deltaX = event.touches && event.touches.length > 1 ? + 0 : + event.touches[0].clientX - this._deltaX + } + + _handleSwipe() { + const absDeltaX = Math.abs(this._deltaX) + + if (absDeltaX <= SWIPE_THRESHOLD) { + return + } + + const direction = absDeltaX / this._deltaX + + this._deltaX = 0 + + if (!direction) { + return + } + + execute(direction > 0 ? this._config.rightCallback : this._config.leftCallback) + } + + _initEvents() { + if (this._supportPointerEvents) { + EventHandler.on(this._element, EVENT_POINTERDOWN, event => this._start(event)) + EventHandler.on(this._element, EVENT_POINTERUP, event => this._end(event)) + + this._element.classList.add(CLASS_NAME_POINTER_EVENT) + } else { + EventHandler.on(this._element, EVENT_TOUCHSTART, event => this._start(event)) + EventHandler.on(this._element, EVENT_TOUCHMOVE, event => this._move(event)) + EventHandler.on(this._element, EVENT_TOUCHEND, event => this._end(event)) + } + } + + _eventIsPointerPenTouch(event) { + return this._supportPointerEvents && (event.pointerType === POINTER_TYPE_PEN || event.pointerType === POINTER_TYPE_TOUCH) + } + + // Static + static isSupported() { + return 'ontouchstart' in document.documentElement || navigator.maxTouchPoints > 0 + } +} + +export default Swipe diff --git a/js/src/util/template-factory.js b/js/src/util/template-factory.js new file mode 100644 index 0000000..cf402fa --- /dev/null +++ b/js/src/util/template-factory.js @@ -0,0 +1,160 @@ +/** + * -------------------------------------------------------------------------- + * Bootstrap (v5.2.3): util/template-factory.js + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + * -------------------------------------------------------------------------- + */ + +import { DefaultAllowlist, sanitizeHtml } from './sanitizer' +import { getElement, isElement } from '../util/index' +import SelectorEngine from '../dom/selector-engine' +import Config from './config' + +/** + * Constants + */ + +const NAME = 'TemplateFactory' + +const Default = { + allowList: DefaultAllowlist, + content: {}, // { selector : text , selector2 : text2 , } + extraClass: '', + html: false, + sanitize: true, + sanitizeFn: null, + template: '
' +} + +const DefaultType = { + allowList: 'object', + content: 'object', + extraClass: '(string|function)', + html: 'boolean', + sanitize: 'boolean', + sanitizeFn: '(null|function)', + template: 'string' +} + +const DefaultContentType = { + entry: '(string|element|function|null)', + selector: '(string|element)' +} + +/** + * Class definition + */ + +class TemplateFactory extends Config { + constructor(config) { + super() + this._config = this._getConfig(config) + } + + // Getters + static get Default() { + return Default + } + + static get DefaultType() { + return DefaultType + } + + static get NAME() { + return NAME + } + + // Public + getContent() { + return Object.values(this._config.content) + .map(config => this._resolvePossibleFunction(config)) + .filter(Boolean) + } + + hasContent() { + return this.getContent().length > 0 + } + + changeContent(content) { + this._checkContent(content) + this._config.content = { ...this._config.content, ...content } + return this + } + + toHtml() { + const templateWrapper = document.createElement('div') + templateWrapper.innerHTML = this._maybeSanitize(this._config.template) + + for (const [selector, text] of Object.entries(this._config.content)) { + this._setContent(templateWrapper, text, selector) + } + + const template = templateWrapper.children[0] + const extraClass = this._resolvePossibleFunction(this._config.extraClass) + + if (extraClass) { + template.classList.add(...extraClass.split(' ')) + } + + return template + } + + // Private + _typeCheckConfig(config) { + super._typeCheckConfig(config) + this._checkContent(config.content) + } + + _checkContent(arg) { + for (const [selector, content] of Object.entries(arg)) { + super._typeCheckConfig({ selector, entry: content }, DefaultContentType) + } + } + + _setContent(template, content, selector) { + const templateElement = SelectorEngine.findOne(selector, template) + + if (!templateElement) { + return + } + + content = this._resolvePossibleFunction(content) + + if (!content) { + templateElement.remove() + return + } + + if (isElement(content)) { + this._putElementInTemplate(getElement(content), templateElement) + return + } + + if (this._config.html) { + templateElement.innerHTML = this._maybeSanitize(content) + return + } + + templateElement.textContent = content + } + + _maybeSanitize(arg) { + return this._config.sanitize ? sanitizeHtml(arg, this._config.allowList, this._config.sanitizeFn) : arg + } + + _resolvePossibleFunction(arg) { + return typeof arg === 'function' ? arg(this) : arg + } + + _putElementInTemplate(element, templateElement) { + if (this._config.html) { + templateElement.innerHTML = '' + templateElement.append(element) + return + } + + templateElement.textContent = element.textContent + } +} + +export default TemplateFactory -- cgit v1.2.3