0
0
mirror of https://github.com/twbs/bootstrap.git synced 2025-02-26 23:54:23 +01:00

Be SSR friendly when accessing DOM objects

This commit is contained in:
Johann-S 2021-02-17 09:22:44 +02:00 committed by XhmikosR
parent c44d99f55c
commit 8306213650
23 changed files with 203 additions and 70 deletions

View File

@ -1,8 +1,10 @@
{ {
"root": true, "root": true,
"plugins": ["ssr-friendly"],
"extends": [ "extends": [
"plugin:import/errors", "plugin:import/errors",
"plugin:import/warnings", "plugin:import/warnings",
"plugin:ssr-friendly/recommended",
"plugin:unicorn/recommended", "plugin:unicorn/recommended",
"xo", "xo",
"xo/browser" "xo/browser"
@ -50,6 +52,8 @@
"error", "error",
"never" "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/explicit-length-check": "off",
"unicorn/no-array-callback-reference": "off", "unicorn/no-array-callback-reference": "off",
"unicorn/no-array-method-this-argument": "off", "unicorn/no-array-method-this-argument": "off",

View File

@ -6,7 +6,12 @@
*/ */
import Data from './dom/data' 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 EventHandler from './dom/event-handler'
import Config from './util/config' import Config from './util/config'
@ -30,6 +35,8 @@ class BaseComponent extends Config {
} }
this._element = element this._element = element
this._window = getWindow()
this._document = getDocument()
this._config = this._getConfig(config) this._config = this._getConfig(config)
Data.set(this._element, this.constructor.DATA_KEY, this) Data.set(this._element, this.constructor.DATA_KEY, this)

View File

@ -5,7 +5,7 @@
* -------------------------------------------------------------------------- * --------------------------------------------------------------------------
*/ */
import { defineJQueryPlugin } from './util/index' import { defineJQueryPlugin, getDocument } from './util/index'
import EventHandler from './dom/event-handler' import EventHandler from './dom/event-handler'
import BaseComponent from './base-component' import BaseComponent from './base-component'
@ -54,7 +54,7 @@ class Button extends BaseComponent {
* Data API implementation * 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() event.preventDefault()
const button = event.target.closest(SELECTOR_DATA_TOGGLE) const button = event.target.closest(SELECTOR_DATA_TOGGLE)

View File

@ -12,7 +12,9 @@ import {
isRTL, isRTL,
isVisible, isVisible,
reflow, reflow,
triggerTransitionEnd triggerTransitionEnd,
getDocument,
getWindow
} from './util/index' } from './util/index'
import EventHandler from './dom/event-handler' import EventHandler from './dom/event-handler'
import Manipulator from './dom/manipulator' import Manipulator from './dom/manipulator'
@ -129,7 +131,7 @@ class Carousel extends BaseComponent {
// FIXME TODO use `document.visibilityState` // FIXME TODO use `document.visibilityState`
// Don't call next when the page isn't visible // Don't call next when the page isn't visible
// or the carousel or its parent 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() this.next()
} }
} }
@ -505,9 +507,9 @@ class Carousel extends BaseComponent {
* Data API implementation * 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) const carousels = SelectorEngine.find(SELECTOR_DATA_RIDE)
for (const carousel of carousels) { for (const carousel of carousels) {

View File

@ -10,7 +10,8 @@ import {
getElement, getElement,
getElementFromSelector, getElementFromSelector,
getSelectorFromElement, getSelectorFromElement,
reflow reflow,
getDocument
} from './util/index' } from './util/index'
import EventHandler from './dom/event-handler' import EventHandler from './dom/event-handler'
import SelectorEngine from './dom/selector-engine' import SelectorEngine from './dom/selector-engine'
@ -279,7 +280,7 @@ class Collapse extends BaseComponent {
* Data API implementation * 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 // 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')) { if (event.target.tagName === 'A' || (event.delegateTarget && event.delegateTarget.tagName === 'A')) {
event.preventDefault() event.preventDefault()

View File

@ -5,6 +5,8 @@
* -------------------------------------------------------------------------- * --------------------------------------------------------------------------
*/ */
import { getWindow } from '../util/index'
function normalizeData(value) { function normalizeData(value) {
if (value === 'true') { if (value === 'true') {
return true return true
@ -61,10 +63,11 @@ const Manipulator = {
offset(element) { offset(element) {
const rect = element.getBoundingClientRect() const rect = element.getBoundingClientRect()
const windowRef = getWindow()
return { return {
top: rect.top + window.pageYOffset, top: rect.top + windowRef.pageYOffset,
left: rect.left + window.pageXOffset left: rect.left + windowRef.pageXOffset
} }
}, },

View File

@ -5,18 +5,18 @@
* -------------------------------------------------------------------------- * --------------------------------------------------------------------------
*/ */
import { isDisabled, isVisible } from '../util/index' import { getDocument, isDisabled, isVisible } from '../util/index'
/** /**
* Constants * Constants
*/ */
const SelectorEngine = { const SelectorEngine = {
find(selector, element = document.documentElement) { find(selector, element = getDocument().documentElement) {
return [].concat(...Element.prototype.querySelectorAll.call(element, selector)) 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) return Element.prototype.querySelector.call(element, selector)
}, },

View File

@ -14,7 +14,8 @@ import {
isElement, isElement,
isRTL, isRTL,
isVisible, isVisible,
noop noop,
getDocument
} from './util/index' } from './util/index'
import EventHandler from './dom/event-handler' import EventHandler from './dom/event-handler'
import Manipulator from './dom/manipulator' import Manipulator from './dom/manipulator'
@ -133,8 +134,8 @@ class Dropdown extends BaseComponent {
// empty mouseover listeners to the body's immediate children; // empty mouseover listeners to the body's immediate children;
// only needed because of broken event delegation on iOS // only needed because of broken event delegation on iOS
// https://www.quirksmode.org/blog/archives/2014/02/mouse_event_bub.html // https://www.quirksmode.org/blog/archives/2014/02/mouse_event_bub.html
if ('ontouchstart' in document.documentElement && !this._parent.closest(SELECTOR_NAVBAR_NAV)) { if ('ontouchstart' in this._document.documentElement && !this._parent.closest(SELECTOR_NAVBAR_NAV)) {
for (const element of [].concat(...document.body.children)) { for (const element of [].concat(...this._document.body.children)) {
EventHandler.on(element, 'mouseover', noop) EventHandler.on(element, 'mouseover', noop)
} }
} }
@ -434,11 +435,13 @@ class Dropdown extends BaseComponent {
* Data API implementation * Data API implementation
*/ */
EventHandler.on(document, EVENT_KEYDOWN_DATA_API, SELECTOR_DATA_TOGGLE, Dropdown.dataApiKeydownHandler) const documentRef = getDocument()
EventHandler.on(document, EVENT_KEYDOWN_DATA_API, SELECTOR_MENU, Dropdown.dataApiKeydownHandler)
EventHandler.on(document, EVENT_CLICK_DATA_API, Dropdown.clearMenus) EventHandler.on(documentRef, EVENT_KEYDOWN_DATA_API, SELECTOR_DATA_TOGGLE, Dropdown.dataApiKeydownHandler)
EventHandler.on(document, EVENT_KEYUP_DATA_API, Dropdown.clearMenus) EventHandler.on(documentRef, EVENT_KEYDOWN_DATA_API, SELECTOR_MENU, Dropdown.dataApiKeydownHandler)
EventHandler.on(document, EVENT_CLICK_DATA_API, SELECTOR_DATA_TOGGLE, function (event) { 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() event.preventDefault()
Dropdown.getOrCreateInstance(this).toggle() Dropdown.getOrCreateInstance(this).toggle()
}) })

View 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 EventHandler from './dom/event-handler'
import SelectorEngine from './dom/selector-engine' import SelectorEngine from './dom/selector-engine'
import ScrollBarHelper from './util/scrollbar' import ScrollBarHelper from './util/scrollbar'
@ -184,8 +191,8 @@ class Modal extends BaseComponent {
_showElement(relatedTarget) { _showElement(relatedTarget) {
// try to append dynamic modal // try to append dynamic modal
if (!document.body.contains(this._element)) { if (!this._document.body.contains(this._element)) {
document.body.append(this._element) this._document.body.append(this._element)
} }
this._element.style.display = 'block' this._element.style.display = 'block'
@ -255,7 +262,7 @@ class Modal extends BaseComponent {
this._isTransitioning = false this._isTransitioning = false
this._backdrop.hide(() => { this._backdrop.hide(() => {
document.body.classList.remove(CLASS_NAME_OPEN) this._document.body.classList.remove(CLASS_NAME_OPEN)
this._resetAdjustments() this._resetAdjustments()
this._scrollBar.reset() this._scrollBar.reset()
EventHandler.trigger(this._element, EVENT_HIDDEN) EventHandler.trigger(this._element, EVENT_HIDDEN)
@ -272,7 +279,7 @@ class Modal extends BaseComponent {
return return
} }
const isModalOverflowing = this._element.scrollHeight > document.documentElement.clientHeight const isModalOverflowing = this._element.scrollHeight > this._document.documentElement.clientHeight
const initialOverflowY = this._element.style.overflowY const initialOverflowY = this._element.style.overflowY
// return if the following background transition hasn't yet completed // return if the following background transition hasn't yet completed
if (initialOverflowY === 'hidden' || this._element.classList.contains(CLASS_NAME_STATIC)) { if (initialOverflowY === 'hidden' || this._element.classList.contains(CLASS_NAME_STATIC)) {
@ -299,7 +306,7 @@ class Modal extends BaseComponent {
*/ */
_adjustDialog() { _adjustDialog() {
const isModalOverflowing = this._element.scrollHeight > document.documentElement.clientHeight const isModalOverflowing = this._element.scrollHeight > this._document.documentElement.clientHeight
const scrollbarWidth = this._scrollBar.getWidth() const scrollbarWidth = this._scrollBar.getWidth()
const isBodyOverflowing = scrollbarWidth > 0 const isBodyOverflowing = scrollbarWidth > 0
@ -341,7 +348,7 @@ class Modal extends BaseComponent {
* Data API implementation * 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) const target = getElementFromSelector(this)
if (['A', 'AREA'].includes(this.tagName)) { if (['A', 'AREA'].includes(this.tagName)) {

View File

@ -9,7 +9,9 @@ import {
defineJQueryPlugin, defineJQueryPlugin,
getElementFromSelector, getElementFromSelector,
isDisabled, isDisabled,
isVisible isVisible,
getDocument,
getWindow
} from './util/index' } from './util/index'
import ScrollBarHelper from './util/scrollbar' import ScrollBarHelper from './util/scrollbar'
import EventHandler from './dom/event-handler' import EventHandler from './dom/event-handler'
@ -209,7 +211,7 @@ class Offcanvas extends BaseComponent {
* Data API implementation * 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) const target = getElementFromSelector(this)
if (['A', 'AREA'].includes(this.tagName)) { if (['A', 'AREA'].includes(this.tagName)) {
@ -237,7 +239,7 @@ EventHandler.on(document, EVENT_CLICK_DATA_API, SELECTOR_DATA_TOGGLE, function (
data.toggle(this) 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)) { for (const selector of SelectorEngine.find(OPEN_SELECTOR)) {
Offcanvas.getOrCreateInstance(selector).show() Offcanvas.getOrCreateInstance(selector).show()
} }

View 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 EventHandler from './dom/event-handler'
import Manipulator from './dom/manipulator' import Manipulator from './dom/manipulator'
import SelectorEngine from './dom/selector-engine' import SelectorEngine from './dom/selector-engine'
@ -58,7 +63,7 @@ const DefaultType = {
class ScrollSpy extends BaseComponent { class ScrollSpy extends BaseComponent {
constructor(element, config) { constructor(element, config) {
super(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._offsets = []
this._targets = [] this._targets = []
this._activeTarget = null this._activeTarget = null
@ -137,14 +142,14 @@ class ScrollSpy extends BaseComponent {
_getScrollHeight() { _getScrollHeight() {
return this._scrollElement.scrollHeight || Math.max( return this._scrollElement.scrollHeight || Math.max(
document.body.scrollHeight, this._document.body.scrollHeight,
document.documentElement.scrollHeight this._document.documentElement.scrollHeight
) )
} }
_getOffsetHeight() { _getOffsetHeight() {
return this._scrollElement === window ? return this._scrollElement === window ?
window.innerHeight : this._window.innerHeight :
this._scrollElement.getBoundingClientRect().height this._scrollElement.getBoundingClientRect().height
} }
@ -251,7 +256,7 @@ class ScrollSpy extends BaseComponent {
* Data API implementation * 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)) { for (const spy of SelectorEngine.find(SELECTOR_DATA_SPY)) {
new ScrollSpy(spy) // eslint-disable-line no-new new ScrollSpy(spy) // eslint-disable-line no-new
} }

View 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 EventHandler from './dom/event-handler'
import SelectorEngine from './dom/selector-engine' import SelectorEngine from './dom/selector-engine'
import BaseComponent from './base-component' import BaseComponent from './base-component'
@ -177,7 +183,7 @@ class Tab extends BaseComponent {
* Data API implementation * 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)) { if (['A', 'AREA'].includes(this.tagName)) {
event.preventDefault() event.preventDefault()
} }

View File

@ -245,8 +245,8 @@ class Tooltip extends BaseComponent {
// empty mouseover listeners to the body's immediate children; // empty mouseover listeners to the body's immediate children;
// only needed because of broken event delegation on iOS // only needed because of broken event delegation on iOS
// https://www.quirksmode.org/blog/archives/2014/02/mouse_event_bub.html // https://www.quirksmode.org/blog/archives/2014/02/mouse_event_bub.html
if ('ontouchstart' in document.documentElement) { if ('ontouchstart' in this._document.documentElement) {
for (const element of [].concat(...document.body.children)) { for (const element of [].concat(...this._document.body.children)) {
EventHandler.on(element, 'mouseover', noop) EventHandler.on(element, 'mouseover', noop)
} }
} }
@ -280,8 +280,8 @@ class Tooltip extends BaseComponent {
// If this is a touch-enabled device we remove the extra // If this is a touch-enabled device we remove the extra
// empty mouseover listeners we added for iOS support // empty mouseover listeners we added for iOS support
if ('ontouchstart' in document.documentElement) { if ('ontouchstart' in this._document.documentElement) {
for (const element of [].concat(...document.body.children)) { for (const element of [].concat(...this._document.body.children)) {
EventHandler.off(element, 'mouseover', noop) EventHandler.off(element, 'mouseover', noop)
} }
} }

View File

@ -6,13 +6,13 @@
*/ */
import EventHandler from '../dom/event-handler' import EventHandler from '../dom/event-handler'
import { getElementFromSelector, isDisabled } from './index' import { getElementFromSelector, isDisabled, getDocument } from './index'
const enableDismissTrigger = (component, method = 'hide') => { const enableDismissTrigger = (component, method = 'hide') => {
const clickEvent = `click.dismiss${component.EVENT_KEY}` const clickEvent = `click.dismiss${component.EVENT_KEY}`
const name = component.NAME 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)) { if (['A', 'AREA'].includes(this.tagName)) {
event.preventDefault() event.preventDefault()
} }

View File

@ -7,6 +7,7 @@
import EventHandler from '../dom/event-handler' import EventHandler from '../dom/event-handler'
import SelectorEngine from '../dom/selector-engine' import SelectorEngine from '../dom/selector-engine'
import { getDocument } from './index'
import Config from './config' import Config from './config'
/** /**
@ -43,6 +44,7 @@ class FocusTrap extends Config {
this._config = this._getConfig(config) this._config = this._getConfig(config)
this._isActive = false this._isActive = false
this._lastTabNavDirection = null this._lastTabNavDirection = null
this._document = getDocument()
} }
// Getters // Getters
@ -68,9 +70,9 @@ class FocusTrap extends Config {
this._config.trapElement.focus() this._config.trapElement.focus()
} }
EventHandler.off(document, EVENT_KEY) // guard against infinite focus loop EventHandler.off(this._document, EVENT_KEY) // guard against infinite focus loop
EventHandler.on(document, EVENT_FOCUSIN, event => this._handleFocusin(event)) EventHandler.on(this._document, EVENT_FOCUSIN, event => this._handleFocusin(event))
EventHandler.on(document, EVENT_KEYDOWN_TAB, event => this._handleKeydown(event)) EventHandler.on(this._document, EVENT_KEYDOWN_TAB, event => this._handleKeydown(event))
this._isActive = true this._isActive = true
} }
@ -81,14 +83,14 @@ class FocusTrap extends Config {
} }
this._isActive = false this._isActive = false
EventHandler.off(document, EVENT_KEY) EventHandler.off(this._document, EVENT_KEY)
} }
// Private // Private
_handleFocusin(event) { _handleFocusin(event) {
const { trapElement } = this._config 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 return
} }

View File

@ -25,7 +25,7 @@ const toType = object => {
const getUID = prefix => { const getUID = prefix => {
do { do {
prefix += Math.floor(Math.random() * MAX_UID) prefix += Math.floor(Math.random() * MAX_UID)
} while (document.getElementById(prefix)) } while (getDocument().getElementById(prefix))
return prefix return prefix
} }
@ -59,7 +59,7 @@ const getSelectorFromElement = element => {
const selector = getSelector(element) const selector = getSelector(element)
if (selector) { if (selector) {
return document.querySelector(selector) ? selector : null return getDocument().querySelector(selector) ? selector : null
} }
return null return null
@ -68,7 +68,7 @@ const getSelectorFromElement = element => {
const getElementFromSelector = element => { const getElementFromSelector = element => {
const selector = getSelector(element) const selector = getSelector(element)
return selector ? document.querySelector(selector) : null return selector ? getDocument().querySelector(selector) : null
} }
const getTransitionDurationFromElement = element => { const getTransitionDurationFromElement = element => {
@ -77,7 +77,7 @@ const getTransitionDurationFromElement = element => {
} }
// Get transition-duration of the 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 floatTransitionDuration = Number.parseFloat(transitionDuration)
const floatTransitionDelay = Number.parseFloat(transitionDelay) const floatTransitionDelay = Number.parseFloat(transitionDelay)
@ -167,7 +167,7 @@ const isDisabled = element => {
} }
const findShadowRoot = element => { const findShadowRoot = element => {
if (!document.documentElement.attachShadow) { if (!getDocument().documentElement.attachShadow) {
return null 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 * @see https://www.charistheo.io/blog/2021/02/restart-a-css-animation-with-javascript/#restarting-a-css-animation
*/ */
const reflow = element => { const reflow = element => {
element.offsetHeight // eslint-disable-line no-unused-expressions // eslint-disable-next-line no-unused-expressions
element.offsetHeight
} }
const getjQuery = () => { 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 return window.jQuery
} }
@ -214,10 +215,11 @@ const getjQuery = () => {
const DOMContentLoadedCallbacks = [] const DOMContentLoadedCallbacks = []
const onDOMContentLoaded = callback => { 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 // add listener on the first call when the document is in loading state
if (!DOMContentLoadedCallbacks.length) { if (!DOMContentLoadedCallbacks.length) {
document.addEventListener('DOMContentLoaded', () => { documentRef.addEventListener('DOMContentLoaded', () => {
for (const callback of DOMContentLoadedCallbacks) { for (const callback of DOMContentLoadedCallbacks) {
callback() callback()
} }
@ -230,7 +232,7 @@ const onDOMContentLoaded = callback => {
} }
} }
const isRTL = () => document.documentElement.dir === 'rtl' const isRTL = () => getDocument().documentElement.dir === 'rtl'
const defineJQueryPlugin = plugin => { const defineJQueryPlugin = plugin => {
onDOMContentLoaded(() => { onDOMContentLoaded(() => {
@ -312,11 +314,26 @@ const getNextActiveElement = (list, activeElement, shouldGetNext, isCycleAllowed
return list[Math.max(0, Math.min(index, listLength - 1))] 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 { export {
defineJQueryPlugin, defineJQueryPlugin,
execute, execute,
executeAfterTransition, executeAfterTransition,
findShadowRoot, findShadowRoot,
getDocument,
getElement, getElement,
getElementFromSelector, getElementFromSelector,
getjQuery, getjQuery,
@ -324,6 +341,7 @@ export {
getSelectorFromElement, getSelectorFromElement,
getTransitionDurationFromElement, getTransitionDurationFromElement,
getUID, getUID,
getWindow,
isDisabled, isDisabled,
isElement, isElement,
isRTL, isRTL,

View File

@ -5,6 +5,8 @@
* -------------------------------------------------------------------------- * --------------------------------------------------------------------------
*/ */
import { getWindow } from './index'
const uriAttributes = new Set([ const uriAttributes = new Set([
'background', 'background',
'cite', 'cite',
@ -91,7 +93,8 @@ export function sanitizeHtml(unsafeHtml, allowList, sanitizeFunction) {
return sanitizeFunction(unsafeHtml) return sanitizeFunction(unsafeHtml)
} }
const domParser = new window.DOMParser() const windowRef = getWindow()
const domParser = new windowRef.DOMParser()
const createdDocument = domParser.parseFromString(unsafeHtml, 'text/html') const createdDocument = domParser.parseFromString(unsafeHtml, 'text/html')
const elements = [].concat(...createdDocument.body.querySelectorAll('*')) const elements = [].concat(...createdDocument.body.querySelectorAll('*'))

View File

@ -7,7 +7,7 @@
import SelectorEngine from '../dom/selector-engine' import SelectorEngine from '../dom/selector-engine'
import Manipulator from '../dom/manipulator' import Manipulator from '../dom/manipulator'
import { isElement } from './index' import { isElement, getDocument, getWindow } from './index'
/** /**
* Constants * Constants
@ -24,14 +24,15 @@ const PROPERTY_MARGIN = 'margin-right'
class ScrollBarHelper { class ScrollBarHelper {
constructor() { constructor() {
this._element = document.body this._element = getDocument().body
this._window = getWindow()
} }
// Public // Public
getWidth() { getWidth() {
// https://developer.mozilla.org/en-US/docs/Web/API/Window/innerWidth#usage_notes // https://developer.mozilla.org/en-US/docs/Web/API/Window/innerWidth#usage_notes
const documentWidth = document.documentElement.clientWidth const documentWidth = document.documentElement.clientWidth
return Math.abs(window.innerWidth - documentWidth) return Math.abs(this._window.innerWidth - documentWidth)
} }
hide() { hide() {
@ -64,12 +65,12 @@ class ScrollBarHelper {
_setElementAttributes(selector, styleProperty, callback) { _setElementAttributes(selector, styleProperty, callback) {
const scrollbarWidth = this.getWidth() const scrollbarWidth = this.getWidth()
const manipulationCallBack = element => { const manipulationCallBack = element => {
if (element !== this._element && window.innerWidth > element.clientWidth + scrollbarWidth) { if (element !== this._element && this._window.innerWidth > element.clientWidth + scrollbarWidth) {
return return
} }
this._saveInitialAttribute(element, styleProperty) 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`) element.style.setProperty(styleProperty, `${callback(Number.parseFloat(calculatedValue))}px`)
} }

View File

@ -7,7 +7,7 @@
import Config from './config' import Config from './config'
import EventHandler from '../dom/event-handler' import EventHandler from '../dom/event-handler'
import { execute } from './index' import { execute, getDocument, getWindow } from './index'
/** /**
* Constants * Constants
@ -52,7 +52,7 @@ class Swipe extends Config {
this._config = this._getConfig(config) this._config = this._getConfig(config)
this._deltaX = 0 this._deltaX = 0
this._supportPointerEvents = Boolean(window.PointerEvent) this._supportPointerEvents = Boolean(getWindow().PointerEvent)
this._initEvents() this._initEvents()
} }
@ -139,7 +139,7 @@ class Swipe extends Config {
// Static // Static
static isSupported() { static isSupported() {
return 'ontouchstart' in document.documentElement || navigator.maxTouchPoints > 0 return 'ontouchstart' in getDocument().documentElement || navigator.maxTouchPoints > 0
} }
} }

View File

@ -1,6 +1,7 @@
import Tooltip from '../../dist/tooltip' import Tooltip from '../../dist/tooltip'
import '../../dist/carousel' import '../../dist/carousel'
// eslint-disable-next-line ssr-friendly/no-dom-globals-in-module-scope
window.addEventListener('load', () => { window.addEventListener('load', () => {
[].concat(...document.querySelectorAll('[data-bs-toggle="tooltip"]')) [].concat(...document.querySelectorAll('[data-bs-toggle="tooltip"]'))
.map(tooltipNode => new Tooltip(tooltipNode)) .map(tooltipNode => new Tooltip(tooltipNode))

View File

@ -1,5 +1,6 @@
import { Tooltip } from '../../../dist/js/bootstrap.esm.js' import { Tooltip } from '../../../dist/js/bootstrap.esm.js'
// eslint-disable-next-line ssr-friendly/no-dom-globals-in-module-scope
window.addEventListener('load', () => { window.addEventListener('load', () => {
[].concat(...document.querySelectorAll('[data-bs-toggle="tooltip"]')) [].concat(...document.querySelectorAll('[data-bs-toggle="tooltip"]'))
.map(tooltipNode => new Tooltip(tooltipNode)) .map(tooltipNode => new Tooltip(tooltipNode))

66
package-lock.json generated
View File

@ -24,6 +24,7 @@
"eslint": "^8.8.0", "eslint": "^8.8.0",
"eslint-config-xo": "^0.39.0", "eslint-config-xo": "^0.39.0",
"eslint-plugin-import": "^2.25.4", "eslint-plugin-import": "^2.25.4",
"eslint-plugin-ssr-friendly": "^1.0.5",
"eslint-plugin-unicorn": "^40.1.0", "eslint-plugin-unicorn": "^40.1.0",
"find-unused-sass-variables": "^3.1.0", "find-unused-sass-variables": "^3.1.0",
"globby": "^11.0.4", "globby": "^11.0.4",
@ -4319,6 +4320,45 @@
"integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=",
"dev": true "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": { "node_modules/eslint-plugin-unicorn": {
"version": "40.1.0", "version": "40.1.0",
"resolved": "https://registry.npmjs.org/eslint-plugin-unicorn/-/eslint-plugin-unicorn-40.1.0.tgz", "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": { "eslint-plugin-unicorn": {
"version": "40.1.0", "version": "40.1.0",
"resolved": "https://registry.npmjs.org/eslint-plugin-unicorn/-/eslint-plugin-unicorn-40.1.0.tgz", "resolved": "https://registry.npmjs.org/eslint-plugin-unicorn/-/eslint-plugin-unicorn-40.1.0.tgz",

View File

@ -113,6 +113,7 @@
"eslint": "^8.8.0", "eslint": "^8.8.0",
"eslint-config-xo": "^0.39.0", "eslint-config-xo": "^0.39.0",
"eslint-plugin-import": "^2.25.4", "eslint-plugin-import": "^2.25.4",
"eslint-plugin-ssr-friendly": "^1.0.5",
"eslint-plugin-unicorn": "^40.1.0", "eslint-plugin-unicorn": "^40.1.0",
"find-unused-sass-variables": "^3.1.0", "find-unused-sass-variables": "^3.1.0",
"globby": "^11.0.4", "globby": "^11.0.4",