MediaWiki:Common.js: Difference between revisions

No edit summary
No edit summary
Line 240: Line 240:
         * position. */
         * position. */
         if ( isActive ) {
         if ( isActive ) {
          /* ── Auto-expand parent section in TOC ────────────────────────
          * Vector collapses child sections under a parent li. When a child
          * becomes active we must expand its parent by removing the
          * vector-toc-list-item-collapsed class, mirroring what Vector's
          * own JS does on click. Walk up to find collapsed ancestors. */
          var ancestor = li.parentNode;
          while ( ancestor && ancestor !== document.body ) {
            if ( ancestor.classList &&
                ancestor.classList.contains( 'vector-toc-list-item' ) &&
                ancestor.classList.contains( 'vector-toc-list-item-collapsed' ) ) {
              ancestor.classList.remove( 'vector-toc-list-item-collapsed' );
            }
            ancestor = ancestor.parentNode;
          }
          /* ── Scroll active item into view within the TOC ──────────────
          * Use getBoundingClientRect for both the item and the scroll
          * container so the measurement is always in viewport coords —
          * avoids the offsetTop-relative-to-offsetParent mismatch. */
           var container = document.querySelector( '.vector-sticky-pinned-container' );
           var container = document.querySelector( '.vector-sticky-pinned-container' );
           if ( container ) {
           if ( container ) {
            /* Check TOC is visible: the container must have nonzero height
            * and must not be hidden by the Vector "pinned/unpinned" toggle */
             var tocVisible = container.offsetHeight > 0 &&
             var tocVisible = container.offsetHeight > 0 &&
                             container.offsetParent !== null &&
                             container.offsetParent !== null &&
Line 251: Line 268:
               var liRect = li.getBoundingClientRect();
               var liRect = li.getBoundingClientRect();
               var cRect  = container.getBoundingClientRect();
               var cRect  = container.getBoundingClientRect();
               if ( liRect.top < cRect.top + 8 || liRect.bottom > cRect.bottom - 8 ) {
              /* Only scroll if the item is outside the visible container area */
                 /* Find the scrollable ancestor within the TOC container.
               if ( liRect.top < cRect.top + 4 || liRect.bottom > cRect.bottom - 4 ) {
                * Vector 2022 uses .vector-sticky-pinned-container as the
                 /* Walk up to find the actual scrollable ancestor */
                * scroll host in some versions, and .vector-toc in others. */
                 var scrollHost = null;
                 var scrollHost = null;
                 var candidate  = li.parentNode;
                 var node = li.parentNode;
                 while ( candidate && candidate !== document.body ) {
                 while ( node && node !== document.body ) {
                   if ( candidate.scrollHeight > candidate.clientHeight + 4 ) {
                  var overflow = window.getComputedStyle( node ).overflowY;
                     scrollHost = candidate;
                   if ( ( overflow === 'auto' || overflow === 'scroll' ) &&
                      node.scrollHeight > node.clientHeight ) {
                     scrollHost = node;
                     break;
                     break;
                   }
                   }
                   candidate = candidate.parentNode;
                   node = node.parentNode;
                }
                /* Fallback: use container itself if no scrollable ancestor found */
                if ( !scrollHost && container.scrollHeight > container.clientHeight ) {
                  scrollHost = container;
                 }
                 }
                 if ( scrollHost ) {
                 if ( scrollHost ) {
                   var targetTop = li.offsetTop - ( scrollHost.clientHeight / 2 );
                  /* Convert li position to scrollHost-relative using rects */
                   scrollHost.scrollTop = Math.max( 0, targetTop );
                  var hostRect = scrollHost.getBoundingClientRect();
                  var currentScroll = scrollHost.scrollTop;
                   var liTop = liRect.top - hostRect.top + currentScroll;
                  var target = liTop - ( scrollHost.clientHeight / 2 ) + ( li.offsetHeight / 2 );
                   scrollHost.scrollTop = Math.max( 0, target );
                 }
                 }
               }
               }
Line 329: Line 355:
     * Vector injects some of these elements via its own JS after DOMContentLoaded,
     * Vector injects some of these elements via its own JS after DOMContentLoaded,
     * so we use a MutationObserver to catch them whenever they appear. */
     * so we use a MutationObserver to catch them whenever they appear. */
     var HIDE_SELS = [
    /* Remove appearance panel elements by their stable IDs only.
       '#vector-appearance',
    * We deliberately avoid class-based selectors like
       '#vector-appearance-pinned-container',
    * .mw-portlet-vector-user-menu-overflow because on some Vector versions
       '#vector-appearance-unpinned-container',
    * that class name is shared with the user-menu portlet (which contains
      '.mw-portlet-appearance',
    * the login/logout/preferences links) and removing it hides the profile
      '.mw-portlet-vector-user-menu-overflow',
    * dropdown.  ID-based removal is safe and precise. */
      '[aria-controls="vector-appearance"]'
     var HIDE_IDS = [
       'vector-appearance',
       'vector-appearance-pinned-container',
       'vector-appearance-unpinned-container'
     ];
     ];
     function removeHiddenEls() {
     function removeHiddenEls() {
       HIDE_SELS.forEach( function ( sel ) {
       HIDE_IDS.forEach( function ( id ) {
         document.querySelectorAll( sel ).forEach( function ( el ) {
         var el = document.getElementById( id );
          if ( el.parentNode ) el.parentNode.removeChild( el );
        if ( el && el.parentNode ) el.parentNode.removeChild( el );
        } );
       } );
       } );
      /* Also remove the appearance toggle button by its aria-controls attribute.
      * Scope the search to page-tools area only — never touch the header user-links. */
      var pageTools = document.getElementById( 'vector-page-tools' ) ||
                      document.querySelector( '.vector-page-tools-pinned-container' );
      if ( pageTools ) {
        pageTools.querySelectorAll( '[aria-controls="vector-appearance"]' )
          .forEach( function ( el ) {
            if ( el.parentNode ) el.parentNode.removeChild( el );
          } );
      }
     }
     }
     removeHiddenEls();
     removeHiddenEls();
    /* ── Teeka view-mode detection ────────────────────────────────────────
    * Teeka pages carry <div class="gr-teeka-page" data-primary="X" data-slug="Y">
    *
    * Two modes:
    *  gr-standalone  — user opened the teeka URL directly, or ?ref=0
    *                    → plain reading view, no coloured containers
    *  gr-ref-mode    — user navigated here from the main doc, or ?ref=1
    *                    → coloured teeka-block containers (the default styled view)
    *
    * Detection order: ?ref= query param > document.referrer > default (standalone)
    * ─────────────────────────────────────────────────────────────────── */
    ( function detectTeekaMode() {
      var teekaPage = document.querySelector( '.gr-teeka-page' );
      if ( !teekaPage ) return;  // not a teeka page — nothing to do
      var primary = teekaPage.getAttribute( 'data-primary' ) || '';
      var artPath = ( window.mw && mw.config.get( 'wgArticlePath' ) ) || '/wiki/$1';
      var mainUrl = artPath.replace( '$1', primary );
      // 1. Query param override: ?ref=1 or ?ref=0
      var qs = window.location.search;
      var refParam = qs.match( /[?&]ref=([01])/ );
      if ( refParam ) {
        document.body.classList.add( refParam[1] === '1' ? 'gr-ref-mode' : 'gr-standalone' );
        return;
      }
      // 2. Referrer check — did we come from the main doc?
      var ref = document.referrer || '';
      var fromMain = ref && primary && ref.indexOf( mainUrl ) !== -1;
      document.body.classList.add( fromMain ? 'gr-ref-mode' : 'gr-standalone' );
    }() );
     if ( window.MutationObserver ) {
     if ( window.MutationObserver ) {
       var hideObserver = new MutationObserver( function ( mutations ) {
       var hideObserver = new MutationObserver( function ( mutations ) {
Line 359: Line 431:
       * liObserver that handles active highlight colouring. */
       * liObserver that handles active highlight colouring. */
       hideObserver.observe( document.body, { childList: true, subtree: false } );
       hideObserver.observe( document.body, { childList: true, subtree: false } );
      /* Also check the header area where Vector sometimes injects the button */
       /* Stop observing after 6s — Vector will have finished by then */
      var mwHeader = document.querySelector( '.mw-header' ) || document.querySelector( '#mw-head' );
       setTimeout( function () { hideObserver.disconnect(); }, 6000 );
      if ( mwHeader ) {
        hideObserver.observe( mwHeader, { childList: true, subtree: true } );
      }
       /* Stop observing after 8s — Vector will have finished by then */
       setTimeout( function () { hideObserver.disconnect(); }, 8000 );
     }
     }