MediaWiki:Common.js: Difference between revisions

No edit summary
Tag: Manual revert
No edit summary
Line 951: Line 951:
     wireUllekhaLinks();
     wireUllekhaLinks();
   }
   }
}() );
/* ── Search result highlight — highlight query term on arrival ──
* When user clicks a search result, we pass the query in the URL
* hash as #gr-search:QUERY or read it from sessionStorage.
* On page load we find all matching text nodes and wrap them.
* ─────────────────────────────────────────────────────────────── */
( function () {
  /* ── Step 1: When leaving via a search result link,
    store the query in sessionStorage ── */
  function storeQueryForLink( url, query ) {
    try {
      sessionStorage.setItem( 'gr_search_hl', JSON.stringify({
        query: query,
        url:  url.split( '#' )[0]  // store without anchor
      }) );
    } catch(e) {}
  }
  /* ── Step 2: On page load, check if we arrived from a search result ── */
  function applyHighlight() {
    var stored;
    try {
      stored = JSON.parse( sessionStorage.getItem( 'gr_search_hl' ) || 'null' );
    } catch(e) { return; }
    if ( !stored || !stored.query ) return;
    /* Check URL matches */
    var currentBase = window.location.href.split( '#' )[0];
    if ( stored.url && stored.url !== currentBase ) {
      /* Different page — clear and bail */
      try { sessionStorage.removeItem( 'gr_search_hl' ); } catch(e) {}
      return;
    }
    var query = stored.query.trim();
    if ( !query ) return;
    /* Clear so refreshing doesn't re-highlight */
    try { sessionStorage.removeItem( 'gr_search_hl' ); } catch(e) {}
    /* Wait for content to render */
    setTimeout( function () {
      highlightText( query );
    }, 400 );
  }
  /* ── Core: walk text nodes and wrap matches ── */
  function highlightText( query ) {
    var content = document.querySelector( '#mw-content-text .mw-parser-output' );
    if ( !content ) return;
    /* Normalise query — strip quotes, split on spaces */
    var raw = query.replace( /^"|"$/g, '' ).trim();
    if ( !raw ) return;
    /* Build regex — escape special chars, match whole query first,
      fall back to individual words */
    var patterns = [];
    /* Full phrase */
    patterns.push( escapeRegex( raw ) );
    /* Individual words (min 2 chars) */
    raw.split( /\s+/ ).forEach( function(w) {
      if ( w.length >= 2 ) patterns.push( escapeRegex( w ) );
    } );
    var matched = false;
    for ( var pi = 0; pi < patterns.length; pi++ ) {
      var re;
      try { re = new RegExp( '(' + patterns[pi] + ')', 'gi' ); }
      catch(e) { continue; }
      var count = wrapMatches( content, re );
      if ( count > 0 ) { matched = true; break; }
    }
    if ( !matched ) return;
    /* Scroll to first highlight */
    var first = document.querySelector( '.gr-search-hl' );
    if ( first ) {
      first.scrollIntoView({ behavior: 'smooth', block: 'center' });
      /* Pulse animation */
      first.classList.add( 'gr-search-hl-pulse' );
      setTimeout( function() {
        first.classList.remove( 'gr-search-hl-pulse' );
      }, 2000 );
    }
    /* Show dismiss button */
    showDismissBar( query );
  }
  function escapeRegex( s ) {
    return s.replace( /[.*+?^${}()|[\]\\]/g, '\\$&' );
  }
  function wrapMatches( root, re ) {
    var count = 0;
    var walker = document.createTreeWalker(
      root, NodeFilter.SHOW_TEXT, {
        acceptNode: function( node ) {
          /* Skip inside script, style, our own highlights */
          var p = node.parentElement;
          if ( !p ) return NodeFilter.FILTER_REJECT;
          var tag = p.tagName.toUpperCase();
          if ( tag === 'SCRIPT' || tag === 'STYLE' || tag === 'NOSCRIPT' ) return NodeFilter.FILTER_REJECT;
          if ( p.classList.contains( 'gr-search-hl' ) ) return NodeFilter.FILTER_REJECT;
          return NodeFilter.FILTER_ACCEPT;
        }
      }, false
    );
    var nodes = [];
    var node;
    while ( ( node = walker.nextNode() ) ) nodes.push( node );
    nodes.forEach( function( textNode ) {
      var val = textNode.nodeValue;
      if ( !re.test( val ) ) return;
      re.lastIndex = 0;
      var frag = document.createDocumentFragment();
      var last = 0;
      var m;
      while ( ( m = re.exec( val ) ) !== null ) {
        if ( m.index > last ) {
          frag.appendChild( document.createTextNode( val.slice( last, m.index ) ) );
        }
        var span = document.createElement( 'span' );
        span.className = 'gr-search-hl';
        span.textContent = m[0];
        frag.appendChild( span );
        last = m.index + m[0].length;
        count++;
      }
      if ( last < val.length ) {
        frag.appendChild( document.createTextNode( val.slice( last ) ) );
      }
      textNode.parentNode.replaceChild( frag, textNode );
    } );
    return count;
  }
  /* ── Dismiss bar ── */
  function showDismissBar( query ) {
    var isMob = window.innerWidth < 768;
    var bar = document.createElement( 'div' );
    bar.id = 'gr-hl-bar';
    /* On mobile: float as a pill near top to avoid bottom bar conflicts.
      On desktop: sit at bottom. */
    if ( isMob ) {
      bar.style.cssText = [
        'position:fixed',
        'top:calc(var(--gr-header-height,56px) + var(--gr-toc-top,52px) + 8px)',
        'left:50%', 'transform:translateX(-50%)',
        'z-index:10200',
        'background:#b5451b', 'color:#fff',
        'padding:8px 14px',
        'border-radius:24px',
        'display:flex', 'align-items:center', 'gap:8px',
        'font-family:system-ui,sans-serif', 'font-size:13px',
        'box-shadow:0 3px 12px rgba(0,0,0,0.25)',
        'white-space:nowrap',
        'max-width:calc(100vw - 32px)'
      ].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 ) {
      /* Mobile: compact pill — just count + prev/next + dismiss */
      bar.innerHTML =
        '<span style="flex-shrink:0">🔍 ' + count + '</span>' +
        '<button id="gr-hl-prev" style="background:rgba(255,255,255,0.2);border:none;color:#fff;min-width:36px;height:36px;border-radius:50%;cursor:pointer;font-size:16px;display:flex;align-items:center;justify-content:center;">↑</button>' +
        '<button id="gr-hl-next" style="background:rgba(255,255,255,0.2);border:none;color:#fff;min-width:36px;height:36px;border-radius:50%;cursor:pointer;font-size:16px;display:flex;align-items:center;justify-content:center;">↓</button>' +
        '<button id="gr-hl-dismiss" style="background:rgba(255,255,255,0.15);border:none;color:#fff;min-width:36px;height:36px;border-radius:50%;cursor:pointer;font-size:16px;display:flex;align-items:center;justify-content:center;">✕</button>';
    } else {
      /* Desktop: full bar */
      var nav = document.createElement( 'div' );
      nav.style.cssText = 'display:flex;align-items:center;gap:12px;';
      nav.innerHTML =
        '<span>🔍 <strong>' + escHtml(query) + '</strong> — ' + count + ' match' + (count===1?'':'es') + '</span>' +
        '<button id="gr-hl-prev" 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;">↑ Prev</button>' +
        '<button id="gr-hl-next" 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;">↓ Next</button>';
      bar.appendChild( nav );
    }
    var dismiss = isMob
      ? bar.querySelector( '#gr-hl-dismiss' )
      : ( function() {
          var b = document.createElement( 'button' );
          b.textContent = '✕ Clear';
          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 () {
      clearHighlights();
      bar.remove();
    };
    document.body.appendChild( bar );
    /* Prev / Next navigation */
    var hlEls = Array.from( document.querySelectorAll( '.gr-search-hl' ) );
    var currentIdx = 0;
    function goTo( idx ) {
      hlEls.forEach( function(el) { el.classList.remove( 'gr-search-hl-current' ); } );
      currentIdx = ( idx + hlEls.length ) % hlEls.length;
      var el = hlEls[ currentIdx ];
      el.classList.add( 'gr-search-hl-current' );
      el.scrollIntoView({ behavior: 'smooth', block: 'center' });
    }
    var nextBtn = document.getElementById( 'gr-hl-next' );
    var prevBtn = document.getElementById( 'gr-hl-prev' );
    if ( nextBtn ) nextBtn.onclick = function() { goTo( currentIdx + 1 ); };
    if ( prevBtn ) prevBtn.onclick = function() { goTo( currentIdx - 1 ); };
  }
  function clearHighlights() {
    document.querySelectorAll( '.gr-search-hl' ).forEach( function( span ) {
      var parent = span.parentNode;
      while ( span.firstChild ) parent.insertBefore( span.firstChild, span );
      parent.removeChild( span );
    } );
  }
  function escHtml( s ) {
    return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
  }
  /* ── Inject CSS for highlights ── */
  function injectHighlightCSS() {
    if ( document.getElementById( 'gr-hl-css' ) ) return;
    var s = document.createElement( 'style' );
    s.id = 'gr-hl-css';
    s.textContent = [
      '.gr-search-hl{',
      '  background:#fff176;color:#1a1a1a;',
      '  border-radius:2px;padding:0 1px;',
      '  box-shadow:0 0 0 1px rgba(181,69,27,0.25);',
      '}',
      '.gr-search-hl-current{',
      '  background:#ffb300!important;',
      '  box-shadow:0 0 0 2px #b5451b!important;',
      '}',
      '@keyframes gr-hl-pulse{',
      '  0%{background:#ffb300;}',
      '  50%{background:#fff176;}',
      '  100%{background:#fff176;}',
      '}',
      '.gr-search-hl-pulse{animation:gr-hl-pulse 1.2s ease 2;}',
    ].join('');
    document.head.appendChild( s );
  }
  /* ── Boot ── */
  injectHighlightCSS();
  if ( document.readyState === 'loading' ) {
    document.addEventListener( 'DOMContentLoaded', applyHighlight );
  } else {
    applyHighlight();
  }
  /* ── Expose storeQueryForLink for readerToolbar to call ── */
  window.grStoreSearchHL = storeQueryForLink;
}() );
}() );