]> git.ipfire.org Git - thirdparty/bootstrap.git/commitdiff
Move `getElementFromSelector` & `getSelectorFromElement` to SelectorEngine (#36027)
authorGeoSot <geo.sotis@gmail.com>
Sun, 6 Nov 2022 18:31:43 +0000 (20:31 +0200)
committerGitHub <noreply@github.com>
Sun, 6 Nov 2022 18:31:43 +0000 (20:31 +0200)
* Move `getElementFromSelector` & getSelectorFromElement` inside selector-engine.js, in order to use SelectorEngine methods, avoiding raw querySelector usage

* add `getMultipleElementsFromSelector` helper

Co-authored-by: Julien Déramond <juderamond@gmail.com>
js/src/carousel.js
js/src/collapse.js
js/src/dom/selector-engine.js
js/src/modal.js
js/src/offcanvas.js
js/src/tab.js
js/src/util/component-functions.js
js/src/util/index.js
js/tests/unit/dom/selector-engine.spec.js
js/tests/unit/util/index.spec.js

index e25395628544f7d406b716a472fd5f57316758da..8ba9f351afb0948d9d13e064e3487ed5b2a28705 100644 (file)
@@ -7,7 +7,6 @@
 
 import {
   defineJQueryPlugin,
-  getElementFromSelector,
   getNextActiveElement,
   isRTL,
   isVisible,
@@ -431,7 +430,7 @@ class Carousel extends BaseComponent {
  */
 
 EventHandler.on(document, EVENT_CLICK_DATA_API, SELECTOR_DATA_SLIDE, function (event) {
-  const target = getElementFromSelector(this)
+  const target = SelectorEngine.getElementFromSelector(this)
 
   if (!target || !target.classList.contains(CLASS_NAME_CAROUSEL)) {
     return
index 04a5f4cdf72d26d8e109ba89fe8c0d39ec6bc9ef..9371740116276005ef4ace7049bf6216598bf801 100644 (file)
@@ -8,8 +8,6 @@
 import {
   defineJQueryPlugin,
   getElement,
-  getElementFromSelector,
-  getSelectorFromElement,
   reflow
 } from './util/index.js'
 import EventHandler from './dom/event-handler.js'
@@ -68,7 +66,7 @@ class Collapse extends BaseComponent {
     const toggleList = SelectorEngine.find(SELECTOR_DATA_TOGGLE)
 
     for (const elem of toggleList) {
-      const selector = getSelectorFromElement(elem)
+      const selector = SelectorEngine.getSelectorFromElement(elem)
       const filterElement = SelectorEngine.find(selector)
         .filter(foundElement => foundElement === this._element)
 
@@ -185,7 +183,7 @@ class Collapse extends BaseComponent {
     this._element.classList.remove(CLASS_NAME_COLLAPSE, CLASS_NAME_SHOW)
 
     for (const trigger of this._triggerArray) {
-      const element = getElementFromSelector(trigger)
+      const element = SelectorEngine.getElementFromSelector(trigger)
 
       if (element && !this._isShown(element)) {
         this._addAriaAndCollapsedClass([trigger], false)
@@ -229,7 +227,7 @@ class Collapse extends BaseComponent {
     const children = this._getFirstLevelChildren(SELECTOR_DATA_TOGGLE)
 
     for (const element of children) {
-      const selected = getElementFromSelector(element)
+      const selected = SelectorEngine.getElementFromSelector(element)
 
       if (selected) {
         this._addAriaAndCollapsedClass([element], this._isShown(selected))
@@ -285,10 +283,7 @@ EventHandler.on(document, EVENT_CLICK_DATA_API, SELECTOR_DATA_TOGGLE, function (
     event.preventDefault()
   }
 
-  const selector = getSelectorFromElement(this)
-  const selectorElements = SelectorEngine.find(selector)
-
-  for (const element of selectorElements) {
+  for (const element of SelectorEngine.getMultipleElementsFromSelector(this)) {
     Collapse.getOrCreateInstance(element, { toggle: false }).toggle()
   }
 })
index 63bc2d1764cc1cc83a2976ffd4ff1054d3a64e96..ad10a6083172c4cf5c893f25312a6add432589f0 100644 (file)
@@ -77,6 +77,53 @@ const SelectorEngine = {
     ].map(selector => `${selector}:not([tabindex^="-"])`).join(',')
 
     return this.find(focusables, element).filter(el => !isDisabled(el) && isVisible(el))
+  },
+
+  getSelector(element) {
+    let selector = element.getAttribute('data-bs-target')
+
+    if (!selector || selector === '#') {
+      let hrefAttribute = element.getAttribute('href')
+
+      // The only valid content that could double as a selector are IDs or classes,
+      // so everything starting with `#` or `.`. If a "real" URL is used as the selector,
+      // `document.querySelector` will rightfully complain it is invalid.
+      // See https://github.com/twbs/bootstrap/issues/32273
+      if (!hrefAttribute || (!hrefAttribute.includes('#') && !hrefAttribute.startsWith('.'))) {
+        return null
+      }
+
+      // Just in case some CMS puts out a full URL with the anchor appended
+      if (hrefAttribute.includes('#') && !hrefAttribute.startsWith('#')) {
+        hrefAttribute = `#${hrefAttribute.split('#')[1]}`
+      }
+
+      selector = hrefAttribute && hrefAttribute !== '#' ? hrefAttribute.trim() : null
+    }
+
+    return selector
+  },
+
+  getSelectorFromElement(element) {
+    const selector = SelectorEngine.getSelector(element)
+
+    if (selector) {
+      return SelectorEngine.findOne(selector) ? selector : null
+    }
+
+    return null
+  },
+
+  getElementFromSelector(element) {
+    const selector = SelectorEngine.getSelector(element)
+
+    return selector ? SelectorEngine.findOne(selector) : null
+  },
+
+  getMultipleElementsFromSelector(element) {
+    const selector = SelectorEngine.getSelector(element)
+
+    return selector ? SelectorEngine.find(selector) : []
   }
 }
 
index 11efab20ad1fddb484493b0c868e57e8d83f7035..aba3b34a6007599bb00f7570f6b270b95b85d469 100644 (file)
@@ -5,7 +5,7 @@
  * --------------------------------------------------------------------------
  */
 
-import { defineJQueryPlugin, getElementFromSelector, isRTL, isVisible, reflow } from './util/index.js'
+import { defineJQueryPlugin, isRTL, isVisible, reflow } from './util/index.js'
 import EventHandler from './dom/event-handler.js'
 import SelectorEngine from './dom/selector-engine.js'
 import ScrollBarHelper from './util/scrollbar.js'
@@ -336,7 +336,7 @@ class Modal extends BaseComponent {
  */
 
 EventHandler.on(document, EVENT_CLICK_DATA_API, SELECTOR_DATA_TOGGLE, function (event) {
-  const target = getElementFromSelector(this)
+  const target = SelectorEngine.getElementFromSelector(this)
 
   if (['A', 'AREA'].includes(this.tagName)) {
     event.preventDefault()
index a857c4d7e421f83adee22549b8f89abaf25f5cb8..6fd7099207c4660f11ea2e2189e538badc689d1c 100644 (file)
@@ -7,7 +7,6 @@
 
 import {
   defineJQueryPlugin,
-  getElementFromSelector,
   isDisabled,
   isVisible
 } from './util/index.js'
@@ -231,7 +230,7 @@ class Offcanvas extends BaseComponent {
  */
 
 EventHandler.on(document, EVENT_CLICK_DATA_API, SELECTOR_DATA_TOGGLE, function (event) {
-  const target = getElementFromSelector(this)
+  const target = SelectorEngine.getElementFromSelector(this)
 
   if (['A', 'AREA'].includes(this.tagName)) {
     event.preventDefault()
index 19d3bfc361a9d9d3108ff68cc40578ae97322f59..d9e71b5ba4be7984900c6524222ecc7e8562ec57 100644 (file)
@@ -5,7 +5,7 @@
  * --------------------------------------------------------------------------
  */
 
-import { defineJQueryPlugin, getElementFromSelector, getNextActiveElement, isDisabled } from './util/index.js'
+import { defineJQueryPlugin, getNextActiveElement, isDisabled } from './util/index.js'
 import EventHandler from './dom/event-handler.js'
 import SelectorEngine from './dom/selector-engine.js'
 import BaseComponent from './base-component.js'
@@ -106,7 +106,7 @@ class Tab extends BaseComponent {
 
     element.classList.add(CLASS_NAME_ACTIVE)
 
-    this._activate(getElementFromSelector(element)) // Search and activate/show the proper section
+    this._activate(SelectorEngine.getElementFromSelector(element)) // Search and activate/show the proper section
 
     const complete = () => {
       if (element.getAttribute('role') !== 'tab') {
@@ -133,7 +133,7 @@ class Tab extends BaseComponent {
     element.classList.remove(CLASS_NAME_ACTIVE)
     element.blur()
 
-    this._deactivate(getElementFromSelector(element)) // Search and deactivate the shown section too
+    this._deactivate(SelectorEngine.getElementFromSelector(element)) // Search and deactivate the shown section too
 
     const complete = () => {
       if (element.getAttribute('role') !== 'tab') {
@@ -203,7 +203,7 @@ class Tab extends BaseComponent {
   }
 
   _setInitialAttributesOnTargetPanel(child) {
-    const target = getElementFromSelector(child)
+    const target = SelectorEngine.getElementFromSelector(child)
 
     if (!target) {
       return
index 2298ac3717e9c65dd364de1a5b7c6a18f336e95f..6896faf56998f275226ab3b00400c7b17487cb28 100644 (file)
@@ -6,7 +6,8 @@
  */
 
 import EventHandler from '../dom/event-handler.js'
-import { getElementFromSelector, isDisabled } from './index.js'
+import { isDisabled } from './index.js'
+import SelectorEngine from '../dom/selector-engine.js'
 
 const enableDismissTrigger = (component, method = 'hide') => {
   const clickEvent = `click.dismiss${component.EVENT_KEY}`
@@ -21,7 +22,7 @@ const enableDismissTrigger = (component, method = 'hide') => {
       return
     }
 
-    const target = getElementFromSelector(this) || this.closest(`.${name}`)
+    const target = SelectorEngine.getElementFromSelector(this) || this.closest(`.${name}`)
     const instance = component.getOrCreateInstance(target)
 
     // Method argument is left, for Alert and only, as it doesn't implement the 'hide' method
index ad99f85ed96896c794af7878cc73fcd96a5418ae..b92eddba2535eb9534527b8c2cf3ea9974307515 100644 (file)
@@ -30,47 +30,6 @@ const getUID = prefix => {
   return prefix
 }
 
-const getSelector = element => {
-  let selector = element.getAttribute('data-bs-target')
-
-  if (!selector || selector === '#') {
-    let hrefAttribute = element.getAttribute('href')
-
-    // The only valid content that could double as a selector are IDs or classes,
-    // so everything starting with `#` or `.`. If a "real" URL is used as the selector,
-    // `document.querySelector` will rightfully complain it is invalid.
-    // See https://github.com/twbs/bootstrap/issues/32273
-    if (!hrefAttribute || (!hrefAttribute.includes('#') && !hrefAttribute.startsWith('.'))) {
-      return null
-    }
-
-    // Just in case some CMS puts out a full URL with the anchor appended
-    if (hrefAttribute.includes('#') && !hrefAttribute.startsWith('#')) {
-      hrefAttribute = `#${hrefAttribute.split('#')[1]}`
-    }
-
-    selector = hrefAttribute && hrefAttribute !== '#' ? hrefAttribute.trim() : null
-  }
-
-  return selector
-}
-
-const getSelectorFromElement = element => {
-  const selector = getSelector(element)
-
-  if (selector) {
-    return document.querySelector(selector) ? selector : null
-  }
-
-  return null
-}
-
-const getElementFromSelector = element => {
-  const selector = getSelector(element)
-
-  return selector ? document.querySelector(selector) : null
-}
-
 const getTransitionDurationFromElement = element => {
   if (!element) {
     return 0
@@ -316,10 +275,8 @@ export {
   executeAfterTransition,
   findShadowRoot,
   getElement,
-  getElementFromSelector,
   getjQuery,
   getNextActiveElement,
-  getSelectorFromElement,
   getTransitionDurationFromElement,
   getUID,
   isDisabled,
index 0245896c68052e71f8c0053b9a2cc0868d315b94..905e25baec653302f963560be8427b45129c0c15 100644 (file)
@@ -1,5 +1,5 @@
 import SelectorEngine from '../../../src/dom/selector-engine'
-import { getFixture, clearFixture } from '../../helpers/fixture'
+import { clearFixture, getFixture } from '../../helpers/fixture'
 
 describe('SelectorEngine', () => {
   let fixtureEl
@@ -232,5 +232,159 @@ describe('SelectorEngine', () => {
       expect(SelectorEngine.focusableChildren(fixtureEl)).toEqual(expectedElements)
     })
   })
-})
 
+  describe('getSelectorFromElement', () => {
+    it('should get selector from data-bs-target', () => {
+      fixtureEl.innerHTML = [
+        '<div id="test" data-bs-target=".target"></div>',
+        '<div class="target"></div>'
+      ].join('')
+
+      const testEl = fixtureEl.querySelector('#test')
+
+      expect(SelectorEngine.getSelectorFromElement(testEl)).toEqual('.target')
+    })
+
+    it('should get selector from href if no data-bs-target set', () => {
+      fixtureEl.innerHTML = [
+        '<a id="test" href=".target"></a>',
+        '<div class="target"></div>'
+      ].join('')
+
+      const testEl = fixtureEl.querySelector('#test')
+
+      expect(SelectorEngine.getSelectorFromElement(testEl)).toEqual('.target')
+    })
+
+    it('should get selector from href if data-bs-target equal to #', () => {
+      fixtureEl.innerHTML = [
+        '<a id="test" data-bs-target="#" href=".target"></a>',
+        '<div class="target"></div>'
+      ].join('')
+
+      const testEl = fixtureEl.querySelector('#test')
+
+      expect(SelectorEngine.getSelectorFromElement(testEl)).toEqual('.target')
+    })
+
+    it('should return null if a selector from a href is a url without an anchor', () => {
+      fixtureEl.innerHTML = [
+        '<a id="test" data-bs-target="#" href="foo/bar.html"></a>',
+        '<div class="target"></div>'
+      ].join('')
+
+      const testEl = fixtureEl.querySelector('#test')
+
+      expect(SelectorEngine.getSelectorFromElement(testEl)).toBeNull()
+    })
+
+    it('should return the anchor if a selector from a href is a url', () => {
+      fixtureEl.innerHTML = [
+        '<a id="test" data-bs-target="#" href="foo/bar.html#target"></a>',
+        '<div id="target"></div>'
+      ].join('')
+
+      const testEl = fixtureEl.querySelector('#test')
+
+      expect(SelectorEngine.getSelectorFromElement(testEl)).toEqual('#target')
+    })
+
+    it('should return null if selector not found', () => {
+      fixtureEl.innerHTML = '<a id="test" href=".target"></a>'
+
+      const testEl = fixtureEl.querySelector('#test')
+
+      expect(SelectorEngine.getSelectorFromElement(testEl)).toBeNull()
+    })
+
+    it('should return null if no selector', () => {
+      fixtureEl.innerHTML = '<div></div>'
+
+      const testEl = fixtureEl.querySelector('div')
+
+      expect(SelectorEngine.getSelectorFromElement(testEl)).toBeNull()
+    })
+  })
+
+  describe('getElementFromSelector', () => {
+    it('should get element from data-bs-target', () => {
+      fixtureEl.innerHTML = [
+        '<div id="test" data-bs-target=".target"></div>',
+        '<div class="target"></div>'
+      ].join('')
+
+      const testEl = fixtureEl.querySelector('#test')
+
+      expect(SelectorEngine.getElementFromSelector(testEl)).toEqual(fixtureEl.querySelector('.target'))
+    })
+
+    it('should get element from href if no data-bs-target set', () => {
+      fixtureEl.innerHTML = [
+        '<a id="test" href=".target"></a>',
+        '<div class="target"></div>'
+      ].join('')
+
+      const testEl = fixtureEl.querySelector('#test')
+
+      expect(SelectorEngine.getElementFromSelector(testEl)).toEqual(fixtureEl.querySelector('.target'))
+    })
+
+    it('should return null if element not found', () => {
+      fixtureEl.innerHTML = '<a id="test" href=".target"></a>'
+
+      const testEl = fixtureEl.querySelector('#test')
+
+      expect(SelectorEngine.getElementFromSelector(testEl)).toBeNull()
+    })
+
+    it('should return null if no selector', () => {
+      fixtureEl.innerHTML = '<div></div>'
+
+      const testEl = fixtureEl.querySelector('div')
+
+      expect(SelectorEngine.getElementFromSelector(testEl)).toBeNull()
+    })
+  })
+
+  describe('getMultipleElementsFromSelector', () => {
+    it('should get elements from data-bs-target', () => {
+      fixtureEl.innerHTML = [
+        '<div id="test" data-bs-target=".target"></div>',
+        '<div class="target"></div>',
+        '<div class="target"></div>'
+      ].join('')
+
+      const testEl = fixtureEl.querySelector('#test')
+
+      expect(SelectorEngine.getMultipleElementsFromSelector(testEl)).toEqual(Array.from(fixtureEl.querySelectorAll('.target')))
+    })
+
+    it('should get elements in array, from href if no data-bs-target set', () => {
+      fixtureEl.innerHTML = [
+        '<a id="test" href=".target"></a>',
+        '<div class="target"></div>',
+        '<div class="target"></div>'
+      ].join('')
+
+      const testEl = fixtureEl.querySelector('#test')
+
+      expect(SelectorEngine.getMultipleElementsFromSelector(testEl)).toEqual(Array.from(fixtureEl.querySelectorAll('.target')))
+    })
+
+    it('should return empty array if elements not found', () => {
+      fixtureEl.innerHTML = '<a id="test" href=".target"></a>'
+
+      const testEl = fixtureEl.querySelector('#test')
+
+      expect(SelectorEngine.getMultipleElementsFromSelector(testEl)).toHaveSize(0)
+    })
+
+    it('should return empty array if no selector', () => {
+      fixtureEl.innerHTML = '<div></div>'
+
+      const testEl = fixtureEl.querySelector('div')
+
+      expect(SelectorEngine.getMultipleElementsFromSelector(testEl)).toHaveSize(0)
+    })
+  })
+})
index 6edc49433572d0d66b3b2b5799c7fb63fc14b98c..202c72061dc5afd68245902ad906638f15ce8e43 100644 (file)
@@ -22,119 +22,6 @@ describe('Util', () => {
     })
   })
 
-  describe('getSelectorFromElement', () => {
-    it('should get selector from data-bs-target', () => {
-      fixtureEl.innerHTML = [
-        '<div id="test" data-bs-target=".target"></div>',
-        '<div class="target"></div>'
-      ].join('')
-
-      const testEl = fixtureEl.querySelector('#test')
-
-      expect(Util.getSelectorFromElement(testEl)).toEqual('.target')
-    })
-
-    it('should get selector from href if no data-bs-target set', () => {
-      fixtureEl.innerHTML = [
-        '<a id="test" href=".target"></a>',
-        '<div class="target"></div>'
-      ].join('')
-
-      const testEl = fixtureEl.querySelector('#test')
-
-      expect(Util.getSelectorFromElement(testEl)).toEqual('.target')
-    })
-
-    it('should get selector from href if data-bs-target equal to #', () => {
-      fixtureEl.innerHTML = [
-        '<a id="test" data-bs-target="#" href=".target"></a>',
-        '<div class="target"></div>'
-      ].join('')
-
-      const testEl = fixtureEl.querySelector('#test')
-
-      expect(Util.getSelectorFromElement(testEl)).toEqual('.target')
-    })
-
-    it('should return null if a selector from a href is a url without an anchor', () => {
-      fixtureEl.innerHTML = [
-        '<a id="test" data-bs-target="#" href="foo/bar.html"></a>',
-        '<div class="target"></div>'
-      ].join('')
-
-      const testEl = fixtureEl.querySelector('#test')
-
-      expect(Util.getSelectorFromElement(testEl)).toBeNull()
-    })
-
-    it('should return the anchor if a selector from a href is a url', () => {
-      fixtureEl.innerHTML = [
-        '<a id="test" data-bs-target="#" href="foo/bar.html#target"></a>',
-        '<div id="target"></div>'
-      ].join('')
-
-      const testEl = fixtureEl.querySelector('#test')
-
-      expect(Util.getSelectorFromElement(testEl)).toEqual('#target')
-    })
-
-    it('should return null if selector not found', () => {
-      fixtureEl.innerHTML = '<a id="test" href=".target"></a>'
-
-      const testEl = fixtureEl.querySelector('#test')
-
-      expect(Util.getSelectorFromElement(testEl)).toBeNull()
-    })
-
-    it('should return null if no selector', () => {
-      fixtureEl.innerHTML = '<div></div>'
-
-      const testEl = fixtureEl.querySelector('div')
-
-      expect(Util.getSelectorFromElement(testEl)).toBeNull()
-    })
-  })
-
-  describe('getElementFromSelector', () => {
-    it('should get element from data-bs-target', () => {
-      fixtureEl.innerHTML = [
-        '<div id="test" data-bs-target=".target"></div>',
-        '<div class="target"></div>'
-      ].join('')
-
-      const testEl = fixtureEl.querySelector('#test')
-
-      expect(Util.getElementFromSelector(testEl)).toEqual(fixtureEl.querySelector('.target'))
-    })
-
-    it('should get element from href if no data-bs-target set', () => {
-      fixtureEl.innerHTML = [
-        '<a id="test" href=".target"></a>',
-        '<div class="target"></div>'
-      ].join('')
-
-      const testEl = fixtureEl.querySelector('#test')
-
-      expect(Util.getElementFromSelector(testEl)).toEqual(fixtureEl.querySelector('.target'))
-    })
-
-    it('should return null if element not found', () => {
-      fixtureEl.innerHTML = '<a id="test" href=".target"></a>'
-
-      const testEl = fixtureEl.querySelector('#test')
-
-      expect(Util.getElementFromSelector(testEl)).toBeNull()
-    })
-
-    it('should return null if no selector', () => {
-      fixtureEl.innerHTML = '<div></div>'
-
-      const testEl = fixtureEl.querySelector('div')
-
-      expect(Util.getElementFromSelector(testEl)).toBeNull()
-    })
-  })
-
   describe('getTransitionDurationFromElement', () => {
     it('should get transition from element', () => {
       fixtureEl.innerHTML = '<div style="transition: all 300ms ease-out;"></div>'