Toggle menu
Toggle preferences menu
Toggle personal menu
Not logged in
Your IP address will be publicly visible if you make any edits.

MediaWiki:Common.js

MediaWiki interface page
Revision as of 23:10, 14 March 2026 by Menhir wiki admin (talk | contribs) (Add ApprovedRevs approve button fix for Citizen skin)

Note: After publishing, you may have to bypass your browser's cache to see the changes.

  • Firefox / Safari: Hold Shift while clicking Reload, or press either Ctrl-F5 or Ctrl-R (⌘-R on a Mac)
  • Google Chrome: Press Ctrl-Shift-R (⌘-Shift-R on a Mac)
  • Edge: Hold Ctrl while clicking Refresh, or press Ctrl-F5.
/* Menhirs Fate Wiki - Custom Header
 * Menu items are loaded from MediaWiki:Mf-navigation
 * Edit that page to change nav links, dropdowns, and ordering.
 */
( function () {
    "use strict";

    /* ── Google Fonts ─────────────────────────────────────────────────── */
    var link = document.createElement( "link" );
    link.rel = "stylesheet";
    link.href = "https://fonts.googleapis.com/css2?family=Crimson+Text:ital,wght@0,400;0,600;0,700;1,400&display=swap";
    document.head.appendChild( link );

    /* ── Build header shell ───────────────────────────────────────────── */
    var header = document.createElement( "div" );
    header.id = "mf-header";
    header.innerHTML =
        '<div id="mf-header-top">' +
            '<a href="/wiki/Main_Page" id="mf-logo-link">' +
                '<img src="https://www.menhirsfate.com/wp-content/uploads/2024/08/white-logo-scaled-120x65.png" alt="Menhirs Fate" id="mf-logo" />' +
            '</a>' +
        '</div>' +
        '<nav id="mf-ribbon">' +
            '<div id="mf-ribbon-inner">' +
                '<span id="mf-nav-loading" style="color:#844725;font-style:italic;padding:0 17px;line-height:50px;">Loading…</span>' +
            '</div>' +
        '</nav>';
    document.body.insertBefore( header, document.body.firstChild );

    /* ── Hamburger button ─────────────────────────────────────────────── */
    var burger = document.createElement( "button" );
    burger.id = "mf-burger";
    burger.setAttribute( "aria-label", "Toggle menu" );
    burger.innerHTML = "<span></span><span></span><span></span>";
    document.getElementById( "mf-header-top" ).appendChild( burger );

    var ribbon = document.getElementById( "mf-ribbon-inner" );

    burger.addEventListener( "click", function () {
        ribbon.classList.toggle( "mf-open" );
        burger.classList.toggle( "mf-open" );
    } );

    /* ── Parse navigation config ──────────────────────────────────────── */
    function parseNavConfig( text ) {
        var items = [];
        /* Strip all HTML comments before parsing */
        text = text.replace( /<!--[\s\S]*?-->/g, "" );
        var lines = text.split( "\n" );
        var current = null;

        lines.forEach( function ( line ) {
            /* skip blanks */
            if ( /^\s*$/.test( line ) ) return;

            var subMatch = line.match( /^\s*\*\*\s+(.+?)\s*\|\s*(.+?)\s*$/ );
            if ( subMatch && current ) {
                current.children.push( { label: subMatch[1], url: subMatch[2].trim() } );
                return;
            }

            var dropMatch = line.match( /^\s*\*\s+(.+?)\s*>\s*$/ );
            if ( dropMatch ) {
                current = { label: dropMatch[1].trim(), children: [] };
                items.push( current );
                return;
            }

            var linkMatch = line.match( /^\s*\*\s+(.+?)\s*\|\s*(.+?)\s*$/ );
            if ( linkMatch ) {
                var parts = linkMatch[2].split( "|" ).map( function(s){ return s.trim(); } );
                var url = parts[0];
                var flags = parts.slice(1);
                current = null;
                items.push( { label: linkMatch[1].trim(), url: url, flags: flags } );
            }
        } );

        return items;
    }

    /* ── Build nav HTML from parsed items ─────────────────────────────── */
    function buildNav( items ) {
        var html = "";
        var dropdownCount = 0;

        items.forEach( function ( item ) {
            if ( item.children ) {
                /* dropdown */
                html += '<div class="mf-has-dropdown">' +
                    '<span class="mf-nav-label">' + item.label + ' <span class="mf-caret">&#9662;</span></span>' +
                    '<div class="mf-dropdown">';
                item.children.forEach( function ( child ) {
                    html += '<a href="' + child.url + '">' + child.label + '</a>';
                } );
                html += '</div></div>';
                dropdownCount++;
            } else {
                /* simple link */
                var cls = "mf-nav-item";
                if ( item.flags && item.flags.indexOf( "back" ) !== -1 ) {
                    cls += " mf-nav-back";
                }
                html += '<a href="' + item.url + '" class="' + cls + '">' + item.label + '</a>';
            }
        } );

        /* Search — inline input for desktop, icon-only for narrow screens */
        html += '<div id="mf-search-wrap">' +
            '<form action="/wiki/Special:Search" id="mf-search-form">' +
                '<input type="text" name="search" id="mf-search-input" placeholder="Search…" autocomplete="off" />' +
                '<button type="submit" id="mf-search-btn" aria-label="Search">' +
                    '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>' +
                '</button>' +
            '</form>' +
            '<button type="button" id="mf-search-icon" aria-label="Search">' +
                '<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>' +
            '</button>' +
            '<div id="mf-search-suggestions"></div>' +
        '</div>';

        /* Update grid columns: back-link(auto) + N nav items(1fr each) + search(auto) */
        var navItemCount = 0;
        items.forEach( function(i) { if ( !i.flags || i.flags.indexOf("back") === -1 ) navItemCount++; } );

        return { html: html, navItemCount: navItemCount };
    }

    /* ── Fetch config and render ──────────────────────────────────────── */
    /* ── Fetch config, render nav, and add Crew Wiki links ─────────────── */
    /* Optimised: nav config cached in sessionStorage; user rights checked
       via wgUserGroups instead of an API call. */

    function renderNavFromConfig( raw ) {
        var items = parseNavConfig( raw );
        var result = buildNav( items );
        ribbon.innerHTML = result.html;

        /* Set grid columns dynamically */
        var cols = "auto repeat(" + result.navItemCount + ", 1fr) auto";
        ribbon.style.gridTemplateColumns = cols;

        initDropdowns();
        initSearch();

        /* ── Dynamic "Crew Wiki" links (Volunteer/Editor/Admin only) ── */
        var groups = mw.config.get( 'wgUserGroups' ) || [];
        var isCrewMember = groups.indexOf( 'sysop' ) !== -1 ||
                           groups.indexOf( 'editor' ) !== -1 ||
                           groups.indexOf( 'volunteer' ) !== -1;

        if ( mw.config.get( 'wgUserName' ) && isCrewMember ) {
            /* Ribbon link */
            if ( !document.getElementById( 'mf-crew-link' ) ) {
                var crewLink = document.createElement( 'a' );
                crewLink.id = 'mf-crew-link';
                crewLink.className = 'mf-nav-item';
                crewLink.href = mw.util.getUrl( 'Crew:Main_Page' );
                crewLink.textContent = 'Crew Wiki';

                var searchWrap = document.getElementById( 'mf-search-wrap' );
                if ( searchWrap ) {
                    ribbon.insertBefore( crewLink, searchWrap );
                } else {
                    ribbon.appendChild( crewLink );
                }

                /* Update grid to include the new item */
                var allNavItems = ribbon.querySelectorAll(
                    '.mf-nav-item:not(.mf-nav-back), .mf-has-dropdown'
                );
                var newCols = 'auto repeat(' + allNavItems.length + ', 1fr) auto';
                ribbon.style.gridTemplateColumns = newCols;
            }

            /* Sidebar section */
            var mainMenu = document.getElementById( 'citizen-main-menu' );
            if ( mainMenu && !document.getElementById( 'p-CrewWiki' ) ) {
                var crewNav = document.createElement( 'nav' );
                crewNav.id = 'p-CrewWiki';
                crewNav.className = 'citizen-menu mw-portlet mw-portlet-CrewWiki';
                crewNav.innerHTML =
                    '<div class="citizen-menu__heading">Crew Wiki</div>' +
                    '<div class="citizen-menu__content">' +
                    '<ul class="citizen-menu__content-list">' +
                    '<li class="mw-list-item"><a href="' +
                        mw.util.getUrl( 'Crew:Main_Page' ) +
                        '"><span>Crew Home</span></a></li>' +
                    '<li class="mw-list-item"><a href="' +
                        mw.util.getUrl( 'Crew:Editor_Guide' ) +
                        '"><span>Editor Guide</span></a></li>' +
                    '</ul></div>';

                var resources = document.getElementById( 'p-Resources' );
                if ( resources && resources.nextSibling ) {
                    mainMenu.insertBefore( crewNav, resources.nextSibling );
                } else {
                    mainMenu.appendChild( crewNav );
                }
            }
        }
    }

    /* ── Cache-first nav loading ──────────────────────────────────────── */
    var NAV_CACHE_KEY = 'mf-nav-config';
    var NAV_CACHE_TS_KEY = 'mf-nav-config-ts';
    var NAV_CACHE_TTL = 24 * 60 * 60 * 1000; /* 24 hours */

    var cachedNav = null;
    var cacheAge = Infinity;

    try {
        cachedNav = sessionStorage.getItem( NAV_CACHE_KEY );
        var cachedTs = parseInt( sessionStorage.getItem( NAV_CACHE_TS_KEY ), 10 );
        if ( cachedTs ) cacheAge = Date.now() - cachedTs;
    } catch ( e ) { /* sessionStorage unavailable */ }

    if ( cachedNav && cacheAge < NAV_CACHE_TTL ) {
        /* Fast path: render from cache, no API call */
        renderNavFromConfig( cachedNav );
    } else {
        /* Slow path: first visit or cache expired — fetch via API */
        mw.loader.using( 'mediawiki.api' ).then( function () {
            new mw.Api().get( {
                action: 'query',
                titles: 'MediaWiki:Mf-navigation',
                prop: 'revisions',
                rvprop: 'content',
                rvslots: 'main',
                format: 'json'
            } ).then( function ( data ) {
                var pages = data.query.pages;
                var page = pages[ Object.keys( pages )[0] ];
                if ( !page.revisions ) return;
                var raw = page.revisions[0].slots.main['*'];

                /* Cache for next page load */
                try {
                    sessionStorage.setItem( NAV_CACHE_KEY, raw );
                    sessionStorage.setItem( NAV_CACHE_TS_KEY, String( Date.now() ) );
                } catch ( e ) { /* quota exceeded or unavailable */ }

                renderNavFromConfig( raw );
            } );
        } );
    }

    /* ── Dropdown toggle logic ────────────────────────────────────────── */
    function initDropdowns() {
        var labels = document.querySelectorAll( ".mf-has-dropdown > .mf-nav-label" );
        Array.prototype.forEach.call( labels, function ( label ) {
            label.addEventListener( "click", function ( e ) {
                e.stopPropagation();
                var parent = this.parentNode;
                var open = document.querySelectorAll( ".mf-has-dropdown.mf-dropdown-open" );
                Array.prototype.forEach.call( open, function ( s ) {
                    if ( s !== parent ) s.classList.remove( "mf-dropdown-open" );
                } );
                parent.classList.toggle( "mf-dropdown-open" );
            } );
        } );
        document.addEventListener( "click", function ( e ) {
            if ( !e.target.closest( ".mf-has-dropdown" ) ) {
                var open = document.querySelectorAll( ".mf-has-dropdown.mf-dropdown-open" );
                Array.prototype.forEach.call( open, function ( el ) {
                    el.classList.remove( "mf-dropdown-open" );
                } );
            }
        } );
        /* Close mobile menu on link click */
        ribbon.addEventListener( "click", function ( e ) {
            if ( e.target.tagName === "A" ) {
                ribbon.classList.remove( "mf-open" );
                burger.classList.remove( "mf-open" );
            }
        } );
    }

    /* ── Search with suggestions ──────────────────────────────────────── */
    function initSearch() {
        var input = document.getElementById( "mf-search-input" );
        var sugBox = document.getElementById( "mf-search-suggestions" );
        var iconBtn = document.getElementById( "mf-search-icon" );
        var debounce = null;

        if ( !input || !sugBox ) return;

        /* Icon button (shown on narrow screens) expands inline search */
        var searchSvg = '<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>';
        var closeSvg  = '<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>';
        if ( iconBtn ) {
            iconBtn.addEventListener( "click", function () {
                var wrap = document.getElementById( "mf-search-wrap" );
                wrap.classList.toggle( "mf-search-expanded" );
                if ( wrap.classList.contains( "mf-search-expanded" ) ) {
                    iconBtn.innerHTML = closeSvg;
                    input.focus();
                } else {
                    iconBtn.innerHTML = searchSvg;
                    input.value = "";
                    sugBox.style.display = "none";
                    sugBox.innerHTML = "";
                }
            } );
        }

        /* Autocomplete suggestions */
        input.addEventListener( "input", function () {
            var q = input.value.trim();
            clearTimeout( debounce );
            if ( q.length < 2 ) {
                sugBox.style.display = "none";
                sugBox.innerHTML = "";
                return;
            }
            debounce = setTimeout( function () {
                mw.loader.using( "mediawiki.api" ).then( function () {
                    new mw.Api().get( {
                        action: "opensearch",
                        search: q,
                        limit: 6,
                        namespace: 0
                    } ).then( function ( data ) {
                        var titles = data[1] || [];
                        if ( !titles.length ) {
                            sugBox.style.display = "none";
                            sugBox.innerHTML = "";
                            return;
                        }
                        sugBox.innerHTML = "";
                        titles.forEach( function ( t ) {
                            var a = document.createElement( "a" );
                            a.href = mw.util.getUrl( t );
                            a.textContent = t;
                            a.className = "mf-suggestion";
                            sugBox.appendChild( a );
                        } );
                        sugBox.style.display = "block";
                    } );
                } );
            }, 200 );
        } );
        input.addEventListener( "focus", function () {
            if ( sugBox.children.length ) sugBox.style.display = "block";
        } );
        document.addEventListener( "click", function ( e ) {
            if ( !e.target.closest( "#mf-search-wrap" ) ) {
                sugBox.style.display = "none";
            }
        } );
    }

    /* ── Fix sitename typo wherever it appears ────────────────────────── */
    document.querySelectorAll( ".citizen-footer__sitetitle, .citizen-footer a, .mw-logo-wordmark" ).forEach( function ( el ) {
        if ( el.textContent.indexOf( "Mehirs" ) !== -1 ) {
            el.textContent = el.textContent.replace( /Mehirs/g, "Menhirs" );
        }
    } );

    /* ── Citizen header z-index ───────────────────────────────────────── */
    /* Handled in Common.css */

}() );


/* === ApprovedRevs: Approve Button Fix for Citizen Skin === */
/* Adds an "Approve" button to the page actions when viewing */
/* an unapproved revision that the current user can approve. */
(function () {
  'use strict';

  // Only run on view action
  if (mw.config.get('wgAction') !== 'view') return;

  // Check if ApprovedRevs is active on this page via body classes
  var body = document.body;
  var isNotApproved = body.classList.contains('approvedRevs-notapproved');
  var hasNoApprovedRev = body.classList.contains('approvedRevs-noapprovedrev');

  // If the page is already showing the approved revision (not notapproved), skip
  if (!isNotApproved && !hasNoApprovedRev) return;

  // We need user to have approverevisions right
  mw.loader.using('mediawiki.api').then(function () {
    var api = new mw.Api();

    // Check user rights
    return api.get({
      action: 'query',
      meta: 'userinfo',
      uiprop: 'rights',
      format: 'json'
    }).then(function (data) {
      var rights = data.query.userinfo.rights || [];
      if (rights.indexOf('approverevisions') === -1) return;

      // Get the revision ID to approve
      var revId = mw.config.get('wgRevisionId');
      var curRevId = mw.config.get('wgCurRevisionId');
      var title = mw.config.get('wgPageName');

      // Determine which revision to approve:
      // If viewing the latest revision (or the default page view), approve curRevId
      // If viewing an old revision, approve that specific revision
      var approveRevId = revId || curRevId;

      // Add approve button to the "More" actions menu (p-cactions)
      var cactions = document.getElementById('p-cactions');
      if (cactions) {
        var list = cactions.querySelector('ul, .citizen-menu__content');
        if (list) {
          var li = document.createElement('li');
          li.id = 'ca-approve';
          li.className = 'mw-list-item';

          var link = document.createElement('a');
          link.textContent = 'Approve';
          link.title = 'Approve revision ' + approveRevId;
          link.style.cursor = 'pointer';
          link.style.color = '#4aba5a';
          link.style.fontWeight = 'bold';

          link.addEventListener('click', function (e) {
            e.preventDefault();
            if (!confirm('Approve revision ' + approveRevId + ' of "' + title.replace(/_/g, ' ') + '"?')) return;

            link.textContent = 'Approving...';
            link.style.pointerEvents = 'none';

            api.postWithToken('csrf', {
              action: 'approve',
              revid: approveRevId
            }).then(function () {
              link.textContent = 'Approved!';
              link.style.color = '#2ecc71';
              // Reload to show the approved version
              setTimeout(function () {
                window.location.href = mw.util.getUrl(title);
              }, 800);
            }, function (code, result) {
              link.textContent = 'Error: ' + code;
              link.style.color = '#e74c3c';
              link.style.pointerEvents = 'auto';
              console.error('ApprovedRevs approve failed:', code, result);
            });
          });

          li.appendChild(link);
          list.appendChild(li);
        }
      }

      // Also add a banner below the page title for visibility
      var contentSub = document.getElementById('contentSub');
      if (contentSub) {
        var banner = document.createElement('div');
        banner.style.cssText = 'background:#2d2d1f; border:1px solid #4aba5a; border-radius:4px; padding:8px 14px; margin:8px 0; display:inline-flex; align-items:center; gap:10px; font-size:0.9em;';

        var msg = document.createElement('span');
        if (isNotApproved) {
          msg.textContent = 'This revision is not yet approved.';
        } else {
          msg.textContent = 'This page has no approved revision.';
        }
        msg.style.color = '#e0d0a0';

        var btn = document.createElement('button');
        btn.textContent = 'Approve Rev ' + approveRevId;
        btn.style.cssText = 'background:#4aba5a; color:#fff; border:none; border-radius:3px; padding:4px 12px; cursor:pointer; font-weight:bold; font-size:0.9em;';

        btn.addEventListener('mouseenter', function () { btn.style.background = '#3da34d'; });
        btn.addEventListener('mouseleave', function () { btn.style.background = '#4aba5a'; });

        btn.addEventListener('click', function () {
          if (!confirm('Approve revision ' + approveRevId + ' of "' + title.replace(/_/g, ' ') + '"?')) return;

          btn.textContent = 'Approving...';
          btn.disabled = true;

          api.postWithToken('csrf', {
            action: 'approve',
            revid: approveRevId
          }).then(function () {
            btn.textContent = 'Approved!';
            btn.style.background = '#2ecc71';
            banner.style.borderColor = '#2ecc71';
            setTimeout(function () {
              window.location.href = mw.util.getUrl(title);
            }, 800);
          }, function (code, result) {
            btn.textContent = 'Error: ' + code;
            btn.style.background = '#e74c3c';
            btn.disabled = false;
            console.error('ApprovedRevs approve failed:', code, result);
          });
        });

        banner.appendChild(msg);
        banner.appendChild(btn);
        contentSub.appendChild(banner);
      }
    });
  });
})();