]> git.ipfire.org Git - thirdparty/bootstrap.git/commitdiff
Properly escape IDs in getSelector() to handle weird IDs (#35565) (#35566)
authorPierre Souchay <pierresouchay@users.noreply.github.com>
Mon, 7 Nov 2022 12:43:06 +0000 (13:43 +0100)
committerGitHub <noreply@github.com>
Mon, 7 Nov 2022 12:43:06 +0000 (14:43 +0200)
.bundlewatch.config.json
js/src/dom/selector-engine.js
js/src/util/index.js
js/tests/unit/collapse.spec.js
js/tests/unit/tab.spec.js

index 105c8c15809d05419e7760269e4d4b5239e974e6..e61a2acd4ee3ac23a5f2ff8053f08a64a914a661 100644 (file)
@@ -38,7 +38,7 @@
     },
     {
       "path": "./dist/js/bootstrap.bundle.min.js",
-      "maxSize": "22.75 kB"
+      "maxSize": "23.0 kB"
     },
     {
       "path": "./dist/js/bootstrap.esm.js",
index ad10a6083172c4cf5c893f25312a6add432589f0..248dab49443074bbba690f4e4dc1250529a3e781 100644 (file)
@@ -5,7 +5,7 @@
  * --------------------------------------------------------------------------
  */
 
-import { isDisabled, isVisible } from '../util/index.js'
+import { isDisabled, isVisible, parseSelector } from '../util/index.js'
 
 /**
  * Constants
@@ -99,6 +99,7 @@ const SelectorEngine = {
       }
 
       selector = hrefAttribute && hrefAttribute !== '#' ? hrefAttribute.trim() : null
+      selector = parseSelector(selector)
     }
 
     return selector
index b92eddba2535eb9534527b8c2cf3ea9974307515..8c692217364d7b312d3165fad44e814ff27d562e 100644 (file)
@@ -9,6 +9,20 @@ const MAX_UID = 1_000_000
 const MILLISECONDS_MULTIPLIER = 1000
 const TRANSITION_END = 'transitionend'
 
+/**
+ * Properly escape IDs selectors to handle weird IDs
+ * @param {string} selector
+ * @returns {string}
+ */
+const parseSelector = selector => {
+  if (selector && window.CSS && window.CSS.escape) {
+    // document.querySelector needs escaping to handle IDs (html5+) containing for instance /
+    selector = selector.replaceAll(/#([^\s"#']+)/g, (match, id) => '#' + CSS.escape(id))
+  }
+
+  return selector
+}
+
 // Shout-out Angus Croll (https://goo.gl/pxwQGp)
 const toType = object => {
   if (object === null || object === undefined) {
@@ -76,7 +90,7 @@ const getElement = object => {
   }
 
   if (typeof object === 'string' && object.length > 0) {
-    return document.querySelector(object)
+    return document.querySelector(parseSelector(object))
   }
 
   return null
@@ -285,6 +299,7 @@ export {
   isVisible,
   noop,
   onDOMContentLoaded,
+  parseSelector,
   reflow,
   triggerTransitionEnd,
   toType
index 9c867198814a059ef5bcc0e7c70589370a6bdb3c..834d1b98e4a46374ad5ff4f5231f338ccee06294 100644 (file)
@@ -887,17 +887,17 @@ describe('Collapse', () => {
       return new Promise(resolve => {
         fixtureEl.innerHTML = [
           '<a id="trigger1" role="button" data-bs-toggle="collapse" href="#test1"></a>',
-          '<a id="trigger2" role="button" data-bs-toggle="collapse" href="#test2"></a>',
+          '<a id="trigger2" role="button" data-bs-toggle="collapse" href="#0/my/id"></a>',
           '<a id="trigger3" role="button" data-bs-toggle="collapse" href=".multi"></a>',
           '<div id="test1" class="multi"></div>',
-          '<div id="test2" class="multi"></div>'
+          '<div id="0/my/id" class="multi"></div>'
         ].join('')
 
         const trigger1 = fixtureEl.querySelector('#trigger1')
         const trigger2 = fixtureEl.querySelector('#trigger2')
         const trigger3 = fixtureEl.querySelector('#trigger3')
         const target1 = fixtureEl.querySelector('#test1')
-        const target2 = fixtureEl.querySelector('#test2')
+        const target2 = fixtureEl.querySelector('#' + CSS.escape('0/my/id'))
 
         const target2Shown = () => {
           expect(trigger1).not.toHaveClass('collapsed')
index 896e611801454da0ab4dda5a7f0de93bc2f32429..66238efbb8b152b4843103a78d81f44d93955dae 100644 (file)
@@ -177,6 +177,43 @@ describe('Tab', () => {
       })
     })
 
+    it('should work with tab id being an int', done => {
+      fixtureEl.innerHTML = [
+        '<div class="card-header d-block d-inline-block">',
+        '  <ul class="nav nav-tabs card-header-tabs" id="page_tabs">',
+        '    <li class="nav-item">',
+        '      <a class="nav-link" draggable="false" data-toggle="tab" href="#tab1">',
+        '        Working Tab 1 (#tab1)',
+        '     </a>',
+        '    </li>',
+        '    <li class="nav-item">',
+        '      <a id="trigger2" class="nav-link" draggable="false" data-toggle="tab" href="#2">',
+        '        Tab with numeric ID should work (#2)',
+        '      </a>',
+        '    </li>',
+        '  </ul>',
+        '</div>',
+        '<div class="card-body">',
+        '  <div class="tab-content" id="page_content">',
+        '     <div class="tab-pane fade" id="tab1">',
+        '      Working Tab 1 (#tab1) Content Here',
+        '  </div>',
+        '  <div class="tab-pane fade" id="2">',
+        '      Working Tab 2 (#2) with numeric ID',
+        '  </div>',
+        '</div>'
+      ].join('')
+      const profileTriggerEl = fixtureEl.querySelector('#trigger2')
+      const tab = new Tab(profileTriggerEl)
+
+      profileTriggerEl.addEventListener('shown.bs.tab', () => {
+        expect(fixtureEl.querySelector('#' + CSS.escape('2'))).toHaveClass('active')
+        done()
+      })
+
+      tab.show()
+    })
+
     it('should not fire shown when show is prevented', () => {
       return new Promise((resolve, reject) => {
         fixtureEl.innerHTML = '<div class="nav"><div class="nav-link"></div></div>'