MediaWiki:Common.js: Difference between revisions

From PickiPedia: A knowledge base of bluegrass, old time psychedelic jams, and other public domain music
Jump to navigationJump to search
Magent (talk | contribs)
Simplify blocks ago display - remove estimated block number (via update-page on MediaWiki MCP Server)
No edit summary
 
(28 intermediate revisions by 2 users not shown)
Line 1: Line 1:
/**
/**
* Blue Railroad Submission - Date to Block Height Converter
* What used to be here now ships with the PickiPediaReleases extension.
* Adds a datepicker that converts dates to Ethereum block heights
*
* Also shows time ago for news templates
*  the HLS video player  -> ext.pickipediaReleases.videoPlayer.js    (#124)
*/
*  Blue Railroad date picker -> ext.pickipediaReleases.blueRailroadDate.js (#131)
*  "X blocks ago"        -> ext.pickipediaReleases.blocksAgo.js      (#131)
*  Instrument form checks -> ext.pickipediaReleases.instrumentForm.js (#131)
*
* All four load on every page from the extension's BeforePageDisplay hook,
* exactly as they did from here, and each does nothing unless the form or
* element it cares about is on the page being shown. Changing any of them is
* a pull request now: reviewed, syntax-checked by CI, deployed by Jenkins —
* rather than several hundred lines pasted into this box, which is how the
* whole of this file went down for 23 minutes on 15 September 2026.
*
* What remains below is the user-subpage zine styling. It stays because it is
* bound to classes defined in MediaWiki:Common.css, and moving the script
* without its stylesheet would split one feature across two homes. It goes
* next, with the CSS, after which this page can be empty.
*
* See cryptograss/pickipedia#124 and #131.
*/


(function() {
/* ============================================================
    'use strict';
  * Pickipedia — User-subpage zine/Substack styling
   
    // Only run on the Blue Railroad Submission form
    if (!document.querySelector('input[name="Blue Railroad Submission[block_height]"]')) {
        return;
    }
   
    // Reference point: known block and timestamp
    // Post-merge average block time is ~12.12 seconds
    const AVG_BLOCK_TIME = 12.12;
   
    // Get current block from footer (format: "24,328,442")
    function getCurrentBlockFromFooter() {
        const footerLink = document.querySelector('a[href*="etherscan.io/block/"]');
        if (footerLink) {
            const match = footerLink.href.match(/block\/(\d+)/);
            if (match) return parseInt(match[1]);
        }
        return null;
    }
   
    // Calculate block height from date
    function dateToBlockHeight(targetDate, refBlock, refTimestamp) {
        const targetTimestamp = targetDate.getTime() / 1000;
        const secondsDiff = refTimestamp - targetTimestamp;
        const blocksDiff = Math.round(secondsDiff / AVG_BLOCK_TIME);
        return refBlock - blocksDiff;
    }
   
    // Add the datepicker UI
    function addDatePicker() {
        const blockInput = document.querySelector('input[name="Blue Railroad Submission[block_height]"]');
        if (!blockInput) return;
       
        const container = document.createElement('div');
        container.style.marginTop = '8px';
        container.innerHTML = `
            <label style="display: block; margin-bottom: 4px; font-size: 0.9em;">
                Or pick a date/time:
            </label>
            <input type="datetime-local" id="br-datepicker" style="padding: 4px; margin-right: 8px;">
            <button type="button" id="br-convert-btn" style="padding: 4px 12px; cursor: pointer;">
                Convert to Block
            </button>
            <span id="br-status" style="margin-left: 8px; font-size: 0.9em; color: #666;"></span>
        `;
       
        blockInput.parentNode.appendChild(container);
       
        const datePicker = document.getElementById('br-datepicker');
        const convertBtn = document.getElementById('br-convert-btn');
        const status = document.getElementById('br-status');
       
        // Set default to now
        const now = new Date();
        datePicker.value = now.toISOString().slice(0, 16);
       
        convertBtn.addEventListener('click', function() {
            const selectedDate = new Date(datePicker.value);
            if (isNaN(selectedDate.getTime())) {
                status.textContent = 'Invalid date';
                status.style.color = 'red';
                return;
            }
           
            const currentBlock = getCurrentBlockFromFooter();
            if (!currentBlock) {
                status.textContent = 'Could not find reference block';
                status.style.color = 'red';
                return;
            }
           
            const currentTimestamp = Date.now() / 1000;
            const estimatedBlock = dateToBlockHeight(selectedDate, currentBlock, currentTimestamp);
           
            if (estimatedBlock > currentBlock) {
                status.textContent = 'Date is in the future!';
                status.style.color = 'orange';
            } else {
                status.textContent = '~estimated';
                status.style.color = 'green';
            }
           
            blockInput.value = estimatedBlock;
        });
    }
   
    // Run when DOM is ready
    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', addDatePicker);
    } else {
        addDatePicker();
    }
})();
 
/**
  * TimeAgo — shows "X blocks ago" for transcluded templates.
  *
  *
  * Usage in wikitext:
  * Layout:
  *  <span class="timeago" data-lastmod-page="Template:NewsShorts"></span>
  *  - corner logo (absolute-positioned upper-left of viewport,
*    OUTSIDE all wiki wrappers — does not push content down)
*  - non-sticky topbar (just nav) at top of body
*  - hero block with title (top), then USER:Username kicker below,
*    optional dek + meta
*  - light/dark mode toggle (persisted in localStorage)
  *
  *
  * Fetches the last revision timestamp from the API and renders
  * Variant from HTML comment marker `pickipedia:variant=letter|masthead`
  * a block-based relative time string using Ethereum block heights.
  * or category membership; default letter.
  */
  * ============================================================ */
(function() {
 
    'use strict';
(function () {
  'use strict';
 
  if (typeof mw === 'undefined' || !mw.config) return;
 
  var nsNumber = mw.config.get('wgNamespaceNumber');
  var pageName = mw.config.get('wgPageName') || '';
  var title    = mw.config.get('wgTitle') || '';
  var action  = mw.config.get('wgAction') || 'view';
 
  if (nsNumber !== 2) return;
  if (action !== 'view') return;
  if (title.indexOf('/') === -1) return;
 
  var slash    = title.indexOf('/');
  var username = title.slice(0, slash).replace(/_/g, ' ');
  var subpage  = title.slice(slash + 1).replace(/_/g, ' ');
 
  var body = document.body;
  body.classList.add('pickipedia-userpost');
 
  var variant = 'letter';
  var content = document.querySelector('.mw-parser-output');
  if (content) {
    var html = content.innerHTML || '';
    var m = html.match(/pickipedia:variant\s*=\s*(letter|masthead)/i);
    if (m) variant = m[1].toLowerCase();
  }
  var cats = mw.config.get('wgCategories') || [];
  if (cats.indexOf('Masthead style') !== -1) variant = 'masthead';
  if (cats.indexOf('Letter style') !== -1) variant = 'letter';
  body.classList.add('pickipedia-variant-' + variant);
 
  var STORAGE_KEY = 'pickipedia-mode';
  var saved = null;
  try { saved = localStorage.getItem(STORAGE_KEY); } catch (e) {}
  if (saved === 'dark') body.classList.add('pickipedia-dark');
 
  var LOGO_URL = '/images/thumb/8/80/Pickipedia-quarter-transparent.png/400px-Pickipedia-quarter-transparent.png';


    // Post-merge average block time is ~12.12 seconds
  // ---------- Corner logo: stand-alone body child, absolute-positioned. ----------
    var AVG_BLOCK_TIME = 12.12;
  var cornerLogo = document.createElement('a');
  cornerLogo.className = 'pp-corner-logo';
  cornerLogo.href = '/wiki/Main_Page';
  cornerLogo.title = 'PickiPedia home';
  cornerLogo.innerHTML = '<img src="' + LOGO_URL + '" alt="PickiPedia">';
  document.body.insertBefore(cornerLogo, document.body.firstChild);


    var spans = document.querySelectorAll('.timeago[data-lastmod-page]');
  // ---------- Topbar (nav only) ----------
    if (!spans.length) return;
  var topbar = document.createElement('header');
  topbar.className = 'pickipedia-topbar';
  topbar.innerHTML =
    '<nav class="pickipedia-topbar-nav">' +
      '<a href="' + mw.util.getUrl(pageName, { action: 'edit' }) + '">edit</a>' +
      '<a href="' + mw.util.getUrl(pageName, { action: 'history' }) + '">history</a>' +
      '<a href="' + mw.util.getUrl('Talk:' + pageName) + '">talk</a>' +
    '</nav>';
  document.body.insertBefore(topbar, cornerLogo.nextSibling);


    // Format number with commas (e.g., 1234567 -> "1,234,567")
  // ---------- Hero ----------
     function formatNumber(num) {
  if (content) {
        return num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',');
    var dek = '';
    var dekEl = content.querySelector('.pp-dek-source');
     if (dekEl) {
      dek = dekEl.innerHTML;
      dekEl.remove();
     }
     }


    // Collect unique page titles
     var kickerHtml = '<a href="' + mw.util.getUrl('User:' + username.replace(/ /g, '_')) +
     var titles = [];
                    '">User:' + escapeHtml(username) + '</a>';
    spans.forEach(function(span) {
    var kickerEl = content.querySelector('.pp-kicker-source');
        var t = span.getAttribute('data-lastmod-page');
    if (kickerEl) {
        if (t && titles.indexOf(t) === -1) titles.push(t);
      kickerHtml = escapeHtml(kickerEl.textContent.trim());
     });
      kickerEl.remove();
     }


     // Batch query the API (up to 50 titles per request)
     var lastMod = '';
     var api = mw.config.get('wgScriptPath') + '/api.php';
     var lastModEl = document.getElementById('footer-info-lastmod');
     var url = api + '?action=query&prop=revisions&rvprop=timestamp&format=json&titles=' +
     if (lastModEl) lastMod = lastModEl.textContent.replace(/^\s*This page was last (edited|modified) on\s*/i, '').trim();
        encodeURIComponent(titles.join('|'));


     fetch(url).then(function(r) { return r.json(); }).then(function(data) {
     var hero = document.createElement('header');
        var pages = data.query && data.query.pages || {};
    hero.className = 'pickipedia-hero';
        var timestamps = {};


        Object.keys(pages).forEach(function(id) {
    if (variant === 'masthead') {
            var page = pages[id];
      hero.innerHTML =
            if (page.revisions && page.revisions[0]) {
        '<div class="pp-topbar">' +
                timestamps[page.title] = new Date(page.revisions[0].timestamp);
          '<div class="pp-kicker">' + kickerHtml + '</div>' +
            }
          '<div class="pp-topbar-rule"></div>' +
        });
          (lastMod ? '<div class="pp-topbar-vol">' + escapeHtml(lastMod) + '</div>' : '') +
 
        '</div>' +
        spans.forEach(function(span) {
        '<div class="pp-titlewrap">' +
            var title = span.getAttribute('data-lastmod-page');
          '<h1 class="pp-title">' + escapeHtml(subpage) + '</h1>' +
            var ts = timestamps[title];
          (dek ? '<p class="pp-dek">' + dek + '</p>' : '') +
            if (!ts) {
        '</div>';
                span.textContent = '';
    } else {
                return;
      // Letter variant: title first, then kicker, then optional dek/meta.
            }
      hero.innerHTML =
            span.textContent = formatBlocksAgo(ts);
        '<h1 class="pp-title">' + escapeHtml(subpage) + '</h1>' +
            span.title = ts.toLocaleString();
        '<div class="pp-kicker">' + kickerHtml + '</div>' +
         });
        (dek ? '<p class="pp-dek">' + dek + '</p>' : '') +
     });
         (lastMod ? '<div class="pp-meta">Last edited ' + escapeHtml(lastMod) + '</div>' : '');
     }
    content.parentNode.insertBefore(hero, content);
  }


    function formatBlocksAgo(date) {
  // ---------- Light/dark toggle ----------
        var secondsAgo = Math.floor((Date.now() - date.getTime()) / 1000);
  var toggle = document.createElement('button');
        var blocksAgo = Math.round(secondsAgo / AVG_BLOCK_TIME);
  toggle.className = 'pickipedia-mode';
  toggle.type = 'button';
  toggle.textContent = body.classList.contains('pickipedia-dark') ? '☼ Light' : '☾ Dark';
  toggle.addEventListener('click', function () {
    var dark = body.classList.toggle('pickipedia-dark');
    toggle.textContent = dark ? '☼ Light' : '☾ Dark';
    try { localStorage.setItem(STORAGE_KEY, dark ? 'dark' : 'light'); } catch (e) {}
  });
  document.body.appendChild(toggle);


        if (blocksAgo < 1) {
  function escapeHtml(s) {
            return 'this block';
    return String(s == null ? '' : s)
        } else if (blocksAgo === 1) {
      .replace(/&/g, '&amp;')
            return '1 block ago';
      .replace(/</g, '&lt;')
        } else {
      .replace(/>/g, '&gt;')
            return formatNumber(blocksAgo) + ' blocks ago';
      .replace(/"/g, '&quot;');
        }
     }
     }
})();
}());

Latest revision as of 18:55, 9 October 2026

/**
 * What used to be here now ships with the PickiPediaReleases extension.
 *
 *   the HLS video player   -> ext.pickipediaReleases.videoPlayer.js    (#124)
 *   Blue Railroad date picker -> ext.pickipediaReleases.blueRailroadDate.js (#131)
 *   "X blocks ago"         -> ext.pickipediaReleases.blocksAgo.js      (#131)
 *   Instrument form checks -> ext.pickipediaReleases.instrumentForm.js (#131)
 *
 * All four load on every page from the extension's BeforePageDisplay hook,
 * exactly as they did from here, and each does nothing unless the form or
 * element it cares about is on the page being shown. Changing any of them is
 * a pull request now: reviewed, syntax-checked by CI, deployed by Jenkins —
 * rather than several hundred lines pasted into this box, which is how the
 * whole of this file went down for 23 minutes on 15 September 2026.
 *
 * What remains below is the user-subpage zine styling. It stays because it is
 * bound to classes defined in MediaWiki:Common.css, and moving the script
 * without its stylesheet would split one feature across two homes. It goes
 * next, with the CSS, after which this page can be empty.
 *
 * See cryptograss/pickipedia#124 and #131.
 */

/* ============================================================
 * Pickipedia — User-subpage zine/Substack styling
 *
 * Layout:
 *   - corner logo (absolute-positioned upper-left of viewport,
 *     OUTSIDE all wiki wrappers — does not push content down)
 *   - non-sticky topbar (just nav) at top of body
 *   - hero block with title (top), then USER:Username kicker below,
 *     optional dek + meta
 *   - light/dark mode toggle (persisted in localStorage)
 *
 * Variant from HTML comment marker `pickipedia:variant=letter|masthead`
 * or category membership; default letter.
 * ============================================================ */

(function () {
  'use strict';

  if (typeof mw === 'undefined' || !mw.config) return;

  var nsNumber = mw.config.get('wgNamespaceNumber');
  var pageName = mw.config.get('wgPageName') || '';
  var title    = mw.config.get('wgTitle') || '';
  var action   = mw.config.get('wgAction') || 'view';

  if (nsNumber !== 2) return;
  if (action !== 'view') return;
  if (title.indexOf('/') === -1) return;

  var slash    = title.indexOf('/');
  var username = title.slice(0, slash).replace(/_/g, ' ');
  var subpage  = title.slice(slash + 1).replace(/_/g, ' ');

  var body = document.body;
  body.classList.add('pickipedia-userpost');

  var variant = 'letter';
  var content = document.querySelector('.mw-parser-output');
  if (content) {
    var html = content.innerHTML || '';
    var m = html.match(/pickipedia:variant\s*=\s*(letter|masthead)/i);
    if (m) variant = m[1].toLowerCase();
  }
  var cats = mw.config.get('wgCategories') || [];
  if (cats.indexOf('Masthead style') !== -1) variant = 'masthead';
  if (cats.indexOf('Letter style') !== -1) variant = 'letter';
  body.classList.add('pickipedia-variant-' + variant);

  var STORAGE_KEY = 'pickipedia-mode';
  var saved = null;
  try { saved = localStorage.getItem(STORAGE_KEY); } catch (e) {}
  if (saved === 'dark') body.classList.add('pickipedia-dark');

  var LOGO_URL = '/images/thumb/8/80/Pickipedia-quarter-transparent.png/400px-Pickipedia-quarter-transparent.png';

  // ---------- Corner logo: stand-alone body child, absolute-positioned. ----------
  var cornerLogo = document.createElement('a');
  cornerLogo.className = 'pp-corner-logo';
  cornerLogo.href = '/wiki/Main_Page';
  cornerLogo.title = 'PickiPedia home';
  cornerLogo.innerHTML = '<img src="' + LOGO_URL + '" alt="PickiPedia">';
  document.body.insertBefore(cornerLogo, document.body.firstChild);

  // ---------- Topbar (nav only) ----------
  var topbar = document.createElement('header');
  topbar.className = 'pickipedia-topbar';
  topbar.innerHTML =
    '<nav class="pickipedia-topbar-nav">' +
      '<a href="' + mw.util.getUrl(pageName, { action: 'edit' }) + '">edit</a>' +
      '<a href="' + mw.util.getUrl(pageName, { action: 'history' }) + '">history</a>' +
      '<a href="' + mw.util.getUrl('Talk:' + pageName) + '">talk</a>' +
    '</nav>';
  document.body.insertBefore(topbar, cornerLogo.nextSibling);

  // ---------- Hero ----------
  if (content) {
    var dek = '';
    var dekEl = content.querySelector('.pp-dek-source');
    if (dekEl) {
      dek = dekEl.innerHTML;
      dekEl.remove();
    }

    var kickerHtml = '<a href="' + mw.util.getUrl('User:' + username.replace(/ /g, '_')) +
                     '">User:' + escapeHtml(username) + '</a>';
    var kickerEl = content.querySelector('.pp-kicker-source');
    if (kickerEl) {
      kickerHtml = escapeHtml(kickerEl.textContent.trim());
      kickerEl.remove();
    }

    var lastMod = '';
    var lastModEl = document.getElementById('footer-info-lastmod');
    if (lastModEl) lastMod = lastModEl.textContent.replace(/^\s*This page was last (edited|modified) on\s*/i, '').trim();

    var hero = document.createElement('header');
    hero.className = 'pickipedia-hero';

    if (variant === 'masthead') {
      hero.innerHTML =
        '<div class="pp-topbar">' +
          '<div class="pp-kicker">' + kickerHtml + '</div>' +
          '<div class="pp-topbar-rule"></div>' +
          (lastMod ? '<div class="pp-topbar-vol">' + escapeHtml(lastMod) + '</div>' : '') +
        '</div>' +
        '<div class="pp-titlewrap">' +
          '<h1 class="pp-title">' + escapeHtml(subpage) + '</h1>' +
          (dek ? '<p class="pp-dek">' + dek + '</p>' : '') +
        '</div>';
    } else {
      // Letter variant: title first, then kicker, then optional dek/meta.
      hero.innerHTML =
        '<h1 class="pp-title">' + escapeHtml(subpage) + '</h1>' +
        '<div class="pp-kicker">' + kickerHtml + '</div>' +
        (dek ? '<p class="pp-dek">' + dek + '</p>' : '') +
        (lastMod ? '<div class="pp-meta">Last edited ' + escapeHtml(lastMod) + '</div>' : '');
    }
    content.parentNode.insertBefore(hero, content);
  }

  // ---------- Light/dark toggle ----------
  var toggle = document.createElement('button');
  toggle.className = 'pickipedia-mode';
  toggle.type = 'button';
  toggle.textContent = body.classList.contains('pickipedia-dark') ? '☼ Light' : '☾ Dark';
  toggle.addEventListener('click', function () {
    var dark = body.classList.toggle('pickipedia-dark');
    toggle.textContent = dark ? '☼ Light' : '☾ Dark';
    try { localStorage.setItem(STORAGE_KEY, dark ? 'dark' : 'light'); } catch (e) {}
  });
  document.body.appendChild(toggle);

  function escapeHtml(s) {
    return String(s == null ? '' : s)
      .replace(/&/g, '&amp;')
      .replace(/</g, '&lt;')
      .replace(/>/g, '&gt;')
      .replace(/"/g, '&quot;');
    }
}());