MediaWiki:Common.js

From NOPOTIONS Wiki
Jump to navigation Jump to search

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.
/* Expand all / Collapse all controls on each affix mod group header
   (.np-modgroup blocks emitted by the wiki generator). */
mw.hook('wikipage.content').add(function ($content) {
  $content.find('.np-modgroup').each(function () {
    var $group = $(this);
    if ($group.data('np-controls')) {
      return;
    }
    $group.data('np-controls', true);
    if ($group.find('.mw-collapsible').length < 2) {
      return;
    }
    var toggleAll = function (expand) {
      $group.find('.mw-collapsible').each(function () {
        var api = $(this).data('mw-collapsible');
        if (api) {
          if (expand) {
            api.expand();
          } else {
            api.collapse();
          }
        }
      });
    };
    var $controls = $('<span>').addClass('np-mod-controls');
    $('<a>')
      .text('Expand all')
      .on('click', function () {
        toggleAll(true);
      })
      .appendTo($controls);
    $controls.append(' · ');
    $('<a>')
      .text('Collapse all')
      .on('click', function () {
        toggleAll(false);
      })
      .appendTo($controls);
    $group.children('.np-modgroup-header').append($controls);
  });
});

/* Page-level Expand all / Collapse all for the accordions on the slot and item
   pages, mirroring the affix-list group controls above. The generator wraps
   each grouped base table in .np-mod.mw-collapsible (the table itself is a
   plain .np-base-table, which is why an earlier .np-base-table.mw-collapsible
   selector matched nothing and these pages had no controls). Accordions inside
   .np-modgroup already carry per-group controls, so they are skipped here.
   Placed under the section's first heading so one control governs the page. */
mw.hook('wikipage.content').add(function ($content) {
  var $tables = $content.find('.np-mod.mw-collapsible').filter(function () {
    return $(this).closest('.np-modgroup').length === 0;
  });
  if ($tables.length < 2) {
    return;
  }
  var $first = $tables.first();
  if ($first.data('np-base-controls')) {
    return;
  }
  $first.data('np-base-controls', true);
  var toggleAll = function (expand) {
    // Nested hand-class accordions ride inside the outer ones, so the control
    // has to reach them too or "Expand all" leaves rows still hidden.
    $tables.add($tables.find('.mw-collapsible')).each(function () {
      var api = $(this).data('mw-collapsible');
      if (api) {
        if (expand) {
          api.expand();
        } else {
          api.collapse();
        }
      }
    });
  };
  var $controls = $('<div>').addClass('np-base-controls');
  $('<a>')
    .text('Expand all')
    .on('click', function () {
      toggleAll(true);
    })
    .appendTo($controls);
  $controls.append(' · ');
  $('<a>')
    .text('Collapse all')
    .on('click', function () {
      toggleAll(false);
    })
    .appendTo($controls);
  var $heading = $first.prevAll('.mw-heading, h2, h3, h4').last();
  if ($heading.length) {
    $controls.insertAfter($heading);
  } else {
    $controls.insertBefore($first);
  }
});

/* Footer meta-link separators wrap to a second row on narrow viewports; CSS can't
   tell where a row breaks, so it would leave a dangling "·" at the right edge of
   the first row. Mark the last link of each visual row with .np-rowend (the CSS
   suppresses its trailing dot) and re-evaluate on resize / font load. */
(function () {
  function markFooterRowEnds() {
    var places = document.getElementById('footer-places');
    if (!places) {
      return;
    }
    var items = Array.prototype.filter.call(places.children, function (li) {
      return li.offsetParent !== null; // visible row items only
    });
    // Sort by rendered position, not DOM order: the "Play NOPOTIONS" CTA uses flex
    // `order` to sit on its own row above the meta links, so its DOM neighbour is
    // not its visual neighbour. Compare laid-out top/left instead.
    var top = function (li) {
      return Math.round(li.getBoundingClientRect().top);
    };
    items.sort(function (a, b) {
      return (
        top(a) - top(b) ||
        a.getBoundingClientRect().left - b.getBoundingClientRect().left
      );
    });
    for (var i = 0; i < items.length; i++) {
      var li = items[i];
      var next = items[i + 1];
      // last item of a row = no next item, or the next item sits lower (new row)
      if (!next || top(next) > top(li)) {
        li.classList.add('np-rowend');
      } else {
        li.classList.remove('np-rowend');
      }
    }
  }
  var timer;
  function schedule() {
    clearTimeout(timer);
    timer = setTimeout(markFooterRowEnds, 100);
  }
  if (document.readyState !== 'loading') {
    markFooterRowEnds();
  } else {
    document.addEventListener('DOMContentLoaded', markFooterRowEnds);
  }
  window.addEventListener('load', markFooterRowEnds);
  window.addEventListener('resize', schedule);
  if (document.fonts && document.fonts.ready) {
    document.fonts.ready.then(markFooterRowEnds);
  }
})();

/* Mobile nav drawer: put the sidebar's link groups back.

   The mobile skin does not read MediaWiki:Sidebar the way the desktop skin
   does. Its menu is built from a fixed two-entry list, Home and Random, and
   every other sidebar section is handed to it and dropped, which is why the
   drawer arrives with no navigation in it at all. Read the sidebar source and
   add the missing groups back, so the tree is still edited in one place and
   both skins show the same links. */
(function () {
  if (mw.config.get('skin') !== 'minerva') {
    return;
  }
  var CACHE_KEY = 'np-mobile-nav';
  /* Sections the drawer must not repeat: navigation is where its own Home and
     Random entries come from, and the rest are skin chrome, not reader links. */
  var SKIP = { navigation: true, toolbox: true, search: true, languages: true };

  /* MediaWiki:Sidebar is "* Section" lines with "** target|label" beneath. */
  function parseSidebar(text) {
    var sections = [];
    var current = null;
    text.split('\n').forEach(function (raw) {
      var line = raw.replace(/\s+$/, '');
      var entry = /^\*\*\s*(.+)$/.exec(line);
      if (entry) {
        var pipe = entry[1].indexOf('|');
        if (!current || pipe === -1) {
          return;
        }
        current.items.push({
          target: entry[1].slice(0, pipe).trim(),
          label: entry[1].slice(pipe + 1).trim(),
        });
        return;
      }
      var head = /^\*\s*(.+)$/.exec(line);
      if (head) {
        current = { name: head[1].trim(), items: [] };
        sections.push(current);
      }
    });
    return sections;
  }

  function hrefFor(target) {
    return /^(https?:)?\/\//.test(target) ? target : mw.util.getUrl(target);
  }

  function buildGroup(section) {
    var list = document.createElement('ul');
    list.className = 'toggle-list__list np-drawer-group';
    /* A one-word lowercase section name is a message key rather than a label
       meant for a reader, so those groups render without a heading. */
    if (!/^[a-z]+$/.test(section.name)) {
      var heading = document.createElement('li');
      heading.className = 'np-drawer-heading';
      heading.textContent = section.name;
      list.appendChild(heading);
    }
    section.items.forEach(function (item) {
      var row = document.createElement('li');
      row.className = 'toggle-list-item';
      var link = document.createElement('a');
      link.className = 'toggle-list-item__anchor';
      link.href = hrefFor(item.target);
      var label = document.createElement('span');
      label.className = 'toggle-list-item__label';
      label.textContent = item.label;
      link.appendChild(label);
      row.appendChild(link);
      list.appendChild(row);
    });
    return list;
  }

  function render(text) {
    var drawer = document.getElementById('mw-mf-page-left');
    if (!drawer || drawer.getAttribute('data-np-nav')) {
      return;
    }
    var groups = parseSidebar(text).filter(function (section) {
      return section.items.length && !SKIP[section.name.toLowerCase()];
    });
    if (!groups.length) {
      return;
    }
    drawer.setAttribute('data-np-nav', '1');
    var fragment = document.createDocumentFragment();
    groups.forEach(function (section) {
      fragment.appendChild(buildGroup(section));
    });
    /* Home and Random keep the top of the drawer; the tree follows them. */
    var first = drawer.querySelector('#p-navigation');
    if (first) {
      drawer.insertBefore(fragment, first.nextSibling);
    } else {
      drawer.insertBefore(fragment, drawer.firstChild);
    }
  }

  function load() {
    var cached = null;
    try {
      cached = window.sessionStorage.getItem(CACHE_KEY);
    } catch (e) {
      cached = null;
    }
    if (cached) {
      render(cached);
      return;
    }
    $.get(mw.util.getUrl('MediaWiki:Sidebar', { action: 'raw' })).done(
      function (text) {
        if (typeof text !== 'string' || !text) {
          return;
        }
        try {
          window.sessionStorage.setItem(CACHE_KEY, text);
        } catch (e) {
          /* A blocked or full store only costs one fetch per page view. */
        }
        render(text);
      }
    );
  }

  mw.loader.using(['mediawiki.util']).then(function () {
    $(load);
  });
})();