MediaWiki:Common.js: Difference between revisions

No edit summary
No edit summary
Line 922: Line 922:
     wireUllekhaLinks();
     wireUllekhaLinks();
   }
   }
}() );/* ── Search result highlight ──────────────────────────────────── */
}() );
/* ── Search result highlight ──────────────────────────────────── */
/* Replace the ENTIRE search highlight IIFE in MediaWiki:Common.js
* (from the line "( function () {" just before "function storeQueryForLink"
*  to its closing "}() );" )
* with this block.
*
* FIXES vs previous version:
* 1. Path comparison now also stores wgPageName (title) as fallback,
*    avoiding any pathname encoding mismatch.
* 2. sessionStorage key is NOT removed until after highlight succeeds
*    (or max retries exhausted) — prevents race with wikipage.content hook.
* 3. Retry logic waits for .mw-parser-output to actually exist in DOM.
* 4. Mobile bar is full-width terracotta (matches annotation bar style).
*/
( function () {
( function () {


Line 931: Line 945:
       sessionStorage.setItem( 'gr_search_hl', JSON.stringify({
       sessionStorage.setItem( 'gr_search_hl', JSON.stringify({
         query:    query,
         query:    query,
         pathname: a.pathname
         pathname: a.pathname,
        /* Also store the decoded title as extra fallback */
        title:    decodeURIComponent( a.pathname.replace( /^\//, '' ) ).replace( /_/g, ' ' )
       }) );
       }) );
     } catch(e) {}
     } catch(e) {}
Line 943: Line 959:
     if ( !stored || !stored.query ) return;
     if ( !stored || !stored.query ) return;


     var currentPath = window.location.pathname;
    /* ── Path / title check ────────────────────────────────────────
     var storedPath = stored.pathname || '';
    * Match if:
    *  (a) pathnames match after normalisation, OR
    *  (b) stored title matches current wgPageName (most reliable)
    * ──────────────────────────────────────────────────────────── */
     var currentPath = window.location.pathname;
     var storedPath   = stored.pathname || '';
 
     function normPath(p) {
     function normPath(p) {
       return decodeURIComponent(p).replace(/\/+$/, '').replace(/_/g,' ');
       return decodeURIComponent(p).replace(/\/+$/, '').replace(/_/g, ' ').toLowerCase();
     }
     }
     var storedNorm = normPath(storedPath);
 
    var currentNorm = normPath(currentPath);
     var pathMatch = storedPath && normPath(storedPath) === normPath(currentPath);
     /* Allow match when current path ends with the stored page slug
 
    * (handles MW short URLs vs /index.php?title= discrepancies) */
     /* wgPageName fallback — most reliable on all URL schemes */
     if ( storedPath && storedNorm !== currentNorm &&
     var currentTitle = ( window.mw && mw.config && mw.config.get('wgPageName') || '' )
        !currentNorm.endsWith(storedNorm.split('/').pop()) ) {
                        .replace(/_/g, ' ');
    var storedTitle  = stored.title || normPath(storedPath).replace(/^\//, '');
    var titleMatch  = storedTitle && currentTitle &&
                      currentTitle.toLowerCase() === storedTitle.toLowerCase();
 
    if ( storedPath && !pathMatch && !titleMatch ) {
      /* Not the right page — clear and bail */
       try { sessionStorage.removeItem( 'gr_search_hl' ); } catch(e) {}
       try { sessionStorage.removeItem( 'gr_search_hl' ); } catch(e) {}
       return;
       return;
Line 961: Line 989:
     if ( !query ) return;
     if ( !query ) return;


     try { sessionStorage.removeItem( 'gr_search_hl' ); } catch(e) {}
     /* ── Retry until .mw-parser-output is in the DOM ──────────────
    * Don't clear sessionStorage until highlight actually ran —
    * the wikipage.content hook may fire before DOMContentLoaded
    * on some MW setups.
    * ──────────────────────────────────────────────────────────── */
    var attempts = 0;
    var MAX_ATTEMPTS = 8;
 
    function tryHighlight() {
      attempts++;
      var content = document.querySelector( '#mw-content-text .mw-parser-output' );
      if ( !content && attempts < MAX_ATTEMPTS ) {
        setTimeout( tryHighlight, attempts * 200 );
        return;
      }
 
      /* Clear key now — we're committed to running */
      try { sessionStorage.removeItem( 'gr_search_hl' ); } catch(e) {}


    var delays = [0, 200, 600];
       if ( !content ) return;
    delays.forEach( function(ms) {
      highlightText( query );
       setTimeout( function () {
    }
        if ( !document.querySelector( '.gr-search-hl' ) ) {
 
          highlightText( query );
    /* First attempt immediately, then back off */
        }
     tryHighlight();
      }, ms );
     } );
   }
   }


Line 980: Line 1,023:
     if ( !raw ) return;
     if ( !raw ) return;


     var patterns = [];
    /* Build patterns: full phrase first, then individual words */
    patterns.push( escapeRegex( raw ) );
     var patterns = [ escapeRegex( raw ) ];
     raw.split( /\s+/ ).forEach( function(w) {
     raw.split( /\s+/ ).forEach( function(w) {
       if ( w.length >= 2 ) patterns.push( escapeRegex( w ) );
       if ( w.length >= 2 ) patterns.push( escapeRegex( w ) );
Line 991: Line 1,034:
       try { re = new RegExp( '(' + patterns[pi] + ')', 'gi' ); }
       try { re = new RegExp( '(' + patterns[pi] + ')', 'gi' ); }
       catch(e) { continue; }
       catch(e) { continue; }
       var count = wrapMatches( content, re );
       var count = wrapMatches( content, re );
       if ( count > 0 ) { matched = true; break; }
       if ( count > 0 ) { matched = true; break; }
Line 1,000: Line 1,042:
     var first = document.querySelector( '.gr-search-hl' );
     var first = document.querySelector( '.gr-search-hl' );
     if ( first ) {
     if ( first ) {
       first.scrollIntoView({ behavior: 'smooth', block: 'center' });
       /* Small delay so page layout is stable before scrolling */
      first.classList.add( 'gr-search-hl-pulse' );
       setTimeout( function() {
       setTimeout( function() {
         first.classList.remove( 'gr-search-hl-pulse' );
         first.scrollIntoView({ behavior: 'smooth', block: 'center' });
       }, 2000 );
        first.classList.add( 'gr-search-hl-pulse' );
        setTimeout( function() {
          first.classList.remove( 'gr-search-hl-pulse' );
        }, 2000 );
       }, 150 );
     }
     }


Line 1,024: Line 1,069:
           if ( tag === 'SCRIPT' || tag === 'STYLE' || tag === 'NOSCRIPT' ) return NodeFilter.FILTER_REJECT;
           if ( tag === 'SCRIPT' || tag === 'STYLE' || tag === 'NOSCRIPT' ) return NodeFilter.FILTER_REJECT;
           if ( p.classList.contains( 'gr-search-hl' ) ) return NodeFilter.FILTER_REJECT;
           if ( p.classList.contains( 'gr-search-hl' ) ) return NodeFilter.FILTER_REJECT;
          /* Skip nodes inside the static bar / UI chrome */
          if ( p.closest && p.closest( '#gr-static-bar, .gr-controls, #gra-fab, #gra-mobile-bar' ) ) return NodeFilter.FILTER_REJECT;
           return NodeFilter.FILTER_ACCEPT;
           return NodeFilter.FILTER_ACCEPT;
         }
         }
Line 1,055: Line 1,102:
         frag.appendChild( document.createTextNode( val.slice( last ) ) );
         frag.appendChild( document.createTextNode( val.slice( last ) ) );
       }
       }
       textNode.parentNode.replaceChild( frag, textNode );
       if ( textNode.parentNode ) {
        textNode.parentNode.replaceChild( frag, textNode );
      }
     } );
     } );


Line 1,062: Line 1,111:


   function showDismissBar( query ) {
   function showDismissBar( query ) {
     var isMob = window.innerWidth < 768;
    /* Remove any existing bar */
    var existing = document.getElementById( 'gr-hl-bar' );
    if ( existing ) existing.remove();
 
     var isMob = window.innerWidth < 768 ||
                !!document.getElementById( 'mw-mf-viewport' );
 
     var bar = document.createElement( 'div' );
     var bar = document.createElement( 'div' );
     bar.id = 'gr-hl-bar';
     bar.id = 'gr-hl-bar';
    var count = document.querySelectorAll( '.gr-search-hl' ).length;
    if ( !count ) return; /* Nothing to navigate */


     if ( isMob ) {
     if ( isMob ) {
      /* ── Mobile: full-width terracotta bottom bar ── */
       bar.style.cssText = [
       bar.style.cssText = [
         'position:fixed', 'bottom:0', 'left:0', 'right:0',
         'position:fixed', 'bottom:0', 'left:0', 'right:0',
Line 1,075: Line 1,134:
         'font-family:system-ui,sans-serif',
         'font-family:system-ui,sans-serif',
         'box-shadow:0 -2px 12px rgba(0,0,0,0.25)',
         'box-shadow:0 -2px 12px rgba(0,0,0,0.25)',
        'height:56px'
       ].join(';');
       ].join(';');
    } else {
      bar.style.cssText = [
        'position:fixed', 'bottom:0', 'left:0', 'right:0', 'z-index:10200',
        'background:#b5451b', 'color:#fff', 'padding:10px 16px',
        'display:flex', 'align-items:center', 'justify-content:space-between',
        'font-family:system-ui,sans-serif', 'font-size:14px',
        'box-shadow:0 -2px 8px rgba(0,0,0,0.2)'
      ].join(';');
    }


    var count = document.querySelectorAll( '.gr-search-hl' ).length;
    if ( isMob ) {
       var mobBtnStyle = 'flex:1;display:flex;flex-direction:column;align-items:center;' +
       var mobBtnStyle = 'flex:1;display:flex;flex-direction:column;align-items:center;' +
         'justify-content:center;background:none;border:none;color:#fff;cursor:pointer;' +
         'justify-content:center;background:none;border:none;color:#fff;cursor:pointer;' +
         'padding:6px 4px;font-family:system-ui,sans-serif;font-size:11px;font-weight:500;' +
         'padding:6px 4px;font-family:system-ui,sans-serif;font-size:11px;font-weight:500;' +
         'gap:3px;letter-spacing:0.02em;-webkit-tap-highlight-color:rgba(0,0,0,0.12);';
         'gap:3px;-webkit-tap-highlight-color:rgba(0,0,0,0.12);';
 
       bar.innerHTML =
       bar.innerHTML =
         '<button id="gr-hl-prev" style="' + mobBtnStyle + '">' +
         '<button id="gr-hl-prev" style="' + mobBtnStyle + '">' +
Line 1,110: Line 1,159:
           '<span>Close</span>' +
           '<span>Close</span>' +
         '</button>';
         '</button>';
     } else {
     } else {
      /* ── Desktop: bottom banner ── */
      bar.style.cssText = [
        'position:fixed', 'bottom:0', 'left:0', 'right:0', 'z-index:10200',
        'background:#b5451b', 'color:#fff', 'padding:10px 16px',
        'display:flex', 'align-items:center', 'justify-content:space-between',
        'font-family:system-ui,sans-serif', 'font-size:14px',
        'box-shadow:0 -2px 8px rgba(0,0,0,0.2)'
      ].join(';');
       var nav = document.createElement( 'div' );
       var nav = document.createElement( 'div' );
       nav.style.cssText = 'display:flex;align-items:center;gap:12px;';
       nav.style.cssText = 'display:flex;align-items:center;gap:12px;';
Line 1,119: Line 1,178:
         '<button id="gr-hl-results" style="background:rgba(255,255,255,0.2);border:none;color:#fff;padding:4px 10px;border-radius:4px;cursor:pointer;font-size:13px;min-height:32px;">← Results</button>';
         '<button id="gr-hl-results" style="background:rgba(255,255,255,0.2);border:none;color:#fff;padding:4px 10px;border-radius:4px;cursor:pointer;font-size:13px;min-height:32px;">← Results</button>';
       bar.appendChild( nav );
       bar.appendChild( nav );
      var dismissBtn = document.createElement( 'button' );
      dismissBtn.id = 'gr-hl-dismiss';
      dismissBtn.textContent = '✕ Close';
      dismissBtn.style.cssText = 'background:rgba(255,255,255,0.15);border:none;color:#fff;padding:4px 12px;border-radius:4px;cursor:pointer;font-size:13px;min-height:32px;';
      bar.appendChild( dismissBtn );
     }
     }


     var dismiss = isMob
     document.body.appendChild( bar );
      ? bar.querySelector( '#gr-hl-dismiss' )
      : ( function() {
          var b = document.createElement( 'button' );
          b.textContent = '✕ Close';
          b.id = 'gr-hl-dismiss';
          b.style.cssText = 'background:rgba(255,255,255,0.15);border:none;color:#fff;padding:4px 12px;border-radius:4px;cursor:pointer;font-size:13px;min-height:32px;';
          bar.appendChild( b );
          return b;
        }() );


     dismiss.onclick = function () {
     /* ── Navigation ── */
      clearHighlights();
    var hlEls  = Array.from( document.querySelectorAll( '.gr-search-hl' ) );
      bar.remove();
     var current = 0;
     };


    document.body.appendChild( bar );
    var hlEls = Array.from( document.querySelectorAll( '.gr-search-hl' ) );
    var currentIdx = 0;
     function goTo( idx ) {
     function goTo( idx ) {
       hlEls.forEach( function(el) { el.classList.remove( 'gr-search-hl-current' ); } );
       hlEls.forEach( function(el) { el.classList.remove( 'gr-search-hl-current' ); } );
       currentIdx = ( idx + hlEls.length ) % hlEls.length;
       current = ( ( idx % hlEls.length ) + hlEls.length ) % hlEls.length;
       var el = hlEls[ currentIdx ];
       var el = hlEls[ current ];
       el.classList.add( 'gr-search-hl-current' );
       el.classList.add( 'gr-search-hl-current' );
       el.scrollIntoView({ behavior: 'smooth', block: 'center' });
       el.scrollIntoView({ behavior: 'smooth', block: 'center' });
    }
    function dismiss() {
      clearHighlights();
      bar.remove();
     }
     }


Line 1,152: Line 1,208:
     var prevBtn    = document.getElementById( 'gr-hl-prev' );
     var prevBtn    = document.getElementById( 'gr-hl-prev' );
     var resultsBtn = document.getElementById( 'gr-hl-results' );
     var resultsBtn = document.getElementById( 'gr-hl-results' );
     if ( nextBtn ) nextBtn.onclick = function() { goTo( currentIdx + 1 ); };
    var dismissEl  = document.getElementById( 'gr-hl-dismiss' );
     if ( prevBtn ) prevBtn.onclick = function() { goTo( currentIdx - 1 ); };
 
     if ( nextBtn )   nextBtn.onclick   = function() { goTo( current + 1 ); };
     if ( prevBtn )   prevBtn.onclick   = function() { goTo( current - 1 ); };
    if ( dismissEl )  dismissEl.onclick  = dismiss;
     if ( resultsBtn ) resultsBtn.onclick = function() {
     if ( resultsBtn ) resultsBtn.onclick = function() {
       bar.remove();
       bar.remove();
       /* Re-open search dialog with the same query prefilled */
       clearHighlights();
       if ( window.showSearchDialog ) { window.showSearchDialog( query ); }
       if ( window.showSearchDialog ) window.showSearchDialog( query );
     };
     };
   }
   }
Line 1,164: Line 1,223:
     document.querySelectorAll( '.gr-search-hl' ).forEach( function( span ) {
     document.querySelectorAll( '.gr-search-hl' ).forEach( function( span ) {
       var parent = span.parentNode;
       var parent = span.parentNode;
      if ( !parent ) return;
       while ( span.firstChild ) parent.insertBefore( span.firstChild, span );
       while ( span.firstChild ) parent.insertBefore( span.firstChild, span );
       parent.removeChild( span );
       parent.removeChild( span );
Line 1,178: Line 1,238:
     s.id = 'gr-hl-css';
     s.id = 'gr-hl-css';
     s.textContent = [
     s.textContent = [
       '.gr-search-hl{',
       '.gr-search-hl{background:#fff176;color:#1a1a1a;border-radius:2px;',
      '  background:#fff176;color:#1a1a1a;',
      'padding:0 1px;box-shadow:0 0 0 1px rgba(181,69,27,0.25);}',
      '  border-radius:2px;padding:0 1px;',
       '.gr-search-hl-current{background:#ffb300!important;',
      '  box-shadow:0 0 0 1px rgba(181,69,27,0.25);',
       'box-shadow:0 0 0 2px #b5451b!important;}',
      '}',
       '.gr-search-hl-current{',
      '  background:#ffb300!important;',
       ' box-shadow:0 0 0 2px #b5451b!important;',
      '}',
       '@keyframes gr-hl-pulse{',
       '@keyframes gr-hl-pulse{',
       ' 0%{background:#ffb300;}',
       '0%{background:#ffb300;}50%{background:#fff176;}100%{background:#fff176;}}',
      '  50%{background:#fff176;}',
      '  100%{background:#fff176;}',
      '}',
       '.gr-search-hl-pulse{animation:gr-hl-pulse 1.2s ease 2;}',
       '.gr-search-hl-pulse{animation:gr-hl-pulse 1.2s ease 2;}',
     ].join('');
     ].join('');
Line 1,198: Line 1,250:


   injectHighlightCSS();
   injectHighlightCSS();
  /* Run at DOMContentLoaded AND on wikipage.content hook (SPA navigation) */
  function run() {
    /* Small delay so mw.config is populated and DOM is ready */
    setTimeout( applyHighlight, 50 );
  }
   if ( document.readyState === 'loading' ) {
   if ( document.readyState === 'loading' ) {
     document.addEventListener( 'DOMContentLoaded', applyHighlight );
     document.addEventListener( 'DOMContentLoaded', run );
   } else {
   } else {
     applyHighlight();
     run();
  }
 
  /* Hook for MW SPA navigation */
  if ( window.mw ) {
    mw.hook( 'wikipage.content' ).add( function() {
      setTimeout( applyHighlight, 100 );
    } );
   }
   }