Jump to content

MediaWiki:Common.js: Difference between revisions

From Fedoverse Wiki, the free encyclopedia
No edit summary
No edit summary
Line 116: Line 116:
   load();
   load();
   setInterval(load, 60000);
   setInterval(load, 60000);
})();
(function () {
  var root = document.getElementById('pc-podcast');
  if (!root) return;
  var feed = root.dataset.feed, page = root.dataset.page;
  var deck = root.querySelector('.pc-deck'), nav = root.querySelector('.pc-nav');
  var cards = [], dots = [], active = 0;
  function el(tag, cls, text) {
    var e = document.createElement(tag);
    if (cls) e.className = cls;
    if (text) e.textContent = text;
    return e;
  }
  function safeUrl(u) { return /^https?:\/\//i.test(u || '') ? u : ''; }
  function txt(node, tag) { var n = node.getElementsByTagName(tag)[0]; return n ? n.textContent.trim() : ''; }
  function plain(html) {
    return new DOMParser().parseFromString(html || '', 'text/html').body.textContent.replace(/\s+/g, ' ').trim();
  }
  function parseDate(s) {
    var d = new Date(s);
    if (isNaN(d)) d = new Date(String(s).replace(' ', 'T') + 'Z');
    return d;
  }
  function fmtDate(s) {
    var d = parseDate(s);
    return isNaN(d) ? '' : d.toLocaleDateString(undefined, { month: 'short', day: 'numeric', year: 'numeric' });
  }
  function fmtDur(v) {
    if (!v) return '';
    if (v.indexOf(':') > -1) return v.replace(/^00:/, '').replace(/^0(\d:)/, '$1');
    var s = parseInt(v, 10);
    if (!s) return '';
    var h = Math.floor(s / 3600), m = Math.floor(s % 3600 / 60), sec = s % 60;
    return (h ? h + ':' + ('0' + m).slice(-2) : m) + ':' + ('0' + sec).slice(-2);
  }
  // direct RSS (has season / episode / duration)
  function fromXml(xml) {
    var doc = new DOMParser().parseFromString(xml, 'application/xml');
    var chImg = doc.getElementsByTagName('itunes:image')[0];
    var chCover = chImg ? chImg.getAttribute('href') : '';
    return Array.prototype.map.call(doc.getElementsByTagName('item'), function (it) {
      var img = it.getElementsByTagName('itunes:image')[0];
      var s = txt(it, 'itunes:season'), e = txt(it, 'itunes:episode');
      return {
        title: txt(it, 'title'), link: txt(it, 'link'), date: txt(it, 'pubDate'),
        dur: fmtDur(txt(it, 'itunes:duration')),
        tag: e ? (s ? 'S' + s + ' · ' : '') + 'E' + e : '',
        desc: plain(txt(it, 'description')),
        cover: (img && img.getAttribute('href')) || chCover
      };
    });
  }
  // fallback through a public RSS-to-JSON proxy if the feed blocks cross-site requests
  function fromJson(j) {
    var chCover = j.feed && j.feed.image;
    return (j.items || []).map(function (it) {
      return { title: it.title, link: it.link, date: it.pubDate, dur: '', tag: '',
              desc: plain(it.description), cover: it.thumbnail || chCover };
    });
  }
  function buildCard(ep) {
    var c = el('article', 'pc-card');
    var cover = el('div', 'pc-cover');
    if (safeUrl(ep.cover)) {
      var img = el('img'); img.alt = ''; img.loading = 'lazy';
      img.onerror = function () { img.remove(); cover.textContent = '🎙️'; };
      img.src = ep.cover; cover.appendChild(img);
    } else cover.textContent = '🎙️';
    var body = el('div', 'pc-body');
    var meta = el('div', 'pc-meta');
    if (ep.tag) meta.appendChild(el('span', 'pc-pill pc-pill-strong', ep.tag));
    var ds = fmtDate(ep.date); if (ds) meta.appendChild(el('span', 'pc-pill', ds));
    if (ep.dur) meta.appendChild(el('span', 'pc-pill', ep.dur));
    if (meta.children.length) body.appendChild(meta);
    var href = safeUrl(ep.link) || safeUrl(page);
    var title = el('div', 'pc-title'); title.setAttribute('role', 'heading'); title.setAttribute('aria-level', '3');
    var a = el('a', '', ep.title || 'Untitled episode');
    if (href) { a.href = href; a.target = '_blank'; a.rel = 'noopener'; }
    title.appendChild(a); body.appendChild(title);
    body.appendChild(el('p', 'pc-desc', ep.desc));
    c.appendChild(cover); c.appendChild(body);
    if (href) {
      var o = el('div', 'mc-open'), oa = el('a');
      oa.href = href; oa.target = '_blank'; oa.rel = 'noopener';
      oa.title = 'Open episode'; oa.setAttribute('aria-label', 'Open episode');
      oa.appendChild(el('span', 'mc-open-icon')); o.appendChild(oa); c.appendChild(o);
    }
    return c;
  }
  function layout() {
    var n = cards.length;
    cards.forEach(function (c, i) {
      var pos = (i - active + n) % n;
      c.dataset.pos = pos;
      c.inert = pos !== 0;
      c.setAttribute('aria-hidden', pos ? 'true' : 'false');
    });
    dots.forEach(function (d, i) { d.setAttribute('aria-current', i === active ? 'true' : 'false'); });
  }
  function go(delta) { active = (active + delta + cards.length) % cards.length; layout(); }
  function arrow(label, path, delta) {
    var b = el('button', 'pc-btn'); b.type = 'button'; b.setAttribute('aria-label', label);
    b.innerHTML = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="' + path + '"/></svg>';
    b.addEventListener('click', function () { go(delta); });
    return b;
  }
  function render(list) {
    if (!list || !list.length) return;              // nothing found → stays an empty box
    list = list.filter(function (e) { return e.title; })
      .sort(function (a, b) { return parseDate(b.date) - parseDate(a.date); }).slice(0, 3);
    if (!list.length) return;
    list.forEach(function (ep) { cards.push(buildCard(ep)); deck.appendChild(cards[cards.length - 1]); });
    for (var k = cards.length; k < 3; k++) {        // empty layers so the stack always looks like 3 cards
      var g = el('div', 'pc-card pc-ghost'); g.dataset.pos = k; g.setAttribute('aria-hidden', 'true');
      deck.appendChild(g);
    }
    if (cards.length > 1) {
      var wrap = el('div', 'pc-dots');
      cards.forEach(function (_, i) {
        var d = el('button', 'pc-dot'); d.type = 'button'; d.setAttribute('aria-label', 'Episode ' + (i + 1));
        d.addEventListener('click', function () { active = i; layout(); });
        dots.push(d); wrap.appendChild(d);
      });
      nav.appendChild(arrow('Previous episode', 'm15 18-6-6 6-6', -1));
      nav.appendChild(wrap);
      nav.appendChild(arrow('Next episode', 'm9 18 6-6-6-6', 1));
      var sx = null;                                // swipe support
      deck.addEventListener('pointerdown', function (e) { sx = e.clientX; });
      deck.addEventListener('pointerup', function (e) {
        if (sx === null) return;
        var dx = e.clientX - sx; sx = null;
        if (Math.abs(dx) > 45) go(dx < 0 ? 1 : -1);
      });
    }
    layout();
  }
  fetch(feed)
    .then(function (r) { if (!r.ok) throw new Error('feed'); return r.text(); })
    .then(fromXml)
    .catch(function () {
      return fetch('https://api.rss2json.com/v1/api.json?rss_url=' + encodeURIComponent(feed))
        .then(function (r) { return r.json(); }).then(fromJson);
    })
    .then(render)
    .catch(function () { /* leave the empty box */ });
})();
})();

Revision as of 16:30, 28 September 2026

/* Any JavaScript here will be loaded for all users on every page load. */

mw.hook('wikipage.content').add(function () {
    const logo = document.querySelector('a.mw-logo');
    if (logo) {
        logo.setAttribute('href', 'https://fedoverse.com');
    }
});

mw.hook('wikipage.content').add(function () {
    const button = document.querySelector('.your-button-class');
    if (!button) return;

    const premiumEase = 'cubic-bezier(0.23, 1, 0.32, 1)';

    button.addEventListener('mouseenter', () => {
        button.style.transition = `transform 0.15s ${premiumEase}`;
    });

    button.addEventListener('mousemove', (e) => {
        const rect = button.getBoundingClientRect();
        const x = (e.clientX - rect.left - rect.width / 2) / (rect.width / 2);
        const y = (e.clientY - rect.top - rect.height / 2) / (rect.height / 2);
        button.style.transform = `perspective(400px) scale(1.05) rotateX(${y * -15}deg) rotateY(${x * 15}deg)`;
    });

    button.addEventListener('mouseleave', () => {
        button.style.transition = `transform 0.65s ${premiumEase}`;
        // Force the browser to commit the 0.65s transition before updating the transform
        void button.offsetHeight;
        button.style.transform = 'perspective(400px) scale(1) rotateX(0deg) rotateY(0deg)';
    });
});

/* Open top search autocomplete result on Enter in Vector 2022 */
(function () {
    window.addEventListener('keydown', function (e) {
        if (e.key !== 'Enter') return;

        const activeEl = document.activeElement;
        // Check if user is typing in a search box
        if (!activeEl || !activeEl.matches('input[type="search"], .cdx-text-input__input')) return;

        // 1. Locate the first visible suggestion item in Codex / Vector 2022
        const firstMenuItem = document.querySelector(
            '.cdx-typeahead-search__menu .cdx-menu-item:not(.cdx-menu-item--disabled), ' +
            '.cdx-menu__listbox .cdx-menu-item:not(.cdx-menu-item--disabled)'
        );

        if (!firstMenuItem) return;

        // 2. Extract destination URL: either from an anchor or from its data/ID/query attributes
        let targetUrl = null;
        const anchor = firstMenuItem.querySelector('a') || (firstMenuItem.tagName === 'A' ? firstMenuItem : null);

        if (anchor && anchor.getAttribute('href')) {
            targetUrl = anchor.getAttribute('href');
        } else {
            // Codex frequently stores the target title in the element's text or label
            const labelEl = firstMenuItem.querySelector('.cdx-search-result-title, .cdx-menu-item__text, .cdx-menu-item__content');
            const title = (labelEl || firstMenuItem).textContent.trim();
            if (title) {
                targetUrl = mw.util.getUrl(title);
            }
        }

        // 3. Prevent standard search submission and redirect immediately
        if (targetUrl) {
            e.preventDefault();
            e.stopPropagation();
            e.stopImmediatePropagation();
            window.location.href = targetUrl;
        }
    }, true); // 'true' captures the event before Vue or native form submit triggers
})();

(function () {
  var box = document.getElementById('mc-status');
  if (!box) return;
  var host = box.dataset.host;
  var q = function (s) { return box.querySelector(s); };
  var logo = q('.mc-icon');
  var customLogo = !!logo.firstElementChild;
  var lastSrc = '';

  function copy() { if (navigator.clipboard) navigator.clipboard.writeText(host); }
  q('.mc-main').addEventListener('click', copy);
  q('.mc-main').addEventListener('keydown', function (e) { if (e.key === 'Enter') copy(); });

  function setIcon(src) {
    if (customLogo || src === lastSrc) return;
    lastSrc = src;
    logo.textContent = '';
    var img = document.createElement('img');
    img.alt = '';
    img.onerror = function () { logo.textContent = '⛏️'; };
    img.src = src;
    logo.appendChild(img);
  }

  function load() {
    fetch('https://api.mcsrvstat.us/3/' + encodeURIComponent(host))
      .then(function (r) { return r.json(); })
      .then(function (d) {
        box.classList.toggle('is-online', !!d.online);
        box.classList.toggle('is-offline', !d.online);
        q('.mc-state').textContent = d.online ? 'Online' : 'Offline';
        q('.mc-players').textContent = d.players ? d.players.online + ' / ' + d.players.max : '– / –';
        q('.mc-players').title = d.players && d.players.list
          ? d.players.list.map(function (p) { return p.name; }).join(', ') : '';
        q('.mc-motd').textContent = d.online && d.motd ? d.motd.clean.join(' ').trim() : '';
        setIcon(d.icon || 'https://api.mcsrvstat.us/icon/' + host);
      })
      .catch(function () { q('.mc-state').textContent = 'Status unavailable'; });
  }
  load();
  setInterval(load, 60000);
})();


(function () {
  var root = document.getElementById('pc-podcast');
  if (!root) return;
  var feed = root.dataset.feed, page = root.dataset.page;
  var deck = root.querySelector('.pc-deck'), nav = root.querySelector('.pc-nav');
  var cards = [], dots = [], active = 0;

  function el(tag, cls, text) {
    var e = document.createElement(tag);
    if (cls) e.className = cls;
    if (text) e.textContent = text;
    return e;
  }
  function safeUrl(u) { return /^https?:\/\//i.test(u || '') ? u : ''; }
  function txt(node, tag) { var n = node.getElementsByTagName(tag)[0]; return n ? n.textContent.trim() : ''; }
  function plain(html) {
    return new DOMParser().parseFromString(html || '', 'text/html').body.textContent.replace(/\s+/g, ' ').trim();
  }
  function parseDate(s) {
    var d = new Date(s);
    if (isNaN(d)) d = new Date(String(s).replace(' ', 'T') + 'Z');
    return d;
  }
  function fmtDate(s) {
    var d = parseDate(s);
    return isNaN(d) ? '' : d.toLocaleDateString(undefined, { month: 'short', day: 'numeric', year: 'numeric' });
  }
  function fmtDur(v) {
    if (!v) return '';
    if (v.indexOf(':') > -1) return v.replace(/^00:/, '').replace(/^0(\d:)/, '$1');
    var s = parseInt(v, 10);
    if (!s) return '';
    var h = Math.floor(s / 3600), m = Math.floor(s % 3600 / 60), sec = s % 60;
    return (h ? h + ':' + ('0' + m).slice(-2) : m) + ':' + ('0' + sec).slice(-2);
  }

  // direct RSS (has season / episode / duration)
  function fromXml(xml) {
    var doc = new DOMParser().parseFromString(xml, 'application/xml');
    var chImg = doc.getElementsByTagName('itunes:image')[0];
    var chCover = chImg ? chImg.getAttribute('href') : '';
    return Array.prototype.map.call(doc.getElementsByTagName('item'), function (it) {
      var img = it.getElementsByTagName('itunes:image')[0];
      var s = txt(it, 'itunes:season'), e = txt(it, 'itunes:episode');
      return {
        title: txt(it, 'title'), link: txt(it, 'link'), date: txt(it, 'pubDate'),
        dur: fmtDur(txt(it, 'itunes:duration')),
        tag: e ? (s ? 'S' + s + ' · ' : '') + 'E' + e : '',
        desc: plain(txt(it, 'description')),
        cover: (img && img.getAttribute('href')) || chCover
      };
    });
  }
  // fallback through a public RSS-to-JSON proxy if the feed blocks cross-site requests
  function fromJson(j) {
    var chCover = j.feed && j.feed.image;
    return (j.items || []).map(function (it) {
      return { title: it.title, link: it.link, date: it.pubDate, dur: '', tag: '',
               desc: plain(it.description), cover: it.thumbnail || chCover };
    });
  }

  function buildCard(ep) {
    var c = el('article', 'pc-card');
    var cover = el('div', 'pc-cover');
    if (safeUrl(ep.cover)) {
      var img = el('img'); img.alt = ''; img.loading = 'lazy';
      img.onerror = function () { img.remove(); cover.textContent = '🎙️'; };
      img.src = ep.cover; cover.appendChild(img);
    } else cover.textContent = '🎙️';

    var body = el('div', 'pc-body');
    var meta = el('div', 'pc-meta');
    if (ep.tag) meta.appendChild(el('span', 'pc-pill pc-pill-strong', ep.tag));
    var ds = fmtDate(ep.date); if (ds) meta.appendChild(el('span', 'pc-pill', ds));
    if (ep.dur) meta.appendChild(el('span', 'pc-pill', ep.dur));
    if (meta.children.length) body.appendChild(meta);

    var href = safeUrl(ep.link) || safeUrl(page);
    var title = el('div', 'pc-title'); title.setAttribute('role', 'heading'); title.setAttribute('aria-level', '3');
    var a = el('a', '', ep.title || 'Untitled episode');
    if (href) { a.href = href; a.target = '_blank'; a.rel = 'noopener'; }
    title.appendChild(a); body.appendChild(title);
    body.appendChild(el('p', 'pc-desc', ep.desc));

    c.appendChild(cover); c.appendChild(body);
    if (href) {
      var o = el('div', 'mc-open'), oa = el('a');
      oa.href = href; oa.target = '_blank'; oa.rel = 'noopener';
      oa.title = 'Open episode'; oa.setAttribute('aria-label', 'Open episode');
      oa.appendChild(el('span', 'mc-open-icon')); o.appendChild(oa); c.appendChild(o);
    }
    return c;
  }

  function layout() {
    var n = cards.length;
    cards.forEach(function (c, i) {
      var pos = (i - active + n) % n;
      c.dataset.pos = pos;
      c.inert = pos !== 0;
      c.setAttribute('aria-hidden', pos ? 'true' : 'false');
    });
    dots.forEach(function (d, i) { d.setAttribute('aria-current', i === active ? 'true' : 'false'); });
  }
  function go(delta) { active = (active + delta + cards.length) % cards.length; layout(); }

  function arrow(label, path, delta) {
    var b = el('button', 'pc-btn'); b.type = 'button'; b.setAttribute('aria-label', label);
    b.innerHTML = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="' + path + '"/></svg>';
    b.addEventListener('click', function () { go(delta); });
    return b;
  }

  function render(list) {
    if (!list || !list.length) return;               // nothing found → stays an empty box
    list = list.filter(function (e) { return e.title; })
      .sort(function (a, b) { return parseDate(b.date) - parseDate(a.date); }).slice(0, 3);
    if (!list.length) return;

    list.forEach(function (ep) { cards.push(buildCard(ep)); deck.appendChild(cards[cards.length - 1]); });
    for (var k = cards.length; k < 3; k++) {         // empty layers so the stack always looks like 3 cards
      var g = el('div', 'pc-card pc-ghost'); g.dataset.pos = k; g.setAttribute('aria-hidden', 'true');
      deck.appendChild(g);
    }
    if (cards.length > 1) {
      var wrap = el('div', 'pc-dots');
      cards.forEach(function (_, i) {
        var d = el('button', 'pc-dot'); d.type = 'button'; d.setAttribute('aria-label', 'Episode ' + (i + 1));
        d.addEventListener('click', function () { active = i; layout(); });
        dots.push(d); wrap.appendChild(d);
      });
      nav.appendChild(arrow('Previous episode', 'm15 18-6-6 6-6', -1));
      nav.appendChild(wrap);
      nav.appendChild(arrow('Next episode', 'm9 18 6-6-6-6', 1));

      var sx = null;                                 // swipe support
      deck.addEventListener('pointerdown', function (e) { sx = e.clientX; });
      deck.addEventListener('pointerup', function (e) {
        if (sx === null) return;
        var dx = e.clientX - sx; sx = null;
        if (Math.abs(dx) > 45) go(dx < 0 ? 1 : -1);
      });
    }
    layout();
  }

  fetch(feed)
    .then(function (r) { if (!r.ok) throw new Error('feed'); return r.text(); })
    .then(fromXml)
    .catch(function () {
      return fetch('https://api.rss2json.com/v1/api.json?rss_url=' + encodeURIComponent(feed))
        .then(function (r) { return r.json(); }).then(fromJson);
    })
    .then(render)
    .catch(function () { /* leave the empty box */ });
})();