fix(core): 🐛 escape double quotes for toc selector

This commit is contained in:
alistair3149 2024-05-25 06:57:02 -04:00
parent 45ee5f897c
commit ae6d207fb4
No known key found for this signature in database
3 changed files with 54 additions and 47 deletions

View file

@ -1,3 +1,3 @@
{ {
".": "2.14.1" ".": "2.14.1"
} }

View file

@ -1,13 +1,13 @@
{ {
"$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json", "$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json",
"release-type": "php", "release-type": "php",
"pull-request-title-pattern": "build: release ${version}", "pull-request-title-pattern": "build: release ${version}",
"extra-files": [ "extra-files": [
"skin.json", "skin.json",
"package.json", "package.json",
"package-lock.json" "package-lock.json"
], ],
"packages": { "packages": {
".": {} ".": {}
} }
} }

View file

@ -2,6 +2,30 @@ const ACTIVE_SECTION_CLASS = 'citizen-toc__listItem--active';
let /** @type {HTMLElement | undefined} */ activeSection; let /** @type {HTMLElement | undefined} */ activeSection;
/**
* Escapes double quotes in the given HTML attribute ID.
*
* @param {string} id - The HTML attribute ID to escape double quotes from.
* @return {string} The escaped HTML attribute ID with double quotes replaced.
*/
function escapeHtmlAttributeQuotes( id ) {
// Escapes double quotes in the given id
return id.replace( /"/g, '\\"' );
}
/**
* Finds a link element in the table of contents (TOC) based on the provided ID.
*
* @param {Element} toc - The table of contents element to search within.
* @param {string} id - The ID of the section to find the link for.
* @return {Element|null} The link element corresponding to the provided ID, or null if not found.
*/
function findLinkById( toc, id ) {
const sanitizedId = escapeHtmlAttributeQuotes( id );
const linkElement = toc.querySelector( `a[href="#${ sanitizedId }"]` );
return linkElement;
}
/** /**
* Changes the active section in the table of contents based on the provided ID. * Changes the active section in the table of contents based on the provided ID.
* *
@ -10,12 +34,7 @@ let /** @type {HTMLElement | undefined} */ activeSection;
* @return {void} * @return {void}
*/ */
function changeActiveSection( toc, id ) { function changeActiveSection( toc, id ) {
const getLink = ( hash ) => { const link = findLinkById( toc, id );
const el = toc.querySelector( `a[href="#${ hash }"], a[href="#${ encodeURIComponent( hash ) }"]` );
return el;
};
const link = getLink( id );
if ( activeSection ) { if ( activeSection ) {
activeSection.classList.remove( ACTIVE_SECTION_CLASS ); activeSection.classList.remove( ACTIVE_SECTION_CLASS );
@ -41,37 +60,26 @@ function init( bodyContent ) {
return; return;
} }
const getHeadlineElements = () => { const extractIds = () => {
const headlineElements = []; return Array.from( toc.querySelectorAll( '.citizen-toc__listItem' ) )
Array.from( toc.querySelectorAll( '.citizen-toc__listItem' ) ).forEach( ( tocListEl ) => { .map( ( tocListEl ) => tocListEl.id.slice( 4 ) );
// Remove 'toc-' prefix from ID
const headlineElement = bodyContent.querySelector( '#' + CSS.escape( tocListEl.id.slice( 4 ) ) );
if ( headlineElement ) {
headlineElements.push( headlineElement );
}
} );
return headlineElements;
}; };
// We use scroll-padding-top to handle scrolling with fixed header const queryElements = ( ids ) => {
// It is better to respect that so it is consistent return ids.map( ( id ) => bodyContent.querySelector( '#' + CSS.escape( id ) ) )
.filter( ( element ) => element !== null && element !== undefined );
};
const headlines = queryElements( extractIds() );
const computedStyle = window.getComputedStyle( document.documentElement );
const scrollPaddingTop = computedStyle.getPropertyValue( 'scroll-padding-top' );
const topMargin = Number( scrollPaddingTop.slice( 0, -2 ) ) + 20;
const getTopMargin = () => { const getTopMargin = () => {
const computedStyle = window.getComputedStyle( document.documentElement ); return topMargin;
return Number(
computedStyle.getPropertyValue( 'scroll-padding-top' )
.slice( 0, -2 )
) + 20;
}; };
const headlines = getHeadlineElements();
// Do not continue if there are no headlines
// TODO: Need to revamp the selector so that it works better with MW 1.40,
// currently MW 1.40 has ToC on non-content pages as well
if ( !headlines ) {
return;
}
const initSectionObserver = require( './sectionObserver.js' ).init; const initSectionObserver = require( './sectionObserver.js' ).init;
const sectionObserver = initSectionObserver( { const sectionObserver = initSectionObserver( {
@ -84,7 +92,6 @@ function init( bodyContent ) {
} }
} ); } );
// TODO: Pause section observer on ToC link click
sectionObserver.resume(); sectionObserver.resume();
} }