Compare commits

...

4 Commits

Author SHA1 Message Date
Denis Lopatin
eb8bc74c40
Merge e0d1dad9ca0a3c20f5f6d473aee1c8b0a40ef2b1 into 380a1d738b221fecc964260add053997399be4d4 2025-09-27 16:38:36 +01:00
Denis Lopatin
e0d1dad9ca
returns a comment 2025-09-07 14:11:17 +03:00
Denis Lopatin
588cf7e5fc
Apply suggestion from @XhmikosR
Co-authored-by: XhmikosR <xhmikosr@gmail.com>
2025-09-07 14:07:10 +03:00
Denis
f84bf47e1a normalizes the selector operation in scrollspy 2025-09-05 23:47:04 +03:00
3 changed files with 34 additions and 4 deletions

View File

@ -9,7 +9,7 @@ import BaseComponent from './base-component.js'
import EventHandler from './dom/event-handler.js' import EventHandler from './dom/event-handler.js'
import SelectorEngine from './dom/selector-engine.js' import SelectorEngine from './dom/selector-engine.js'
import { import {
defineJQueryPlugin, getElement, isDisabled, isVisible defineJQueryPlugin, getElement, isDisabled, isVisible, parseSelector
} from './util/index.js' } from './util/index.js'
/** /**
@ -210,11 +210,13 @@ class ScrollSpy extends BaseComponent {
continue continue
} }
const observableSection = SelectorEngine.findOne(decodeURI(anchor.hash), this._element) const withDecodeUri = decodeURI(anchor.hash)
const withEscape = parseSelector(withDecodeUri)
const observableSection = SelectorEngine.findOne(withEscape, this._element)
// ensure that the observableSection exists & is visible // ensure that the observableSection exists & is visible
if (isVisible(observableSection)) { if (isVisible(observableSection)) {
this._targetLinks.set(decodeURI(anchor.hash), anchor) this._targetLinks.set(withDecodeUri, anchor)
this._observableSections.set(anchor.hash, observableSection) this._observableSections.set(anchor.hash, observableSection)
} }
} }

View File

@ -17,7 +17,7 @@ const TRANSITION_END = 'transitionend'
const parseSelector = selector => { const parseSelector = selector => {
if (selector && window.CSS && window.CSS.escape) { if (selector && window.CSS && window.CSS.escape) {
// document.querySelector needs escaping to handle IDs (html5+) containing for instance / // document.querySelector needs escaping to handle IDs (html5+) containing for instance /
selector = selector.replace(/#([^\s"#']+)/g, (match, id) => `#${CSS.escape(id)}`) selector = selector.replace(/#([^\s"']+)/g, (match, id) => `#${CSS.escape(id)}`)
} }
return selector return selector

View File

@ -194,6 +194,34 @@ describe('ScrollSpy', () => {
expect(scrollSpy._targetLinks.size).toBe(1) expect(scrollSpy._targetLinks.size).toBe(1)
}) })
it('should take account of escaped IDs', () => {
fixtureEl.innerHTML = [
'<nav id="navigation" class="navbar">',
' <ul class="navbar-nav">',
' <li class="nav-item"><a class="nav-link active" id="one-link" href="#div-2.1">One</a></li>',
' <li class="nav-item"><a class="nav-link" id="two-link" href="#!@#$_^&*()">Two</a></li>',
' <li class="nav-item"><a class="nav-link" id="three-link" href="#id.div.Element@data-custom=true">Three</a></li>',
' <li class="nav-item"><a class="nav-link" id="four-link" href="#https://domain.to/#%2F%40user%3Aname.test">Four</a></li>',
' <li class="nav-item"><a class="nav-link" id="five-link" href="#https://domain.to/#/@user:name.test">Five</a></li>',
' </ul>',
'</nav>',
'<div id="content" style="height: 200px; overflow-y: auto;">',
' <div id="div-2.1" style="height: 300px;">test</div>',
' <div id="!@#$_^&*()" style="height: 300px;">test</div>',
' <div id="id.div.Element@data-custom=true" style="height: 300px;">test</div>',
' <div id="https://domain.to/#%2F%40user%3Aname.test" style="height: 300px;">test</div>',
' <div id="https://domain.to/#/@user:name.test">test</div>',
'</div>'
].join('')
const scrollSpy = new ScrollSpy(fixtureEl.querySelector('#content'), {
target: '#navigation'
})
expect(scrollSpy._observableSections.size).toBe(5)
expect(scrollSpy._targetLinks.size).toBe(5)
})
it('should not process element without target', () => { it('should not process element without target', () => {
fixtureEl.innerHTML = [ fixtureEl.innerHTML = [
'<nav id="navigation" class="navbar">', '<nav id="navigation" class="navbar">',