From 64e13162faa692aa2d12071ad9a14a3ac1b08a6f Mon Sep 17 00:00:00 2001 From: XhmikosR Date: Thu, 7 Oct 2021 17:48:36 +0300 Subject: Sanitizer: fix logic and add a test. (#35133) This was broken in 2596c97 inadvertently. Added a test so that we don't hit this in the future. --- js/src/util/sanitizer.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'js/src/util') diff --git a/js/src/util/sanitizer.js b/js/src/util/sanitizer.js index f5a8287cd..232416f3a 100644 --- a/js/src/util/sanitizer.js +++ b/js/src/util/sanitizer.js @@ -45,7 +45,7 @@ const allowedAttribute = (attribute, allowedAttributeList) => { // Check if a regular expression validates the attribute. return allowedAttributeList.filter(attributeRegex => attributeRegex instanceof RegExp) - .every(regex => regex.test(attributeName)) + .some(regex => regex.test(attributeName)) } export const DefaultAllowlist = { -- cgit v1.2.3 From 1a6fdfae6be09b09eaced8f0e442ca6f7680a61e Mon Sep 17 00:00:00 2001 From: XhmikosR Date: Sat, 9 Oct 2021 09:33:12 +0300 Subject: Bump version to 5.1.3. --- js/src/util/backdrop.js | 2 +- js/src/util/component-functions.js | 2 +- js/src/util/focustrap.js | 2 +- js/src/util/index.js | 2 +- js/src/util/sanitizer.js | 2 +- js/src/util/scrollbar.js | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) (limited to 'js/src/util') diff --git a/js/src/util/backdrop.js b/js/src/util/backdrop.js index e5ca0c860..04c763518 100644 --- a/js/src/util/backdrop.js +++ b/js/src/util/backdrop.js @@ -1,6 +1,6 @@ /** * -------------------------------------------------------------------------- - * Bootstrap (v5.1.2): util/backdrop.js + * Bootstrap (v5.1.3): util/backdrop.js * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) * -------------------------------------------------------------------------- */ diff --git a/js/src/util/component-functions.js b/js/src/util/component-functions.js index c678ecadf..bd44c3fdc 100644 --- a/js/src/util/component-functions.js +++ b/js/src/util/component-functions.js @@ -1,6 +1,6 @@ /** * -------------------------------------------------------------------------- - * Bootstrap (v5.1.2): util/component-functions.js + * Bootstrap (v5.1.3): util/component-functions.js * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) * -------------------------------------------------------------------------- */ diff --git a/js/src/util/focustrap.js b/js/src/util/focustrap.js index 500a5b3bc..44d5f47eb 100644 --- a/js/src/util/focustrap.js +++ b/js/src/util/focustrap.js @@ -1,6 +1,6 @@ /** * -------------------------------------------------------------------------- - * Bootstrap (v5.1.2): util/focustrap.js + * Bootstrap (v5.1.3): util/focustrap.js * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) * -------------------------------------------------------------------------- */ diff --git a/js/src/util/index.js b/js/src/util/index.js index 7e9e9b046..d05a3cbd7 100644 --- a/js/src/util/index.js +++ b/js/src/util/index.js @@ -1,6 +1,6 @@ /** * -------------------------------------------------------------------------- - * Bootstrap (v5.1.2): util/index.js + * Bootstrap (v5.1.3): util/index.js * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) * -------------------------------------------------------------------------- */ diff --git a/js/src/util/sanitizer.js b/js/src/util/sanitizer.js index 2a0597be7..339c916c6 100644 --- a/js/src/util/sanitizer.js +++ b/js/src/util/sanitizer.js @@ -1,6 +1,6 @@ /** * -------------------------------------------------------------------------- - * Bootstrap (v5.1.2): util/sanitizer.js + * Bootstrap (v5.1.3): util/sanitizer.js * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) * -------------------------------------------------------------------------- */ diff --git a/js/src/util/scrollbar.js b/js/src/util/scrollbar.js index 2d5d0ffa6..a90f21a79 100644 --- a/js/src/util/scrollbar.js +++ b/js/src/util/scrollbar.js @@ -1,6 +1,6 @@ /** * -------------------------------------------------------------------------- - * Bootstrap (v5.1.2): util/scrollBar.js + * Bootstrap (v5.1.3): util/scrollBar.js * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) * -------------------------------------------------------------------------- */ -- cgit v1.2.3 From 8ec6c9452286472ddad12d1af59b173ede22b5ac Mon Sep 17 00:00:00 2001 From: GeoSot Date: Mon, 11 Oct 2021 17:04:43 +0300 Subject: Extract Carousel's swipe functionality to a separate Class (#32999) --- js/src/util/swipe.js | 122 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 122 insertions(+) create mode 100644 js/src/util/swipe.js (limited to 'js/src/util') diff --git a/js/src/util/swipe.js b/js/src/util/swipe.js new file mode 100644 index 000000000..321572eb8 --- /dev/null +++ b/js/src/util/swipe.js @@ -0,0 +1,122 @@ +import EventHandler from '../dom/event-handler' +import { execute, typeCheckConfig } from './index' + +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 NAME = 'swipe' + +const Default = { + leftCallback: null, + rightCallback: null, + endCallback: null +} + +const DefaultType = { + leftCallback: '(function|null)', + rightCallback: '(function|null)', + endCallback: '(function|null)' +} + +class Swipe { + constructor(element, config) { + this._element = element + + if (!element || !Swipe.isSupported()) { + return + } + + this._config = this._getConfig(config) + this._deltaX = 0 + this._supportPointerEvents = Boolean(window.PointerEvent) + this._initEvents() + } + + dispose() { + EventHandler.off(this._element, EVENT_KEY) + } + + _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)) + } + } + + _getConfig(config) { + config = { + ...Default, + ...(typeof config === 'object' ? config : {}) + } + typeCheckConfig(NAME, config, DefaultType) + return config + } + + _eventIsPointerPenTouch(event) { + return this._supportPointerEvents && (event.pointerType === POINTER_TYPE_PEN || event.pointerType === POINTER_TYPE_TOUCH) + } + + static isSupported() { + return 'ontouchstart' in document.documentElement || navigator.maxTouchPoints > 0 + } +} + +export default Swipe -- cgit v1.2.3 From db44392bda22f3d5319d2880c992f76d27d2a60c Mon Sep 17 00:00:00 2001 From: GeoSot Date: Tue, 12 Oct 2021 15:48:19 +0300 Subject: Swipe: add test to ensure that it ignores `pinch` events (#35161) --- js/src/util/swipe.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'js/src/util') diff --git a/js/src/util/swipe.js b/js/src/util/swipe.js index 321572eb8..a78f598d9 100644 --- a/js/src/util/swipe.js +++ b/js/src/util/swipe.js @@ -1,6 +1,7 @@ import EventHandler from '../dom/event-handler' import { execute, typeCheckConfig } from './index' +const NAME = 'swipe' const EVENT_KEY = '.bs.swipe' const EVENT_TOUCHSTART = `touchstart${EVENT_KEY}` const EVENT_TOUCHMOVE = `touchmove${EVENT_KEY}` @@ -11,7 +12,6 @@ const POINTER_TYPE_TOUCH = 'touch' const POINTER_TYPE_PEN = 'pen' const CLASS_NAME_POINTER_EVENT = 'pointer-event' const SWIPE_THRESHOLD = 40 -const NAME = 'swipe' const Default = { leftCallback: null, -- cgit v1.2.3 From e8f702666f285a3e69866ed1f8d29fa6eaaaeabb Mon Sep 17 00:00:00 2001 From: XhmikosR Date: Wed, 13 Oct 2021 15:19:28 +0300 Subject: JS: minor refactoring (#35183) * add missing comments * shorten block comments * reorder constants * reorder public/private methods * sort exports alphabetically in util/index.js * fix a couple of typos --- js/src/util/backdrop.js | 40 ++++++++++++++++++++++++---------------- js/src/util/focustrap.js | 28 ++++++++++++++++++---------- js/src/util/index.js | 36 +++++++++++++++++------------------- js/src/util/scrollbar.js | 32 +++++++++++++++++++++----------- js/src/util/swipe.js | 18 ++++++++++++++++++ 5 files changed, 98 insertions(+), 56 deletions(-) (limited to 'js/src/util') diff --git a/js/src/util/backdrop.js b/js/src/util/backdrop.js index 04c763518..fb1b2776b 100644 --- a/js/src/util/backdrop.js +++ b/js/src/util/backdrop.js @@ -8,6 +8,15 @@ import EventHandler from '../dom/event-handler' import { execute, executeAfterTransition, getElement, reflow, typeCheckConfig } from './index' +/** + * 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', isVisible: true, // if false, we use the backdrop helper without adding any element to the dom @@ -23,11 +32,10 @@ const DefaultType = { rootElement: '(element|string)', clickCallback: '(function|null)' } -const NAME = 'backdrop' -const CLASS_NAME_FADE = 'fade' -const CLASS_NAME_SHOW = 'show' -const EVENT_MOUSEDOWN = `mousedown.bs.${NAME}` +/** + * Class definition + */ class Backdrop { constructor(config) { @@ -36,6 +44,7 @@ class Backdrop { this._element = null } + // Public show(callback) { if (!this._config.isVisible) { execute(callback) @@ -69,8 +78,18 @@ class Backdrop { }) } - // Private + 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') @@ -111,17 +130,6 @@ class Backdrop { this._isAppended = true } - dispose() { - if (!this._isAppended) { - return - } - - EventHandler.off(this._element, EVENT_MOUSEDOWN) - - this._element.remove() - this._isAppended = false - } - _emulateAnimation(callback) { executeAfterTransition(callback, this._getElement(), this._config.isAnimated) } diff --git a/js/src/util/focustrap.js b/js/src/util/focustrap.js index 44d5f47eb..a1975f489 100644 --- a/js/src/util/focustrap.js +++ b/js/src/util/focustrap.js @@ -9,15 +9,9 @@ import EventHandler from '../dom/event-handler' import SelectorEngine from '../dom/selector-engine' import { typeCheckConfig } from './index' -const Default = { - trapElement: null, // The element to trap focus inside of - autofocus: true -} - -const DefaultType = { - trapElement: 'element', - autofocus: 'boolean' -} +/** + * Constants + */ const NAME = 'focustrap' const DATA_KEY = 'bs.focustrap' @@ -29,6 +23,20 @@ const TAB_KEY = 'Tab' const TAB_NAV_FORWARD = 'forward' const TAB_NAV_BACKWARD = 'backward' +const Default = { + trapElement: null, // The element to trap focus inside of + autofocus: true +} + +const DefaultType = { + trapElement: 'element', + autofocus: 'boolean' +} + +/** + * Class definition + */ + class FocusTrap { constructor(config) { this._config = this._getConfig(config) @@ -36,6 +44,7 @@ class FocusTrap { this._lastTabNavDirection = null } + // Public activate() { const { trapElement, autofocus } = this._config @@ -64,7 +73,6 @@ class FocusTrap { } // Private - _handleFocusin(event) { const { target } = event const { trapElement } = this._config diff --git a/js/src/util/index.js b/js/src/util/index.js index 93f7223b1..0ba6ce6f8 100644 --- a/js/src/util/index.js +++ b/js/src/util/index.js @@ -19,9 +19,7 @@ const toType = obj => { } /** - * -------------------------------------------------------------------------- - * Public Util Api - * -------------------------------------------------------------------------- + * Public Util API */ const getUID = prefix => { @@ -113,7 +111,8 @@ const isElement = obj => { } const getElement = obj => { - if (isElement(obj)) { // it's a jQuery object or a node element + // it's a jQuery object or a node element + if (isElement(obj)) { return obj.jquery ? obj[0] : obj } @@ -196,8 +195,7 @@ const noop = () => {} * @see https://www.charistheo.io/blog/2021/02/restart-a-css-animation-with-javascript/#restarting-a-css-animation */ const reflow = element => { - // eslint-disable-next-line no-unused-expressions - element.offsetHeight + element.offsetHeight // eslint-disable-line no-unused-expressions } const getjQuery = () => { @@ -312,24 +310,24 @@ const getNextActiveElement = (list, activeElement, shouldGetNext, isCycleAllowed } export { + defineJQueryPlugin, + execute, + executeAfterTransition, + findShadowRoot, getElement, - getUID, - getSelectorFromElement, getElementFromSelector, + getjQuery, + getNextActiveElement, + getSelectorFromElement, getTransitionDurationFromElement, - triggerTransitionEnd, + getUID, + isDisabled, isElement, - typeCheckConfig, + isRTL, isVisible, - isDisabled, - findShadowRoot, noop, - getNextActiveElement, - reflow, - getjQuery, onDOMContentLoaded, - isRTL, - defineJQueryPlugin, - execute, - executeAfterTransition + reflow, + triggerTransitionEnd, + typeCheckConfig } diff --git a/js/src/util/scrollbar.js b/js/src/util/scrollbar.js index 16e14d1f3..55b7244ab 100644 --- a/js/src/util/scrollbar.js +++ b/js/src/util/scrollbar.js @@ -9,14 +9,23 @@ 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' +/** + * 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 @@ -33,6 +42,18 @@ class ScrollBarHelper { this._setElementAttributes(SELECTOR_STICKY_CONTENT, 'marginRight', calculatedValue => calculatedValue - width) } + reset() { + this._resetElementAttributes(this._element, 'overflow') + this._resetElementAttributes(this._element, 'paddingRight') + this._resetElementAttributes(SELECTOR_FIXED_CONTENT, 'paddingRight') + this._resetElementAttributes(SELECTOR_STICKY_CONTENT, 'marginRight') + } + + isOverflowing() { + return this.getWidth() > 0 + } + + // Private _disableOverFlow() { this._saveInitialAttribute(this._element, 'overflow') this._element.style.overflow = 'hidden' @@ -53,13 +74,6 @@ class ScrollBarHelper { this._applyManipulationCallback(selector, manipulationCallBack) } - reset() { - this._resetElementAttributes(this._element, 'overflow') - this._resetElementAttributes(this._element, 'paddingRight') - this._resetElementAttributes(SELECTOR_FIXED_CONTENT, 'paddingRight') - this._resetElementAttributes(SELECTOR_STICKY_CONTENT, 'marginRight') - } - _saveInitialAttribute(element, styleProp) { const actualValue = element.style[styleProp] if (actualValue) { @@ -90,10 +104,6 @@ class ScrollBarHelper { } } } - - isOverflowing() { - return this.getWidth() > 0 - } } export default ScrollBarHelper diff --git a/js/src/util/swipe.js b/js/src/util/swipe.js index a78f598d9..87a5f7f5a 100644 --- a/js/src/util/swipe.js +++ b/js/src/util/swipe.js @@ -1,6 +1,17 @@ +/** + * -------------------------------------------------------------------------- + * Bootstrap (v5.1.3): util/swipe.js + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + * -------------------------------------------------------------------------- + */ + import EventHandler from '../dom/event-handler' import { execute, typeCheckConfig } from './index' +/** + * Constants + */ + const NAME = 'swipe' const EVENT_KEY = '.bs.swipe' const EVENT_TOUCHSTART = `touchstart${EVENT_KEY}` @@ -25,6 +36,10 @@ const DefaultType = { endCallback: '(function|null)' } +/** + * Class definition + */ + class Swipe { constructor(element, config) { this._element = element @@ -39,10 +54,12 @@ class Swipe { this._initEvents() } + // Public dispose() { EventHandler.off(this._element, EVENT_KEY) } + // Private _start(event) { if (!this._supportPointerEvents) { this._deltaX = event.touches[0].clientX @@ -114,6 +131,7 @@ class Swipe { return this._supportPointerEvents && (event.pointerType === POINTER_TYPE_PEN || event.pointerType === POINTER_TYPE_TOUCH) } + // Static static isSupported() { return 'ontouchstart' in document.documentElement || navigator.maxTouchPoints > 0 } -- cgit v1.2.3 From 94a596fbcb1011ba990da2078ba7e20b39dba2d9 Mon Sep 17 00:00:00 2001 From: GeoSot Date: Thu, 25 Nov 2021 19:14:02 +0200 Subject: Add a template factory helper to handle all template cases (#34519) Co-authored-by: XhmikosR --- js/src/util/template-factory.js | 161 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 161 insertions(+) create mode 100644 js/src/util/template-factory.js (limited to 'js/src/util') diff --git a/js/src/util/template-factory.js b/js/src/util/template-factory.js new file mode 100644 index 000000000..a9cee1086 --- /dev/null +++ b/js/src/util/template-factory.js @@ -0,0 +1,161 @@ +/** + * -------------------------------------------------------------------------- + * Bootstrap (v5.1.3): util/template-factory.js + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + * -------------------------------------------------------------------------- + */ + +import { DefaultAllowlist, sanitizeHtml } from './sanitizer' +import { getElement, isElement, typeCheckConfig } from '../util/index' +import SelectorEngine from '../dom/selector-engine' + +/** + * Constants + */ + +const NAME = 'TemplateFactory' + +const Default = { + extraClass: '', + template: '
', + content: {}, // { selector : text , selector2 : text2 , } + html: false, + sanitize: true, + sanitizeFn: null, + allowList: DefaultAllowlist +} + +const DefaultType = { + extraClass: '(string|function)', + template: 'string', + content: 'object', + html: 'boolean', + sanitize: 'boolean', + sanitizeFn: '(null|function)', + allowList: 'object' +} + +const DefaultContentType = { + selector: '(string|element)', + entry: '(string|element|function|null)' +} + +/** + * Class definition + */ + +class TemplateFactory { + constructor(config) { + this._config = this._getConfig(config) + } + + // Getters + static get NAME() { + return NAME + } + + static get Default() { + return Default + } + + // 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 + _getConfig(config) { + config = { + ...Default, + ...(typeof config === 'object' ? config : {}) + } + + typeCheckConfig(NAME, config, DefaultType) + this._checkContent(config.content) + + return config + } + + _checkContent(arg) { + for (const [selector, content] of Object.entries(arg)) { + typeCheckConfig(NAME, { 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