Commit c0e8faf2 authored by Tatiana's avatar Tatiana Committed by Kirill Roskolii
Browse files

Issue #3281000 by tatianag: Scroll to element with ID if there is no ToC entry

parent 1f47467f
Loading
Loading
Loading
Loading
+54 −48
Original line number Diff line number Diff line
@@ -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());
@@ -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 = {
@@ -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: ';