]> git.ipfire.org Git - thirdparty/bootstrap.git/commitdiff
Be SSR friendly when accessing DOM objects
authorJohann-S <johann.servoire@gmail.com>
Wed, 17 Feb 2021 07:22:44 +0000 (09:22 +0200)
committerXhmikosR <xhmikosr@gmail.com>
Tue, 1 Feb 2022 07:15:13 +0000 (09:15 +0200)
23 files changed:
.eslintrc.json
js/src/base-component.js
js/src/button.js
js/src/carousel.js
js/src/collapse.js
js/src/dom/manipulator.js
js/src/dom/selector-engine.js
js/src/dropdown.js
js/src/modal.js
js/src/offcanvas.js
js/src/scrollspy.js
js/src/tab.js
js/src/tooltip.js
js/src/util/component-functions.js
js/src/util/focustrap.js
js/src/util/index.js
js/src/util/sanitizer.js
js/src/util/scrollbar.js
js/src/util/swipe.js
js/tests/integration/bundle-modularity.js
js/tests/integration/bundle.js
package-lock.json
package.json

index d8e83a8d2eec8f9da62e0280c61c67d9b39b9103..123212f4a7b2760c9ed063844c28ffaafc575099 100644 (file)
@@ -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",
index 4140bf19475b480beb3c9f4a12d15d834e882f55..df64079bb26c9c26a7ac664e31fb49fb728c1b56 100644 (file)
@@ -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)
index e2a52e7ebaa8ba3f1af93c02e9211d80f130ca14..2073714d61e844edc1a8ac9f3675076a77c3907f 100644 (file)
@@ -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)
index 5a0cbc208de744c19d963480ba4641921bbc6e85..b4e6d060999c9719ca123d1438fe3854dc61367b 100644 (file)
@@ -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) {
index 8894342dfc29a7ac5b26b8ba28298a75fa10cfc0..1d6e33af881ce538ff835e465133da327ed66935 100644 (file)
@@ -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 <a> elements (which change the URL) not inside the collapsible element
   if (event.target.tagName === 'A' || (event.delegateTarget && event.delegateTarget.tagName === 'A')) {
     event.preventDefault()
index e3ee293c7df488308f02bbc1c234d2fc82542d66..8ad793fd91000f3a68a84f1038c757e225b30cab 100644 (file)
@@ -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
     }
   },
 
index ed565bebbf4e587cff64fce73018fe2e931c0748..69a3a29adf92d3a4b0cbb606e7f8810cd2a330c4 100644 (file)
@@ -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)
   },
 
index 5635ec96ec6c495389ab8ef83e5d0f3f13b50ac1..33aa8a0963f4ca9dfb2fd567f9c76a4ad83c413c 100644 (file)
@@ -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()
 })
index 054750c5f7fdba8bc3331b4e957c0b0d781aa4f7..3310376060edfae4412a9e947881fb63592518b7 100644 (file)
@@ -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)) {
index 2735a9c2aeacf01269cc5eb1c291936e09f274f3..e610d714b8c0ff79fa4e7de35f453b554c53d066 100644 (file)
@@ -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()
   }
index 029970ed2a5d54b95f269ecdde41b55a98b4a864..c5b0a5c7e660d44b3b9c9ecd281f77e6cbd9582e 100644 (file)
@@ -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
   }
index f9969fb7a4eccf81439d758aaec28a8e9c468b5e..06435f395a4eb838b511fabf3dade81d529c68ee 100644 (file)
@@ -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()
   }
index ef5b9fa825a261c9841138608a1a40efd9cee1df..640de68a9c02f9a0d45632d54e85d5fcb7b283b7 100644 (file)
@@ -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)
       }
     }
index bd44c3fdc4ba1f613c3331faa0544ac545089bff..d6e49f83c511d57f68ec7dee04f78cdce8623ed8 100644 (file)
@@ -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()
     }
index 88fd16b10447b85f9ced36eae60ff91f9612eaa6..a1131bc90e33c2c65826a0c82f3ae40b84652e17 100644 (file)
@@ -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
     }
 
index 4e52fd3eb0bfc6e95eca99ec6f7630f483852778..f6ef29df34f3a22966204197d08baa01db79f661 100644 (file)
@@ -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,
index 1db61ae707c24303234654fda8775a170c31452a..22dd663ffcd7aa7b86dd326840f1a0be55e49006 100644 (file)
@@ -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('*'))
 
index 86a2bca01ff33d5a0d3b054f702e1ddfec5fa72b..c19123c0f71317a1195f12175112f1ae5f2b9499 100644 (file)
@@ -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`)
     }
 
index ac09b6fa1399228fa6d9caa713d64b338dd2e460..b2578c952d2f67746fbc2d4b539a76666b478a19 100644 (file)
@@ -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
   }
 }
 
index 8546141b195544fbed5a0ea23c5cdaa6005080f6..ecfd2335f9b75b5abc550cdcd544a599aadc54f0 100644 (file)
@@ -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))
index 452088a7d811718dcb98572e1ce3f855860849f0..8c5442626df9495e5f156f8197b946b43effe12f 100644 (file)
@@ -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))
index ffaf2fed9acf2dcc4781e6695658c446745ead53..755672fbd009b55aad9852ff941a652d4f850c95 100644 (file)
@@ -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",
       "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",
         }
       }
     },
+    "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",
index ff6f43f459fb027669ca632a041d889f0ad41025..7d12e449c641ec54eeb37a95b26eb29cb510775e 100644 (file)
     "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",