From 3e217def6e6800171b053b136818891e229d4d1f Mon Sep 17 00:00:00 2001 From: alistair3149 Date: Fri, 2 Apr 2021 15:42:56 -0400 Subject: [PATCH 1/2] feat: rewrite ToC scrollspy Remove legacy fallbacks and rewrite scrollspy using IntersectionObserver API --- .eslintignore | 1 + .../skins.citizen.scripts.toc.js | 107 ++++-------------- 2 files changed, 22 insertions(+), 86 deletions(-) diff --git a/.eslintignore b/.eslintignore index 6c7e0224..053e8f44 100644 --- a/.eslintignore +++ b/.eslintignore @@ -9,3 +9,4 @@ resources/skins.citizen.scripts.search/typeahead-init.js resources/skins.citizen.scripts.search/underscore.partial.js resources/skins.citizen.scripts.search/wm-typeahead.js resources/skins.citizen.scripts.theme/inline.js +resources/skins.citizen.scripts.toc/skins.citizen.scripts.toc.js diff --git a/resources/skins.citizen.scripts.toc/skins.citizen.scripts.toc.js b/resources/skins.citizen.scripts.toc/skins.citizen.scripts.toc.js index 652b4e18..cfc19826 100644 --- a/resources/skins.citizen.scripts.toc/skins.citizen.scripts.toc.js +++ b/resources/skins.citizen.scripts.toc/skins.citizen.scripts.toc.js @@ -1,77 +1,28 @@ /* * Citizen - ToC JS * https://starcitizen.tools - * - * Smooth scroll fallback and Scrollspy */ -/** - * Implement smooth scroll when an item in table of content is clicked. - * - * @constructor - */ -function SmoothScroll() { - var navLinks, eventListener, link; - if ( !( 'scrollBehavior' in document.documentElement.style ) ) { - navLinks = document.querySelectorAll( '#toc a' ); - eventListener = function clickHandler( e ) { - e.preventDefault(); - e.target.scrollIntoView( { - behavior: 'smooth' - } ); - }; - - for ( link in navLinks ) { - if ( Object.prototype.hasOwnProperty.call( navLinks, link ) ) { - navLinks[ link ].addEventListener( 'click', eventListener ); - } - } - } -} - -/** - * Throttle scroll event - * - * @param {Function} fn - function - * @param {number} wait - wait time in ms - * @return {Function} - */ -function throttle( fn, wait ) { - var time = Date.now(); - return function () { - if ( ( time + wait - Date.now() ) < 0 ) { - fn(); - time = Date.now(); - } - }; -} - /** * Add active HTML class to items in table of content based on user viewport. * * @constructor */ -function ScrollSpy() { +function intersectionHandler() { var sections = document.querySelectorAll( '.mw-headline' ), - mwbody = document.querySelector( '.mw-body' ), - mwbodyStyle = window.getComputedStyle( mwbody ), - scrollOffset = parseInt( mwbodyStyle.marginTop, 10 ) + 1; + htmlStyles = window.getComputedStyle( document.documentElement ), + scrollPaddingTop = htmlStyles.getPropertyValue( 'scroll-padding-top' ), + // Align with scroll offset set in CSS + marginTop = "-" + scrollPaddingTop, + id, id2, toclink, node, active; - window.addEventListener( 'scroll', throttle( function () { - var scrollPos = document.documentElement.scrollTop || document.body.scrollTop, - section, id, id2, toclink, node, active; - - scrollPos += scrollOffset; - - for ( section in sections ) { - if ( - Object.prototype.hasOwnProperty.call( sections, section ) && - sections[ section ].offsetTop <= scrollPos - ) { - id = sections[ section ].id; + for (let i = 0; i < sections.length; i++) { + const observer = new IntersectionObserver( ( entry ) => { + if ( entry[ 0 ].isIntersecting ) { + id = sections[i].id; // Try the ID of the span before - if ( sections[ section ].previousSibling !== null ) { - id2 = sections[ section ].previousSibling.id || ''; + if ( sections[i].previousSibling !== null ) { + id2 = sections[i].previousSibling.id || ''; } toclink = document.querySelector( "a[href='#" + id + "']" ) || document.querySelector( "a[href='#" + id2 + "']" ); node = toclink.parentNode; @@ -83,35 +34,19 @@ function ScrollSpy() { node.classList.add( 'active' ); } } - } - }, 10 ), { passive: true } ); -} - -/** - * Run SmoothScroll() and ScrollSpy() when table of content is present. - * - * @constructor - */ -function CheckToC() { - if ( document.getElementById( 'toc' ) ) { - SmoothScroll(); - ScrollSpy(); + }, { + // Will break in viewport with short height + // But calculating bottom margin on the fly is too costly + rootMargin: marginTop + " 0px -85% 0px" + } ); + observer.observe( sections[i] ); } } function main() { - if ( document.readyState !== 'loading' ) { - CheckToC(); - } else if ( document.addEventListener ) { - // All modern browsers to register DOMContentLoaded - document.addEventListener( 'DOMContentLoaded', CheckToC ); - } else { - // Old IE browsers - document.attachEvent( 'onreadystatechange', function () { - if ( document.readyState === 'complete' ) { - CheckToC(); - } - } ); + // Check for has-toc class since it is loaded way before #toc is present + if ( document.body.classList.contains( 'skin-citizen-has-toc' ) ) { + intersectionHandler(); } } From e9b5ec5575da859b21f05c1507c5998dec4e5882 Mon Sep 17 00:00:00 2001 From: alistair3149 Date: Fri, 2 Apr 2021 15:46:52 -0400 Subject: [PATCH 2/2] feat: only use smooth scroll in larger viewport --- resources/skins.citizen.styles/common/common.less | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/resources/skins.citizen.styles/common/common.less b/resources/skins.citizen.styles/common/common.less index ee3ecdb2..cc378a38 100644 --- a/resources/skins.citizen.styles/common/common.less +++ b/resources/skins.citizen.styles/common/common.less @@ -253,11 +253,6 @@ video { max-width: 100%; // Prevent overflow } -html.citizen-animations-ready { - // So that the delay scroll animation won't happen on load - scroll-behavior: smooth; // not supported by IE, Edge, Safari, and Opera, use JQuery as fallback -} - .skin-citizen-dark { .mw-editsection > a:before { filter: invert( 1 ); @@ -272,4 +267,11 @@ html.citizen-animations-ready { table { display: table; } + + // Delay scroll animation won't happen on load + // Use it only for larger viewport as it is very costly + // on mobile devices + html.citizen-animations-ready { + scroll-behavior: smooth; + } }