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

Fix Scrollspy to work with all valid HTML5 element ids #34412

Closed
wants to merge 6 commits into from

Conversation

chrisyeh96
Copy link

Fixes #34381 by using document.getElementById() instead of document.querySelector() when testing the validity of an element ID.

Whereas document.querySelector() only allows valid CSS selectors and requires ID-based selectors to start with a letter, document.getElementById() handles all valid HTML5 element IDs.

Add function to specifically retrieve target element ID, instead of a generic CSS selector. Especially useful for Scrollspy, nav-tabs, list-groups, offcanvas, modals, etc. where the target is guaranteed to be only a single element (as opposed to multiple elements sharing the same CSS class).
@chrisyeh96 chrisyeh96 requested a review from a team as a code owner July 4, 2021 00:30
@@ -8,7 +8,7 @@
import {
defineJQueryPlugin,
getElement,
getSelectorFromElement,
getTargetIdFromElement,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you @chrisyeh96 for you contribution 😃

Instead of introducing another function to just make the job, you could probably make a minor refactor to

getElementFromSelector to check if string starts with # and use the getElementById.

After this you can use it inside getSelectorFromElement as they use the exactly same code 😉

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unfortunately, I don't think that your suggestion will work with edge cases. For example #someID > div is a valid CSS Selector that starts with a # but cannot be passed directly to document.getElementById().

I wrote the new function getTargetIdFromElement() specifically to be used in cases such as Scrollspy where the Bootstrap documentation explicitly says that the href or data-bs-target attributes of the element have the format #someValidHTML5Id.

The more general getElementFromSelector() should still be allowed for generic CSS selectors such as the example above (#someID > div).

Copy link
Member

@GeoSot GeoSot Jul 6, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unfortunately, I don't think that your suggestion will work with edge cases. For example #someID > div is a valid CSS Selector that starts with a # but cannot be passed directly to document.getElementById().

Maybe this could do the job. Right?

reg = new RegExp(/^#[\w\-]+$/gm)
reg.test(givenId)

Copy link
Author

@chrisyeh96 chrisyeh96 Jul 6, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That doesn't work either. As I mentioned in my original issue #34381, any string of Unicode characters without whitespace is considered a valid HTML5 ID. In particular, numbers, emoji, and non-Latin characters are all valid. These are not matched by your suggested regex.

My pitch for a separate getTargetIdFromElement() method

In my mind, there are 2 distinct cases that Bootstrap should support:

Case 1: Arbitrary CSS Selectors

The Bootstrap docs for components such as Collapse and Offcanvas specifically mention supporting arbitrary CSS selectors. These should use the existing getSelectorFromElement() / getElementFromSelector() methods, which call document.querySelector() behind the scenes. By definition of CSS selectors (see official spec below), these components would NOT support IDs that start with digits.

Case 2: ID-specific Selectors

The Bootstrap docs for components such as Scrollspy and Carousel explicitly mention requiring matching an HTML5 ID (see official spec below). These should use the new getTargetIdFromElement() method which only matches HTML5 IDs instead of arbitrary CSS selectors. Thus, these components would be able to support IDs that start with digits.

Official Specs for HTML5 IDs and CSS Selectors

HTML5 ID: The official spec states

When specified on HTML elements, the id attribute value must be unique amongst all the IDs in the element's tree and must contain at least one character. The value must not contain any ASCII whitespace. [...] There are no other restrictions on what form an ID can take; in particular, IDs can consist of just digits, start with a digit, start with an underscore, consist of just punctuation, etc.

The method document.getElementById() supports all valid HTML5 IDs.

CSS Selectors: The official spec states

An ID selector consists of a “number sign” (U+0023, #) immediately followed by the ID value, which must be a CSS identifier

where a CSS identifier is defined (official spec) as follows:

In CSS, identifiers (including element names, classes, and IDs in selectors) can contain only the characters [a-zA-Z0-9] and ISO 10646 characters U+00A0 and higher, plus the hyphen (-) and the underscore (_); they cannot start with a digit, two hyphens, or a hyphen followed by a digit.

The method document.querySelector() only supports valid CSS selectors. For example, document.querySelector("#3rd") will raise an exception.

// a valid HTML5 id is any string without whitespace
const match = selector.match(/#(\S+)/)
const id = match ? match[1] : null
return document.getElementById(id) ? id : null
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can't this be tweaked a bit further? Like, there's no need to even call getElementById if id is null.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

True, but getElementById() already performs a check against null anyway.

As-is
getElementById() always performs a single check on whether id is null.

Your proposed tweak
If id is null, then we perform a single check on whether it's null. This is probably slightly faster than the function call to getElementById().
If id is not null, then we still perform a check ourselves, but then getElementById() also performs a check. In other words, there are two branching statements.

In other words, your proposal may speed up the case when id is null, but it's slower when id is not null. I'm happy with either option. Feel free to change it as you see fit.

@chrisyeh96
Copy link
Author

@XhmikosR @GeoSot Any update on this? What more needs to be done to get this merged?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Scrollspy does not handle all valid HTML5 element ids
3 participants