diff options
author | Pierre Souchay <pierresouchay@users.noreply.github.com> | 2022-11-07 15:43:06 +0300 |
---|---|---|
committer | GitHub <noreply@github.com> | 2022-11-07 15:43:06 +0300 |
commit | ef4e2daa48193463b36fdc297d79c6a002e4ee67 (patch) | |
tree | db68bf3bc32bfdfce97f1c3824c29c86e24b8f27 | |
parent | e81e7cda90026cdb2a05fcdadd2d66f48f0bbdc4 (diff) |
Properly escape IDs in getSelector() to handle weird IDs (#35565) (#35566)
-rw-r--r-- | .bundlewatch.config.json | 2 | ||||
-rw-r--r-- | js/src/dom/selector-engine.js | 3 | ||||
-rw-r--r-- | js/src/util/index.js | 17 | ||||
-rw-r--r-- | js/tests/unit/collapse.spec.js | 6 | ||||
-rw-r--r-- | js/tests/unit/tab.spec.js | 37 |
5 files changed, 59 insertions, 6 deletions
diff --git a/.bundlewatch.config.json b/.bundlewatch.config.json index 105c8c1580..e61a2acd4e 100644 --- a/.bundlewatch.config.json +++ b/.bundlewatch.config.json @@ -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", diff --git a/js/src/dom/selector-engine.js b/js/src/dom/selector-engine.js index ad10a60831..248dab4944 100644 --- a/js/src/dom/selector-engine.js +++ b/js/src/dom/selector-engine.js @@ -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 diff --git a/js/src/util/index.js b/js/src/util/index.js index b92eddba25..8c69221736 100644 --- a/js/src/util/index.js +++ b/js/src/util/index.js @@ -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 diff --git a/js/tests/unit/collapse.spec.js b/js/tests/unit/collapse.spec.js index 9c86719881..834d1b98e4 100644 --- a/js/tests/unit/collapse.spec.js +++ b/js/tests/unit/collapse.spec.js @@ -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') diff --git a/js/tests/unit/tab.spec.js b/js/tests/unit/tab.spec.js index 896e611801..66238efbb8 100644 --- a/js/tests/unit/tab.spec.js +++ b/js/tests/unit/tab.spec.js @@ -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>' |