Loading js/toc.js +54 −48 Original line number Diff line number Diff line Loading @@ -10,29 +10,25 @@ */ Drupal.behaviors.sector_toc = { attach: function (context, settings) { var isScrolling; // Add event handler to toc links. $('.toc-tree a').each(function() { $(this).on('click', function() { logger('clicked ' + $(this).attr('href')); // Set flag on .toc-tree to ignore updates while we scroll to target. $('.toc-tree').data('scroll-ignore', 1); // Get a new header id from the toc link. let new_heading_id = $(this).attr('href'); logger('clicked id=' + new_heading_id); updateHeadingInUrl(new_heading_id.replace('#', '')); }); }); function tocLocationHashChange() { var hash = location.hash; logger("tocLocationHashChange() hash=" + hash); // If hash matches a toc entry... if ($('.toc-tree a[href$="' + hash + '"]').length) { // If hash matches an element in node body field. if ($(hash).length) { setActive('.toc_tree', hash); } else { logger('tocLocationHashChange() no hash==toc match, want to ignore scroll?'); // Suppress URL hash changes while we scroll to target. $('.toc-tree').data('scroll-ignore', 1); logger('tocLocationHashChange() no hash==toc match'); // Find parent chunker section, then make h2 active. let h2 = $(hash).parents('div.chunker-section').find('h2'); logger('nearest h2=' + h2.html()); Loading @@ -44,33 +40,50 @@ // Set hyperlink and parent list item active in ToC. function setActive(toc_selector, hash) { // If hash matches a toc entry. if ($('.toc-tree a[href$="' + hash + '"]').length) { // In case h2 or h3 the hash is the same. var hash_toc = hash; } else { // If there is no a toc entry the closest h3 should be active in the ToC block. // The content should be scrolled to the element with the specified ID. let h3 = $(hash).parents('.embedded-entity').prevAll('h3').first(); // Use different hash for h3 (toc entry) and the element (scrolling in the content). var hash_toc = '#' + h3.attr('id'); } // Remove active from any non-matched link. $('.toc-tree a[href!="' + hash + '"]').removeClass('active'); $('.toc-tree a[href!="' + hash_toc + '"]').removeClass('active'); // Remove active class from any active list items. $('.toc-tree li.active').each(function () { $(this).removeClass('active'); }); // Add active class to matched anchor, and parent list item. $('.toc-tree a[href$="' + hash + '"]') logger('toc block, open link with hash=' + hash_toc); $('.toc-tree a[href$="' + hash_toc + '"]') .addClass('active') .parents('li') .each(function () { $(this).addClass('active') }); // Make the correct focus point in the HTML after changing the 'active' class in a header link. $("html, body").animate({scrollTop: ($(hash).offset().top - 20) + 'px'}, "fast"); } // @todo: what is correct Drupal way to bind to window events? window.addEventListener("hashchange", tocLocationHashChange, false); // On page load, do we have a URL fragment? // e.g. arriving from bookmark, or refresh existing page? if (location.hash) { logger('detected hash in url, triggering local hashchange event') logger('detected hash in url, triggering local hashchange event'); tocLocationHashChange(); } // When a hashchange event is fired open the correct h2/h3 header link in the ToC block. window.addEventListener("hashchange", tocLocationHashChange, false); // When the whole page has loaded create a new observer. window.addEventListener("load", createObserver, false); // Prepare variables and create the observer. function createObserver() { let observer; let headings = document.querySelectorAll('.node h2, .node h3'); let options = { Loading @@ -78,48 +91,41 @@ rootMargin: '0px', threshold: 1.0 } let observer = new IntersectionObserver(handleObserver, options); observer = new IntersectionObserver(handleObserver, options); headings.forEach(heading => { observer.observe(heading); }); } function handleObserver(entries, observer){ entries.forEach(entry => { let heading_id = entry.target.getAttribute('id'); // Get a new heading after scrolling. let new_heading_id = entry.target.getAttribute('id'); // Check isIntersecting to be sure the target currently intersects the root. // Check intersectionRatio to know how much of the target element is actually visible // within the root's intersection rectangle. if (entry.isIntersecting && entry.intersectionRatio === 1) { logger('intersecting heading_id=' + heading_id); if (heading_id !== null) { const location = window.location.toString().split('#')[0]; if (heading_id !== location.hash) { // Are we ignoring scroll (e.g. relocating to toc link click). if ($('.toc-tree').data('scroll-ignore') !== 1) { logger('observer, setting id=' + heading_id); history.replaceState(null, null, location + '#' + heading_id); window.dispatchEvent(new HashChangeEvent("hashchange")); } else { logger('scroll, scroll-ignore'); } } logger('intersecting heading_id=' + new_heading_id); if (new_heading_id !== null) { updateHeadingInUrl(new_heading_id); } } }); } // Detect completion of scrolling. document.addEventListener('scroll', (e) => { // console.log('sector_toc: scroll'); // Reset timeout while scrolling. window.clearTimeout(isScrolling); isScrolling = setTimeout(function(){ logger('I think scrolling has stopped...'); $('.toc-tree').data('scroll-ignore', 0); }, 600); // Perhaps better would be to bind intersection observer to element // with id being sought via URL so we know when we have arrived... }); function updateHeadingInUrl(new_heading_id) { const url = window.location.href.split('#')[0]; // Get the current heading from url. let old_heading_id = window.location.hash.replace('#', ''); // Update heading in url if a new one is visible on a page. if (new_heading_id !== old_heading_id) { // Update heading in url. history.replaceState(null, null, url + '#' + new_heading_id); // Fire the new hashchange event. window.dispatchEvent(new HashChangeEvent("hashchange")); } } function logger(message) { const prefix = 'sector_toc: '; Loading Loading
js/toc.js +54 −48 Original line number Diff line number Diff line Loading @@ -10,29 +10,25 @@ */ Drupal.behaviors.sector_toc = { attach: function (context, settings) { var isScrolling; // Add event handler to toc links. $('.toc-tree a').each(function() { $(this).on('click', function() { logger('clicked ' + $(this).attr('href')); // Set flag on .toc-tree to ignore updates while we scroll to target. $('.toc-tree').data('scroll-ignore', 1); // Get a new header id from the toc link. let new_heading_id = $(this).attr('href'); logger('clicked id=' + new_heading_id); updateHeadingInUrl(new_heading_id.replace('#', '')); }); }); function tocLocationHashChange() { var hash = location.hash; logger("tocLocationHashChange() hash=" + hash); // If hash matches a toc entry... if ($('.toc-tree a[href$="' + hash + '"]').length) { // If hash matches an element in node body field. if ($(hash).length) { setActive('.toc_tree', hash); } else { logger('tocLocationHashChange() no hash==toc match, want to ignore scroll?'); // Suppress URL hash changes while we scroll to target. $('.toc-tree').data('scroll-ignore', 1); logger('tocLocationHashChange() no hash==toc match'); // Find parent chunker section, then make h2 active. let h2 = $(hash).parents('div.chunker-section').find('h2'); logger('nearest h2=' + h2.html()); Loading @@ -44,33 +40,50 @@ // Set hyperlink and parent list item active in ToC. function setActive(toc_selector, hash) { // If hash matches a toc entry. if ($('.toc-tree a[href$="' + hash + '"]').length) { // In case h2 or h3 the hash is the same. var hash_toc = hash; } else { // If there is no a toc entry the closest h3 should be active in the ToC block. // The content should be scrolled to the element with the specified ID. let h3 = $(hash).parents('.embedded-entity').prevAll('h3').first(); // Use different hash for h3 (toc entry) and the element (scrolling in the content). var hash_toc = '#' + h3.attr('id'); } // Remove active from any non-matched link. $('.toc-tree a[href!="' + hash + '"]').removeClass('active'); $('.toc-tree a[href!="' + hash_toc + '"]').removeClass('active'); // Remove active class from any active list items. $('.toc-tree li.active').each(function () { $(this).removeClass('active'); }); // Add active class to matched anchor, and parent list item. $('.toc-tree a[href$="' + hash + '"]') logger('toc block, open link with hash=' + hash_toc); $('.toc-tree a[href$="' + hash_toc + '"]') .addClass('active') .parents('li') .each(function () { $(this).addClass('active') }); // Make the correct focus point in the HTML after changing the 'active' class in a header link. $("html, body").animate({scrollTop: ($(hash).offset().top - 20) + 'px'}, "fast"); } // @todo: what is correct Drupal way to bind to window events? window.addEventListener("hashchange", tocLocationHashChange, false); // On page load, do we have a URL fragment? // e.g. arriving from bookmark, or refresh existing page? if (location.hash) { logger('detected hash in url, triggering local hashchange event') logger('detected hash in url, triggering local hashchange event'); tocLocationHashChange(); } // When a hashchange event is fired open the correct h2/h3 header link in the ToC block. window.addEventListener("hashchange", tocLocationHashChange, false); // When the whole page has loaded create a new observer. window.addEventListener("load", createObserver, false); // Prepare variables and create the observer. function createObserver() { let observer; let headings = document.querySelectorAll('.node h2, .node h3'); let options = { Loading @@ -78,48 +91,41 @@ rootMargin: '0px', threshold: 1.0 } let observer = new IntersectionObserver(handleObserver, options); observer = new IntersectionObserver(handleObserver, options); headings.forEach(heading => { observer.observe(heading); }); } function handleObserver(entries, observer){ entries.forEach(entry => { let heading_id = entry.target.getAttribute('id'); // Get a new heading after scrolling. let new_heading_id = entry.target.getAttribute('id'); // Check isIntersecting to be sure the target currently intersects the root. // Check intersectionRatio to know how much of the target element is actually visible // within the root's intersection rectangle. if (entry.isIntersecting && entry.intersectionRatio === 1) { logger('intersecting heading_id=' + heading_id); if (heading_id !== null) { const location = window.location.toString().split('#')[0]; if (heading_id !== location.hash) { // Are we ignoring scroll (e.g. relocating to toc link click). if ($('.toc-tree').data('scroll-ignore') !== 1) { logger('observer, setting id=' + heading_id); history.replaceState(null, null, location + '#' + heading_id); window.dispatchEvent(new HashChangeEvent("hashchange")); } else { logger('scroll, scroll-ignore'); } } logger('intersecting heading_id=' + new_heading_id); if (new_heading_id !== null) { updateHeadingInUrl(new_heading_id); } } }); } // Detect completion of scrolling. document.addEventListener('scroll', (e) => { // console.log('sector_toc: scroll'); // Reset timeout while scrolling. window.clearTimeout(isScrolling); isScrolling = setTimeout(function(){ logger('I think scrolling has stopped...'); $('.toc-tree').data('scroll-ignore', 0); }, 600); // Perhaps better would be to bind intersection observer to element // with id being sought via URL so we know when we have arrived... }); function updateHeadingInUrl(new_heading_id) { const url = window.location.href.split('#')[0]; // Get the current heading from url. let old_heading_id = window.location.hash.replace('#', ''); // Update heading in url if a new one is visible on a page. if (new_heading_id !== old_heading_id) { // Update heading in url. history.replaceState(null, null, url + '#' + new_heading_id); // Fire the new hashchange event. window.dispatchEvent(new HashChangeEvent("hashchange")); } } function logger(message) { const prefix = 'sector_toc: '; Loading