Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Properly escape IDs in getSelector() to handle weird IDs #35566

Merged
2 changes: 1 addition & 1 deletion .bundlewatch.config.json
Expand Up @@ -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",
Expand Down
3 changes: 2 additions & 1 deletion js/src/dom/selector-engine.js
Expand Up @@ -5,7 +5,7 @@
* --------------------------------------------------------------------------
*/

import { isDisabled, isVisible } from '../util/index.js'
import { isDisabled, isVisible, parseSelector } from '../util/index.js'

/**
* Constants
Expand Down Expand Up @@ -99,6 +99,7 @@ const SelectorEngine = {
}

selector = hrefAttribute && hrefAttribute !== '#' ? hrefAttribute.trim() : null
selector = parseSelector(selector)
}

return selector
Expand Down
17 changes: 16 additions & 1 deletion js/src/util/index.js
Expand Up @@ -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) {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -285,6 +299,7 @@ export {
isVisible,
noop,
onDOMContentLoaded,
parseSelector,
reflow,
triggerTransitionEnd,
toType
Expand Down
6 changes: 3 additions & 3 deletions js/tests/unit/collapse.spec.js
Expand Up @@ -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')
Expand Down
37 changes: 37 additions & 0 deletions js/tests/unit/tab.spec.js
Expand Up @@ -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>'
Expand Down