From 8306213650fb3473416df7445393ab01b6a44331 Mon Sep 17 00:00:00 2001 From: Johann-S Date: Wed, 17 Feb 2021 09:22:44 +0200 Subject: [PATCH] Be SSR friendly when accessing DOM objects --- .eslintrc.json | 4 ++ js/src/base-component.js | 9 +++- js/src/button.js | 4 +- js/src/carousel.js | 10 ++-- js/src/collapse.js | 5 +- js/src/dom/manipulator.js | 7 ++- js/src/dom/selector-engine.js | 6 +-- js/src/dropdown.js | 19 ++++--- js/src/modal.js | 21 +++++--- js/src/offcanvas.js | 8 +-- js/src/scrollspy.js | 17 +++--- js/src/tab.js | 10 +++- js/src/tooltip.js | 8 +-- js/src/util/component-functions.js | 4 +- js/src/util/focustrap.js | 12 +++-- js/src/util/index.js | 38 +++++++++---- js/src/util/sanitizer.js | 5 +- js/src/util/scrollbar.js | 11 ++-- js/src/util/swipe.js | 6 +-- js/tests/integration/bundle-modularity.js | 1 + js/tests/integration/bundle.js | 1 + package-lock.json | 66 +++++++++++++++++++++++ package.json | 1 + 23 files changed, 203 insertions(+), 70 deletions(-) diff --git a/.eslintrc.json b/.eslintrc.json index d8e83a8d2e..123212f4a7 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -1,8 +1,10 @@ { "root": true, + "plugins": ["ssr-friendly"], "extends": [ "plugin:import/errors", "plugin:import/warnings", + "plugin:ssr-friendly/recommended", "plugin:unicorn/recommended", "xo", "xo/browser" @@ -50,6 +52,8 @@ "error", "never" ], + "ssr-friendly/no-dom-globals-in-react-cc-render": "off", + "ssr-friendly/no-dom-globals-in-react-fc": "off", "unicorn/explicit-length-check": "off", "unicorn/no-array-callback-reference": "off", "unicorn/no-array-method-this-argument": "off", diff --git a/js/src/base-component.js b/js/src/base-component.js index 4140bf1947..df64079bb2 100644 --- a/js/src/base-component.js +++ b/js/src/base-component.js @@ -6,7 +6,12 @@ */ import Data from './dom/data' -import { executeAfterTransition, getElement } from './util/index' +import { + executeAfterTransition, + getElement, + getWindow, + getDocument +} from './util/index' import EventHandler from './dom/event-handler' import Config from './util/config' @@ -30,6 +35,8 @@ class BaseComponent extends Config { } this._element = element + this._window = getWindow() + this._document = getDocument() this._config = this._getConfig(config) Data.set(this._element, this.constructor.DATA_KEY, this) diff --git a/js/src/button.js b/js/src/button.js index e2a52e7eba..2073714d61 100644 --- a/js/src/button.js +++ b/js/src/button.js @@ -5,7 +5,7 @@ * -------------------------------------------------------------------------- */ -import { defineJQueryPlugin } from './util/index' +import { defineJQueryPlugin, getDocument } from './util/index' import EventHandler from './dom/event-handler' import BaseComponent from './base-component' @@ -54,7 +54,7 @@ class Button extends BaseComponent { * Data API implementation */ -EventHandler.on(document, EVENT_CLICK_DATA_API, SELECTOR_DATA_TOGGLE, event => { +EventHandler.on(getDocument(), EVENT_CLICK_DATA_API, SELECTOR_DATA_TOGGLE, event => { event.preventDefault() const button = event.target.closest(SELECTOR_DATA_TOGGLE) diff --git a/js/src/carousel.js b/js/src/carousel.js index 5a0cbc208d..b4e6d06099 100644 --- a/js/src/carousel.js +++ b/js/src/carousel.js @@ -12,7 +12,9 @@ import { isRTL, isVisible, reflow, - triggerTransitionEnd + triggerTransitionEnd, + getDocument, + getWindow } from './util/index' import EventHandler from './dom/event-handler' import Manipulator from './dom/manipulator' @@ -129,7 +131,7 @@ class Carousel extends BaseComponent { // FIXME TODO use `document.visibilityState` // Don't call next when the page isn't visible // or the carousel or its parent isn't visible - if (!document.hidden && isVisible(this._element)) { + if (!this._document.hidden && isVisible(this._element)) { this.next() } } @@ -505,9 +507,9 @@ class Carousel extends BaseComponent { * Data API implementation */ -EventHandler.on(document, EVENT_CLICK_DATA_API, SELECTOR_DATA_SLIDE, Carousel.dataApiClickHandler) +EventHandler.on(getDocument(), EVENT_CLICK_DATA_API, SELECTOR_DATA_SLIDE, Carousel.dataApiClickHandler) -EventHandler.on(window, EVENT_LOAD_DATA_API, () => { +EventHandler.on(getWindow(), EVENT_LOAD_DATA_API, () => { const carousels = SelectorEngine.find(SELECTOR_DATA_RIDE) for (const carousel of carousels) { diff --git a/js/src/collapse.js b/js/src/collapse.js index 8894342dfc..1d6e33af88 100644 --- a/js/src/collapse.js +++ b/js/src/collapse.js @@ -10,7 +10,8 @@ import { getElement, getElementFromSelector, getSelectorFromElement, - reflow + reflow, + getDocument } from './util/index' import EventHandler from './dom/event-handler' import SelectorEngine from './dom/selector-engine' @@ -279,7 +280,7 @@ class Collapse extends BaseComponent { * Data API implementation */ -EventHandler.on(document, EVENT_CLICK_DATA_API, SELECTOR_DATA_TOGGLE, function (event) { +EventHandler.on(getDocument(), EVENT_CLICK_DATA_API, SELECTOR_DATA_TOGGLE, function (event) { // preventDefault only for elements (which change the URL) not inside the collapsible element if (event.target.tagName === 'A' || (event.delegateTarget && event.delegateTarget.tagName === 'A')) { event.preventDefault() diff --git a/js/src/dom/manipulator.js b/js/src/dom/manipulator.js index e3ee293c7d..8ad793fd91 100644 --- a/js/src/dom/manipulator.js +++ b/js/src/dom/manipulator.js @@ -5,6 +5,8 @@ * -------------------------------------------------------------------------- */ +import { getWindow } from '../util/index' + function normalizeData(value) { if (value === 'true') { return true @@ -61,10 +63,11 @@ const Manipulator = { offset(element) { const rect = element.getBoundingClientRect() + const windowRef = getWindow() return { - top: rect.top + window.pageYOffset, - left: rect.left + window.pageXOffset + top: rect.top + windowRef.pageYOffset, + left: rect.left + windowRef.pageXOffset } }, diff --git a/js/src/dom/selector-engine.js b/js/src/dom/selector-engine.js index ed565bebbf..69a3a29adf 100644 --- a/js/src/dom/selector-engine.js +++ b/js/src/dom/selector-engine.js @@ -5,18 +5,18 @@ * -------------------------------------------------------------------------- */ -import { isDisabled, isVisible } from '../util/index' +import { getDocument, isDisabled, isVisible } from '../util/index' /** * Constants */ const SelectorEngine = { - find(selector, element = document.documentElement) { + find(selector, element = getDocument().documentElement) { return [].concat(...Element.prototype.querySelectorAll.call(element, selector)) }, - findOne(selector, element = document.documentElement) { + findOne(selector, element = getDocument().documentElement) { return Element.prototype.querySelector.call(element, selector) }, diff --git a/js/src/dropdown.js b/js/src/dropdown.js index 5635ec96ec..33aa8a0963 100644 --- a/js/src/dropdown.js +++ b/js/src/dropdown.js @@ -14,7 +14,8 @@ import { isElement, isRTL, isVisible, - noop + noop, + getDocument } from './util/index' import EventHandler from './dom/event-handler' import Manipulator from './dom/manipulator' @@ -133,8 +134,8 @@ class Dropdown extends BaseComponent { // empty mouseover listeners to the body's immediate children; // only needed because of broken event delegation on iOS // https://www.quirksmode.org/blog/archives/2014/02/mouse_event_bub.html - if ('ontouchstart' in document.documentElement && !this._parent.closest(SELECTOR_NAVBAR_NAV)) { - for (const element of [].concat(...document.body.children)) { + if ('ontouchstart' in this._document.documentElement && !this._parent.closest(SELECTOR_NAVBAR_NAV)) { + for (const element of [].concat(...this._document.body.children)) { EventHandler.on(element, 'mouseover', noop) } } @@ -434,11 +435,13 @@ class Dropdown extends BaseComponent { * Data API implementation */ -EventHandler.on(document, EVENT_KEYDOWN_DATA_API, SELECTOR_DATA_TOGGLE, Dropdown.dataApiKeydownHandler) -EventHandler.on(document, EVENT_KEYDOWN_DATA_API, SELECTOR_MENU, Dropdown.dataApiKeydownHandler) -EventHandler.on(document, EVENT_CLICK_DATA_API, Dropdown.clearMenus) -EventHandler.on(document, EVENT_KEYUP_DATA_API, Dropdown.clearMenus) -EventHandler.on(document, EVENT_CLICK_DATA_API, SELECTOR_DATA_TOGGLE, function (event) { +const documentRef = getDocument() + +EventHandler.on(documentRef, EVENT_KEYDOWN_DATA_API, SELECTOR_DATA_TOGGLE, Dropdown.dataApiKeydownHandler) +EventHandler.on(documentRef, EVENT_KEYDOWN_DATA_API, SELECTOR_MENU, Dropdown.dataApiKeydownHandler) +EventHandler.on(documentRef, EVENT_CLICK_DATA_API, Dropdown.clearMenus) +EventHandler.on(documentRef, EVENT_KEYUP_DATA_API, Dropdown.clearMenus) +EventHandler.on(documentRef, EVENT_CLICK_DATA_API, SELECTOR_DATA_TOGGLE, function (event) { event.preventDefault() Dropdown.getOrCreateInstance(this).toggle() }) diff --git a/js/src/modal.js b/js/src/modal.js index 054750c5f7..3310376060 100644 --- a/js/src/modal.js +++ b/js/src/modal.js @@ -5,7 +5,14 @@ * -------------------------------------------------------------------------- */ -import { defineJQueryPlugin, getElementFromSelector, isRTL, isVisible, reflow } from './util/index' +import { + defineJQueryPlugin, + getElementFromSelector, + isRTL, + isVisible, + reflow, + getDocument +} from './util/index' import EventHandler from './dom/event-handler' import SelectorEngine from './dom/selector-engine' import ScrollBarHelper from './util/scrollbar' @@ -184,8 +191,8 @@ class Modal extends BaseComponent { _showElement(relatedTarget) { // try to append dynamic modal - if (!document.body.contains(this._element)) { - document.body.append(this._element) + if (!this._document.body.contains(this._element)) { + this._document.body.append(this._element) } this._element.style.display = 'block' @@ -255,7 +262,7 @@ class Modal extends BaseComponent { this._isTransitioning = false this._backdrop.hide(() => { - document.body.classList.remove(CLASS_NAME_OPEN) + this._document.body.classList.remove(CLASS_NAME_OPEN) this._resetAdjustments() this._scrollBar.reset() EventHandler.trigger(this._element, EVENT_HIDDEN) @@ -272,7 +279,7 @@ class Modal extends BaseComponent { return } - const isModalOverflowing = this._element.scrollHeight > document.documentElement.clientHeight + const isModalOverflowing = this._element.scrollHeight > this._document.documentElement.clientHeight const initialOverflowY = this._element.style.overflowY // return if the following background transition hasn't yet completed if (initialOverflowY === 'hidden' || this._element.classList.contains(CLASS_NAME_STATIC)) { @@ -299,7 +306,7 @@ class Modal extends BaseComponent { */ _adjustDialog() { - const isModalOverflowing = this._element.scrollHeight > document.documentElement.clientHeight + const isModalOverflowing = this._element.scrollHeight > this._document.documentElement.clientHeight const scrollbarWidth = this._scrollBar.getWidth() const isBodyOverflowing = scrollbarWidth > 0 @@ -341,7 +348,7 @@ class Modal extends BaseComponent { * Data API implementation */ -EventHandler.on(document, EVENT_CLICK_DATA_API, SELECTOR_DATA_TOGGLE, function (event) { +EventHandler.on(getDocument(), EVENT_CLICK_DATA_API, SELECTOR_DATA_TOGGLE, function (event) { const target = getElementFromSelector(this) if (['A', 'AREA'].includes(this.tagName)) { diff --git a/js/src/offcanvas.js b/js/src/offcanvas.js index 2735a9c2ae..e610d714b8 100644 --- a/js/src/offcanvas.js +++ b/js/src/offcanvas.js @@ -9,7 +9,9 @@ import { defineJQueryPlugin, getElementFromSelector, isDisabled, - isVisible + isVisible, + getDocument, + getWindow } from './util/index' import ScrollBarHelper from './util/scrollbar' import EventHandler from './dom/event-handler' @@ -209,7 +211,7 @@ class Offcanvas extends BaseComponent { * Data API implementation */ -EventHandler.on(document, EVENT_CLICK_DATA_API, SELECTOR_DATA_TOGGLE, function (event) { +EventHandler.on(getDocument(), EVENT_CLICK_DATA_API, SELECTOR_DATA_TOGGLE, function (event) { const target = getElementFromSelector(this) if (['A', 'AREA'].includes(this.tagName)) { @@ -237,7 +239,7 @@ EventHandler.on(document, EVENT_CLICK_DATA_API, SELECTOR_DATA_TOGGLE, function ( data.toggle(this) }) -EventHandler.on(window, EVENT_LOAD_DATA_API, () => { +EventHandler.on(getWindow(), EVENT_LOAD_DATA_API, () => { for (const selector of SelectorEngine.find(OPEN_SELECTOR)) { Offcanvas.getOrCreateInstance(selector).show() } diff --git a/js/src/scrollspy.js b/js/src/scrollspy.js index 029970ed2a..c5b0a5c7e6 100644 --- a/js/src/scrollspy.js +++ b/js/src/scrollspy.js @@ -5,7 +5,12 @@ * -------------------------------------------------------------------------- */ -import { defineJQueryPlugin, getElement, getSelectorFromElement } from './util/index' +import { + defineJQueryPlugin, + getElement, + getSelectorFromElement, + getWindow +} from './util/index' import EventHandler from './dom/event-handler' import Manipulator from './dom/manipulator' import SelectorEngine from './dom/selector-engine' @@ -58,7 +63,7 @@ const DefaultType = { class ScrollSpy extends BaseComponent { constructor(element, config) { super(element, config) - this._scrollElement = this._element.tagName === 'BODY' ? window : this._element + this._scrollElement = this._element.tagName === 'BODY' ? this._window : this._element this._offsets = [] this._targets = [] this._activeTarget = null @@ -137,14 +142,14 @@ class ScrollSpy extends BaseComponent { _getScrollHeight() { return this._scrollElement.scrollHeight || Math.max( - document.body.scrollHeight, - document.documentElement.scrollHeight + this._document.body.scrollHeight, + this._document.documentElement.scrollHeight ) } _getOffsetHeight() { return this._scrollElement === window ? - window.innerHeight : + this._window.innerHeight : this._scrollElement.getBoundingClientRect().height } @@ -251,7 +256,7 @@ class ScrollSpy extends BaseComponent { * Data API implementation */ -EventHandler.on(window, EVENT_LOAD_DATA_API, () => { +EventHandler.on(getWindow(), EVENT_LOAD_DATA_API, () => { for (const spy of SelectorEngine.find(SELECTOR_DATA_SPY)) { new ScrollSpy(spy) // eslint-disable-line no-new } diff --git a/js/src/tab.js b/js/src/tab.js index f9969fb7a4..06435f395a 100644 --- a/js/src/tab.js +++ b/js/src/tab.js @@ -5,7 +5,13 @@ * -------------------------------------------------------------------------- */ -import { defineJQueryPlugin, getElementFromSelector, isDisabled, reflow } from './util/index' +import { + defineJQueryPlugin, + getDocument, + getElementFromSelector, + isDisabled, + reflow +} from './util/index' import EventHandler from './dom/event-handler' import SelectorEngine from './dom/selector-engine' import BaseComponent from './base-component' @@ -177,7 +183,7 @@ class Tab extends BaseComponent { * Data API implementation */ -EventHandler.on(document, EVENT_CLICK_DATA_API, SELECTOR_DATA_TOGGLE, function (event) { +EventHandler.on(getDocument(), EVENT_CLICK_DATA_API, SELECTOR_DATA_TOGGLE, function (event) { if (['A', 'AREA'].includes(this.tagName)) { event.preventDefault() } diff --git a/js/src/tooltip.js b/js/src/tooltip.js index ef5b9fa825..640de68a9c 100644 --- a/js/src/tooltip.js +++ b/js/src/tooltip.js @@ -245,8 +245,8 @@ class Tooltip extends BaseComponent { // empty mouseover listeners to the body's immediate children; // only needed because of broken event delegation on iOS // https://www.quirksmode.org/blog/archives/2014/02/mouse_event_bub.html - if ('ontouchstart' in document.documentElement) { - for (const element of [].concat(...document.body.children)) { + if ('ontouchstart' in this._document.documentElement) { + for (const element of [].concat(...this._document.body.children)) { EventHandler.on(element, 'mouseover', noop) } } @@ -280,8 +280,8 @@ class Tooltip extends BaseComponent { // If this is a touch-enabled device we remove the extra // empty mouseover listeners we added for iOS support - if ('ontouchstart' in document.documentElement) { - for (const element of [].concat(...document.body.children)) { + if ('ontouchstart' in this._document.documentElement) { + for (const element of [].concat(...this._document.body.children)) { EventHandler.off(element, 'mouseover', noop) } } diff --git a/js/src/util/component-functions.js b/js/src/util/component-functions.js index bd44c3fdc4..d6e49f83c5 100644 --- a/js/src/util/component-functions.js +++ b/js/src/util/component-functions.js @@ -6,13 +6,13 @@ */ import EventHandler from '../dom/event-handler' -import { getElementFromSelector, isDisabled } from './index' +import { getElementFromSelector, isDisabled, getDocument } 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) { + EventHandler.on(getDocument(), clickEvent, `[data-bs-dismiss="${name}"]`, function (event) { if (['A', 'AREA'].includes(this.tagName)) { event.preventDefault() } diff --git a/js/src/util/focustrap.js b/js/src/util/focustrap.js index 88fd16b104..a1131bc90e 100644 --- a/js/src/util/focustrap.js +++ b/js/src/util/focustrap.js @@ -7,6 +7,7 @@ import EventHandler from '../dom/event-handler' import SelectorEngine from '../dom/selector-engine' +import { getDocument } from './index' import Config from './config' /** @@ -43,6 +44,7 @@ class FocusTrap extends Config { this._config = this._getConfig(config) this._isActive = false this._lastTabNavDirection = null + this._document = getDocument() } // Getters @@ -68,9 +70,9 @@ class FocusTrap extends Config { 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)) + EventHandler.off(this._document, EVENT_KEY) // guard against infinite focus loop + EventHandler.on(this._document, EVENT_FOCUSIN, event => this._handleFocusin(event)) + EventHandler.on(this._document, EVENT_KEYDOWN_TAB, event => this._handleKeydown(event)) this._isActive = true } @@ -81,14 +83,14 @@ class FocusTrap extends Config { } this._isActive = false - EventHandler.off(document, EVENT_KEY) + EventHandler.off(this._document, EVENT_KEY) } // Private _handleFocusin(event) { const { trapElement } = this._config - if (event.target === document || event.target === trapElement || trapElement.contains(event.target)) { + if (event.target === this._document || event.target === trapElement || trapElement.contains(event.target)) { return } diff --git a/js/src/util/index.js b/js/src/util/index.js index 4e52fd3eb0..f6ef29df34 100644 --- a/js/src/util/index.js +++ b/js/src/util/index.js @@ -25,7 +25,7 @@ const toType = object => { const getUID = prefix => { do { prefix += Math.floor(Math.random() * MAX_UID) - } while (document.getElementById(prefix)) + } while (getDocument().getElementById(prefix)) return prefix } @@ -59,7 +59,7 @@ const getSelectorFromElement = element => { const selector = getSelector(element) if (selector) { - return document.querySelector(selector) ? selector : null + return getDocument().querySelector(selector) ? selector : null } return null @@ -68,7 +68,7 @@ const getSelectorFromElement = element => { const getElementFromSelector = element => { const selector = getSelector(element) - return selector ? document.querySelector(selector) : null + return selector ? getDocument().querySelector(selector) : null } const getTransitionDurationFromElement = element => { @@ -77,7 +77,7 @@ const getTransitionDurationFromElement = element => { } // Get transition-duration of the element - let { transitionDuration, transitionDelay } = window.getComputedStyle(element) + let { transitionDuration, transitionDelay } = getWindow().getComputedStyle(element) const floatTransitionDuration = Number.parseFloat(transitionDuration) const floatTransitionDelay = Number.parseFloat(transitionDelay) @@ -167,7 +167,7 @@ const isDisabled = element => { } const findShadowRoot = element => { - if (!document.documentElement.attachShadow) { + if (!getDocument().documentElement.attachShadow) { return null } @@ -200,11 +200,12 @@ const noop = () => {} * @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 + // eslint-disable-next-line no-unused-expressions + element.offsetHeight } const getjQuery = () => { - if (window.jQuery && !document.body.hasAttribute('data-bs-no-jquery')) { + if (getWindow().jQuery && !getDocument().body.hasAttribute('data-bs-no-jquery')) { return window.jQuery } @@ -214,10 +215,11 @@ const getjQuery = () => { const DOMContentLoadedCallbacks = [] const onDOMContentLoaded = callback => { - if (document.readyState === 'loading') { + const documentRef = getDocument() + if (documentRef.readyState === 'loading') { // add listener on the first call when the document is in loading state if (!DOMContentLoadedCallbacks.length) { - document.addEventListener('DOMContentLoaded', () => { + documentRef.addEventListener('DOMContentLoaded', () => { for (const callback of DOMContentLoadedCallbacks) { callback() } @@ -230,7 +232,7 @@ const onDOMContentLoaded = callback => { } } -const isRTL = () => document.documentElement.dir === 'rtl' +const isRTL = () => getDocument().documentElement.dir === 'rtl' const defineJQueryPlugin = plugin => { onDOMContentLoaded(() => { @@ -312,11 +314,26 @@ const getNextActiveElement = (list, activeElement, shouldGetNext, isCycleAllowed return list[Math.max(0, Math.min(index, listLength - 1))] } +/** + * @return {window|{}} The proper element + */ +const getWindow = () => { + return typeof window !== 'undefined' ? window : {} +} + +/** + * @return {document|{}} The proper element + */ +const getDocument = () => { + return typeof document !== 'undefined' ? document : {} +} + export { defineJQueryPlugin, execute, executeAfterTransition, findShadowRoot, + getDocument, getElement, getElementFromSelector, getjQuery, @@ -324,6 +341,7 @@ export { getSelectorFromElement, getTransitionDurationFromElement, getUID, + getWindow, isDisabled, isElement, isRTL, diff --git a/js/src/util/sanitizer.js b/js/src/util/sanitizer.js index 1db61ae707..22dd663ffc 100644 --- a/js/src/util/sanitizer.js +++ b/js/src/util/sanitizer.js @@ -5,6 +5,8 @@ * -------------------------------------------------------------------------- */ +import { getWindow } from './index' + const uriAttributes = new Set([ 'background', 'cite', @@ -91,7 +93,8 @@ export function sanitizeHtml(unsafeHtml, allowList, sanitizeFunction) { return sanitizeFunction(unsafeHtml) } - const domParser = new window.DOMParser() + const windowRef = getWindow() + const domParser = new windowRef.DOMParser() const createdDocument = domParser.parseFromString(unsafeHtml, 'text/html') const elements = [].concat(...createdDocument.body.querySelectorAll('*')) diff --git a/js/src/util/scrollbar.js b/js/src/util/scrollbar.js index 86a2bca01f..c19123c0f7 100644 --- a/js/src/util/scrollbar.js +++ b/js/src/util/scrollbar.js @@ -7,7 +7,7 @@ import SelectorEngine from '../dom/selector-engine' import Manipulator from '../dom/manipulator' -import { isElement } from './index' +import { isElement, getDocument, getWindow } from './index' /** * Constants @@ -24,14 +24,15 @@ const PROPERTY_MARGIN = 'margin-right' class ScrollBarHelper { constructor() { - this._element = document.body + this._element = getDocument().body + this._window = getWindow() } // 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) + return Math.abs(this._window.innerWidth - documentWidth) } hide() { @@ -64,12 +65,12 @@ class ScrollBarHelper { _setElementAttributes(selector, styleProperty, callback) { const scrollbarWidth = this.getWidth() const manipulationCallBack = element => { - if (element !== this._element && window.innerWidth > element.clientWidth + scrollbarWidth) { + if (element !== this._element && this._window.innerWidth > element.clientWidth + scrollbarWidth) { return } this._saveInitialAttribute(element, styleProperty) - const calculatedValue = window.getComputedStyle(element).getPropertyValue(styleProperty) + const calculatedValue = this._window.getComputedStyle(element).getPropertyValue(styleProperty) element.style.setProperty(styleProperty, `${callback(Number.parseFloat(calculatedValue))}px`) } diff --git a/js/src/util/swipe.js b/js/src/util/swipe.js index ac09b6fa13..b2578c952d 100644 --- a/js/src/util/swipe.js +++ b/js/src/util/swipe.js @@ -7,7 +7,7 @@ import Config from './config' import EventHandler from '../dom/event-handler' -import { execute } from './index' +import { execute, getDocument, getWindow } from './index' /** * Constants @@ -52,7 +52,7 @@ class Swipe extends Config { this._config = this._getConfig(config) this._deltaX = 0 - this._supportPointerEvents = Boolean(window.PointerEvent) + this._supportPointerEvents = Boolean(getWindow().PointerEvent) this._initEvents() } @@ -139,7 +139,7 @@ class Swipe extends Config { // Static static isSupported() { - return 'ontouchstart' in document.documentElement || navigator.maxTouchPoints > 0 + return 'ontouchstart' in getDocument().documentElement || navigator.maxTouchPoints > 0 } } diff --git a/js/tests/integration/bundle-modularity.js b/js/tests/integration/bundle-modularity.js index 8546141b19..ecfd2335f9 100644 --- a/js/tests/integration/bundle-modularity.js +++ b/js/tests/integration/bundle-modularity.js @@ -1,6 +1,7 @@ import Tooltip from '../../dist/tooltip' import '../../dist/carousel' +// eslint-disable-next-line ssr-friendly/no-dom-globals-in-module-scope window.addEventListener('load', () => { [].concat(...document.querySelectorAll('[data-bs-toggle="tooltip"]')) .map(tooltipNode => new Tooltip(tooltipNode)) diff --git a/js/tests/integration/bundle.js b/js/tests/integration/bundle.js index 452088a7d8..8c5442626d 100644 --- a/js/tests/integration/bundle.js +++ b/js/tests/integration/bundle.js @@ -1,5 +1,6 @@ import { Tooltip } from '../../../dist/js/bootstrap.esm.js' +// eslint-disable-next-line ssr-friendly/no-dom-globals-in-module-scope window.addEventListener('load', () => { [].concat(...document.querySelectorAll('[data-bs-toggle="tooltip"]')) .map(tooltipNode => new Tooltip(tooltipNode)) diff --git a/package-lock.json b/package-lock.json index ffaf2fed9a..755672fbd0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24,6 +24,7 @@ "eslint": "^8.8.0", "eslint-config-xo": "^0.39.0", "eslint-plugin-import": "^2.25.4", + "eslint-plugin-ssr-friendly": "^1.0.5", "eslint-plugin-unicorn": "^40.1.0", "find-unused-sass-variables": "^3.1.0", "globby": "^11.0.4", @@ -4319,6 +4320,45 @@ "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", "dev": true }, + "node_modules/eslint-plugin-ssr-friendly": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/eslint-plugin-ssr-friendly/-/eslint-plugin-ssr-friendly-1.0.5.tgz", + "integrity": "sha512-F1vKfzhOnrIXhcx91Y3r1x8vjJAoCex25PUgYErOe6q95T4KuCTz6+LgGQ4TTvhBdCfNqu1U0krAHe3UNuEOqg==", + "dev": true, + "dependencies": { + "globals": "^13.2.0" + }, + "peerDependencies": { + "eslint": ">=0.8.0" + } + }, + "node_modules/eslint-plugin-ssr-friendly/node_modules/globals": { + "version": "13.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.12.0.tgz", + "integrity": "sha512-uS8X6lSKN2JumVoXrbUz+uG4BYG+eiawqm3qFcT7ammfbUHeCBoJMlHcec/S3krSk73/AE/f0szYFmgAA3kYZg==", + "dev": true, + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint-plugin-ssr-friendly/node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/eslint-plugin-unicorn": { "version": "40.1.0", "resolved": "https://registry.npmjs.org/eslint-plugin-unicorn/-/eslint-plugin-unicorn-40.1.0.tgz", @@ -14030,6 +14070,32 @@ } } }, + "eslint-plugin-ssr-friendly": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/eslint-plugin-ssr-friendly/-/eslint-plugin-ssr-friendly-1.0.5.tgz", + "integrity": "sha512-F1vKfzhOnrIXhcx91Y3r1x8vjJAoCex25PUgYErOe6q95T4KuCTz6+LgGQ4TTvhBdCfNqu1U0krAHe3UNuEOqg==", + "dev": true, + "requires": { + "globals": "^13.2.0" + }, + "dependencies": { + "globals": { + "version": "13.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.12.0.tgz", + "integrity": "sha512-uS8X6lSKN2JumVoXrbUz+uG4BYG+eiawqm3qFcT7ammfbUHeCBoJMlHcec/S3krSk73/AE/f0szYFmgAA3kYZg==", + "dev": true, + "requires": { + "type-fest": "^0.20.2" + } + }, + "type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true + } + } + }, "eslint-plugin-unicorn": { "version": "40.1.0", "resolved": "https://registry.npmjs.org/eslint-plugin-unicorn/-/eslint-plugin-unicorn-40.1.0.tgz", diff --git a/package.json b/package.json index ff6f43f459..7d12e449c6 100644 --- a/package.json +++ b/package.json @@ -113,6 +113,7 @@ "eslint": "^8.8.0", "eslint-config-xo": "^0.39.0", "eslint-plugin-import": "^2.25.4", + "eslint-plugin-ssr-friendly": "^1.0.5", "eslint-plugin-unicorn": "^40.1.0", "find-unused-sass-variables": "^3.1.0", "globby": "^11.0.4",