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: Difference between revisions

MediaWiki interface page
No edit summary
No edit summary
 
(24 intermediate revisions by the same user not shown)
Line 1: Line 1:
/* Any JavaScript here will be loaded for all users on every page load. */// Load DataTables library
/* Any JavaScript here will be loaded for all users on every page load. */


mw.hook('wikipage.content').add(function ($content) {
mw.hook('wikipage.content').add(function ($content) {
Line 7: Line 7:
});
});


// Auto-embed X/Twitter status links into live tweets (no extension required)
(function () {
  'use strict';


   // Options you can tweak
mw.hook('wikipage.content').add(function () {
   var TW_OPTIONS = {
 
     // theme: 'dark',           // uncomment for dark
   /* ===========================
     // conversation: 'none',    // hide reply thread
    WAIT FOR TABLE (IMPORTANT)
     // dnt: true,               // "Do Not Track"
    =========================== */
     // align: 'center'           // center the embed
 
  };
   function waitForTable(callback) {
    const table = document.getElementById("leaderboardTable");
    if (table) {
      callback(table);
     } else {
      setTimeout(() => waitForTable(callback), 50);
    }
  }
 
 
  waitForTable(function(table) {
 
    /* ===========================
      HELPERS
      =========================== */
 
     function parseTime(t) {
      if (!t || !t.includes(":")) return 999999;
      const [m, s] = t.trim().split(":").map(Number);
      return m * 60 + s;
     }
 
     function getPoints(rank) {
      const scoring = {
        1:25,2:20,3:17,4:15,5:13,
        6:11,7:10,8:9,9:8,10:7,
        11:6,12:5,13:4,14:3,15:2
      };
      return scoring[rank] || 1;
    }
 
 
    /* ===========================
      FILTER UI
      =========================== */
 
    const filterWrapper = document.createElement("div");
    filterWrapper.style.marginBottom = "20px";
 
    const filterTitle = document.createElement("h3");
     filterTitle.innerText = "🔍 Filter Runs";
    filterWrapper.appendChild(filterTitle);
 
    const dungeonSelect = document.createElement("select");
    dungeonSelect.style.marginRight = "10px";
 
    [
      "All",
      "Wolf's Den","Wolf's Den II","Wolf's Den III",
      "The Red Nest","For The Eggs",
      "Scarlet Revenge","Scarlet Revenge II",
      "Promised One","Promised One II","Promised One III",
      "Lost and Found","The Hostage","Cactus On Fire",
      "Deep Echoes","Deep Echoes II","Deep Echoes III",
      "King's Hoard","King's Hoard II",
      "Hall of Daggers"
    ].forEach(name => {
      const opt = document.createElement("option");
      opt.value = name;
      opt.textContent = name;
      dungeonSelect.appendChild(opt);
    });
 
    filterWrapper.appendChild(document.createTextNode("Dungeon: "));
    filterWrapper.appendChild(dungeonSelect);
 
    const playerInput = document.createElement("input");
    playerInput.placeholder = "Search player...";
    playerInput.style.marginLeft = "10px";
    filterWrapper.appendChild(playerInput);
 
 
    /* ===========================
      SORT + RANK
      =========================== */
 
    function sortAndRank() {
 
      const rows = Array.from(table.querySelectorAll("tr")).slice(1);
 
      rows.sort((a,b) => {
 
        if (a.cells.length < 5 || b.cells.length < 5) return 0;
 
        const dA = a.cells[1].innerText.trim();
        const dB = b.cells[1].innerText.trim();
 
        if (dA !== dB) return dA.localeCompare(dB);
 
        return parseTime(a.cells[4].innerText) - parseTime(b.cells[4].innerText);
 
      });
 
      rows.forEach(r => table.appendChild(r));
 
      let currentDungeon = "";
      let position = 0;
      let rank = 0;
      let prevTime = null;
 
      rows.forEach(row => {
 
        if (row.cells.length < 5) return;


  function loadXWidgets (cb) {
        const dungeon = row.cells[1].innerText.trim();
    // If the widget API is ready, go
        const time = parseTime(row.cells[4].innerText);
    if (window.twttr && twttr.widgets) { cb(); return; }


    // Already loading?
        if (dungeon !== currentDungeon) {
    if (document.querySelector('script[src*="platform.twitter.com/widgets.js"]')) {
          currentDungeon = dungeon;
      // Poll until ready
          position = 1;
      var tries = 0, t = setInterval(function () {
          rank = 1;
        if (window.twttr && twttr.widgets) { clearInterval(t); cb(); }
          prevTime = time;
         else if (++tries > 40) { clearInterval(t); }
        } else {
       }, 250);
          position++;
      return;
          if (time !== prevTime) rank = position;
          prevTime = time;
        }
 
         row.cells[0].innerText = rank;
 
       });
     }
     }


    // Inject the widgets.js script
    var s = document.createElement('script');
    s.async = true;
    s.src = 'https://platform.twitter.com/widgets.js';
    s.onload = function () { cb(); };
    document.head.appendChild(s);
  }


  // Find eligible links and embed them
    /* ===========================
  function embedLinks(root) {
      BUILD TOP SECTION
    var scope = root || document;
      =========================== */
    var selector = [
 
       'a[href^="https://x.com/"][href*="/status/"]',
    function buildTopSection() {
       'a[href^="http://x.com/"][href*="/status/"]',
 
       'a[href^="https://twitter.com/"][href*="/status/"]',
       const rows = Array.from(table.querySelectorAll("tr")).slice(1);
       'a[href^="http://twitter.com/"][href*="/status/"]'
 
    ].join(',');
       const playerPoints = {};
       const records = {};
 
       rows.forEach(row => {
 
        if (row.cells.length < 5) return;


    var anchors = scope.querySelectorAll(selector);
        const dungeon = row.cells[1].innerText.trim();
    if (!anchors.length) return;
        const player = row.cells[2].innerText.trim();
        const time = row.cells[4].innerText.trim();
        const rank = parseInt(row.cells[0].innerText);


    loadXWidgets(function () {
        // Only require valid time (CRITICAL FIX)
      anchors.forEach(function (a) {
         if (!time || !time.includes(":")) return;
         if (a.dataset.tweetEmbedded === '1') return; // avoid duplicates
        a.dataset.tweetEmbedded = '1';


         // Extract the numeric tweet ID
         /* ===========================
        var match = a.href.match(/\/status\/(\d+)/);
          POINTS
        if (!match) return;
          =========================== */
        var id = match[1];


         // Create a container after the link
         if (!isNaN(rank)) {
        var container = document.createElement('div');
          playerPoints[player] = (playerPoints[player] || 0) + getPoints(rank);
         container.className = 'tweet-embed';
         }
        a.insertAdjacentElement('afterend', container);


         // Ask widgets API to render
         /* ===========================
         if (window.twttr && twttr.widgets && twttr.widgets.createTweet) {
          WORLD RECORDS (WORKING)
           twttr.widgets.createTweet(id, container, TW_OPTIONS).catch(function(){});
          =========================== */
 
        const seconds = parseTime(time);
 
         if (!records[dungeon]) {
           records[dungeon] = {
            time: time,
            seconds: seconds,
            players: [player]
          };
        } else {
          if (seconds < records[dungeon].seconds) {
            records[dungeon] = {
              time: time,
              seconds: seconds,
              players: [player]
            };
          } else if (seconds === records[dungeon].seconds) {
            records[dungeon].players.push(player);
          }
         }
         }
       });
       });


      // Ensure any dynamically added blockquotes also render
      if (window.twttr && twttr.widgets && twttr.widgets.load) {
        twttr.widgets.load(scope);
      }
    });
  }


  // Run on page load and whenever VE/LQT/etc. inject content
      /* REMOVE OLD */
  if (window.mw && mw.hook) {
      const existing = document.getElementById("topSection");
    mw.hook('wikipage.content').add(function ($content) {
      if (existing) existing.remove();
       embedLinks($content && $content[0]);
 
    });
 
  } else {
      /* FLEX CONTAINER */
    document.addEventListener('DOMContentLoaded', function () { embedLinks(document); });
      const top = document.createElement("div");
  }
      top.id = "topSection";
})();
      top.style.display = "flex";
      top.style.gap = "30px";
      top.style.marginBottom = "25px";
 
 
      /* ===========================
        COMPETITION TABLE
        =========================== */
 
      const compBox = document.createElement("div");
      compBox.style.flex = "1";
 
      const compTitle = document.createElement("h2");
      compTitle.innerText = "🏆 Competition Leaderboard";
      compBox.appendChild(compTitle);
 
       const compTable = document.createElement("table");
      compTable.className = "wikitable";
      compTable.style.width = "100%";
 
      compTable.innerHTML = `
        <tr>
          <th>Rank</th>
          <th>Player</th>
          <th>Points</th>
        </tr>
      `;
 
      const leaderboard = Object.entries(playerPoints)
        .map(([player, points]) => ({player, points}))
        .sort((a,b)=>b.points-a.points);
 
      let prevPoints = null;
      let rank = 0;
      let position = 0;
 
      leaderboard.forEach(entry => {


        position++;
        if (entry.points !== prevPoints) rank = position;
        prevPoints = entry.points;


/* ===========================
        const medal =
  Dungeon Speedrun Leaderboard
          rank===1?"🥇 1":
  Per-Dungeon Ranking + Filters
          rank===2?"🥈 2":
  =========================== */
          rank===3?"🥉 3":
          rank;


mw.hook('wikipage.content').add(function () {
        const row = document.createElement("tr");
        row.innerHTML = `
          <td>${medal}</td>
          <td>${entry.player}</td>
          <td>${entry.points}</td>
        `;
        compTable.appendChild(row);


  // Run only if table exists
      });
  const table = document.getElementById("leaderboardTable");
  if (!table) return;


  const container = document.getElementById("filterContainer");
      compBox.appendChild(compTable);
  if (!container) return;


  /* ---------- CREATE FILTER UI ---------- */


  const wrapper = document.createElement("div");
      /* ===========================
  wrapper.style.marginBottom = "15px";
        WORLD RECORD TABLE
        =========================== */


  // Dungeon dropdown
      const wrBox = document.createElement("div");
  const dungeonLabel = document.createElement("strong");
      wrBox.style.flex = "1";
  dungeonLabel.innerText = "Dungeon: ";
  wrapper.appendChild(dungeonLabel);


  const dungeonSelect = document.createElement("select");
      const wrTitle = document.createElement("h2");
  dungeonSelect.style.marginRight = "20px";
      wrTitle.innerText = "🏁 Competition Dungeon Records";
      wrBox.appendChild(wrTitle);


  const dungeons = [
      const wrTable = document.createElement("table");
    "All",
      wrTable.className = "wikitable";
    "Wolf's Den",
      wrTable.style.width = "100%";
    "Wolf's Den II",
    "Wolf's Den III",
    "The Red Nest",
    "For The Eggs",
    "Scarlet Revenge",
    "Scarlet Revenge II",
    "Promised One",
    "Promised One II",
    "Promised One III",
    "Lost and Found",
    "The Hostage",
    "Cactus On Fire",
    "Deep Echoes",
    "Deep Echoes II",
    "Deep Echoes III"
  ];


  dungeons.forEach(name => {
      wrTable.innerHTML = `
    const option = document.createElement("option");
        <tr>
    option.value = name;
          <th>Dungeon</th>
    option.textContent = name;
          <th>Time</th>
    dungeonSelect.appendChild(option);
          <th>Player(s)</th>
  });
        </tr>
      `;


  wrapper.appendChild(dungeonSelect);
      Object.entries(records).forEach(([d, data]) => {


  // Player search
        const row = document.createElement("tr");
  const playerLabel = document.createElement("strong");
  playerLabel.innerText = "Player: ";
  wrapper.appendChild(playerLabel);


  const playerInput = document.createElement("input");
        row.innerHTML = `
  playerInput.type = "text";
          <td>${d}</td>
  playerInput.placeholder = "Search player name...";
          <td>${data.time}</td>
  playerInput.style.padding = "4px";
          <td>${data.players.join(", ")}</td>
  wrapper.appendChild(playerInput);
        `;


  container.appendChild(wrapper);
        wrTable.appendChild(row);


  /* ---------- UTILITIES ---------- */
      });


  function parseTime(t) {
      wrBox.appendChild(wrTable);
    if (!t || !t.includes(":")) return 999999;
    const parts = t.trim().split(":");
    return parseInt(parts[0], 10) * 60 + parseInt(parts[1], 10);
  }


  function sortAndRank() {
    const rows = Array.from(table.querySelectorAll("tr")).slice(1);


    // Sort by Dungeon first, then Time
      /* COMBINE */
    rows.sort((a, b) => {
       top.appendChild(compBox);
       const dungeonA = a.cells[1].innerText.trim();
       top.appendChild(wrBox);
       const dungeonB = b.cells[1].innerText.trim();


       if (dungeonA < dungeonB) return -1;
       table.parentNode.insertBefore(top, table);
      if (dungeonA > dungeonB) return 1;


       return parseTime(a.cells[4].innerText) -
       /* FILTER BELOW */
            parseTime(b.cells[4].innerText);
      table.parentNode.insertBefore(filterWrapper, table);
     });
     }


    rows.forEach(row => table.appendChild(row));


     let currentDungeon = "";
     /* ===========================
    let dungeonRank = 0;
      FILTER
      =========================== */


     rows.forEach(row => {
     function filterTable() {
      if (row.style.display === "none") return;


       const dungeon = row.cells[1].innerText.trim();
       const dungeon = dungeonSelect.value;
       const rankCell = row.cells[0];
       const player = playerInput.value.toLowerCase();


       if (dungeon !== currentDungeon) {
       const rows = Array.from(table.querySelectorAll("tr")).slice(1);
        currentDungeon = dungeon;
        dungeonRank = 1;
      } else {
        dungeonRank++;
      }


       if (dungeonRank === 1) rankCell.innerHTML = "🥇 1";
       rows.forEach(row => {
      else if (dungeonRank === 2) rankCell.innerHTML = "🥈 2";
      else if (dungeonRank === 3) rankCell.innerHTML = "🥉 3";
      else rankCell.innerText = dungeonRank;
    });
  }


  function filterTable() {
        if (row.cells.length < 5) return;
    const selectedDungeon = dungeonSelect.value;
    const playerSearch = playerInput.value.toLowerCase();


    const rows = Array.from(table.querySelectorAll("tr")).slice(1);
        const d = row.cells[1].innerText.trim();
        const p = row.cells[2].innerText.toLowerCase();


    rows.forEach(row => {
        row.style.display =
      const dungeon = row.cells[1].innerText.trim();
          (dungeon === "All" || d === dungeon) &&
       const player = row.cells[2].innerText.toLowerCase();
          p.includes(player)
            ? ""
            : "none";
       });
    }


      const dungeonMatch =
    dungeonSelect.addEventListener("change", filterTable);
        selectedDungeon === "All" || dungeon === selectedDungeon;
    playerInput.addEventListener("input", filterTable);


      const playerMatch =
        player.includes(playerSearch);


      row.style.display =
    /* ===========================
        (dungeonMatch && playerMatch) ? "" : "none";
      INIT
    });
      =========================== */


     sortAndRank();
     sortAndRank();
  }
    buildTopSection();


   dungeonSelect.addEventListener("change", filterTable);
   });
  playerInput.addEventListener("input", filterTable);
 
  sortAndRank();


});
});

Latest revision as of 18:07, 14 May 2026

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

mw.hook('wikipage.content').add(function ($content) {
  $content.find('.spoiler').on('click', function () {
    $(this).toggleClass('revealed');
  });
});


mw.hook('wikipage.content').add(function () {

  /* ===========================
     WAIT FOR TABLE (IMPORTANT)
     =========================== */

  function waitForTable(callback) {
    const table = document.getElementById("leaderboardTable");
    if (table) {
      callback(table);
    } else {
      setTimeout(() => waitForTable(callback), 50);
    }
  }


  waitForTable(function(table) {

    /* ===========================
       HELPERS
       =========================== */

    function parseTime(t) {
      if (!t || !t.includes(":")) return 999999;
      const [m, s] = t.trim().split(":").map(Number);
      return m * 60 + s;
    }

    function getPoints(rank) {
      const scoring = {
        1:25,2:20,3:17,4:15,5:13,
        6:11,7:10,8:9,9:8,10:7,
        11:6,12:5,13:4,14:3,15:2
      };
      return scoring[rank] || 1;
    }


    /* ===========================
       FILTER UI
       =========================== */

    const filterWrapper = document.createElement("div");
    filterWrapper.style.marginBottom = "20px";

    const filterTitle = document.createElement("h3");
    filterTitle.innerText = "🔍 Filter Runs";
    filterWrapper.appendChild(filterTitle);

    const dungeonSelect = document.createElement("select");
    dungeonSelect.style.marginRight = "10px";

    [
      "All",
      "Wolf's Den","Wolf's Den II","Wolf's Den III",
      "The Red Nest","For The Eggs",
      "Scarlet Revenge","Scarlet Revenge II",
      "Promised One","Promised One II","Promised One III",
      "Lost and Found","The Hostage","Cactus On Fire",
      "Deep Echoes","Deep Echoes II","Deep Echoes III",
      "King's Hoard","King's Hoard II",
      "Hall of Daggers"
    ].forEach(name => {
      const opt = document.createElement("option");
      opt.value = name;
      opt.textContent = name;
      dungeonSelect.appendChild(opt);
    });

    filterWrapper.appendChild(document.createTextNode("Dungeon: "));
    filterWrapper.appendChild(dungeonSelect);

    const playerInput = document.createElement("input");
    playerInput.placeholder = "Search player...";
    playerInput.style.marginLeft = "10px";
    filterWrapper.appendChild(playerInput);


    /* ===========================
       SORT + RANK
       =========================== */

    function sortAndRank() {

      const rows = Array.from(table.querySelectorAll("tr")).slice(1);

      rows.sort((a,b) => {

        if (a.cells.length < 5 || b.cells.length < 5) return 0;

        const dA = a.cells[1].innerText.trim();
        const dB = b.cells[1].innerText.trim();

        if (dA !== dB) return dA.localeCompare(dB);

        return parseTime(a.cells[4].innerText) - parseTime(b.cells[4].innerText);

      });

      rows.forEach(r => table.appendChild(r));

      let currentDungeon = "";
      let position = 0;
      let rank = 0;
      let prevTime = null;

      rows.forEach(row => {

        if (row.cells.length < 5) return;

        const dungeon = row.cells[1].innerText.trim();
        const time = parseTime(row.cells[4].innerText);

        if (dungeon !== currentDungeon) {
          currentDungeon = dungeon;
          position = 1;
          rank = 1;
          prevTime = time;
        } else {
          position++;
          if (time !== prevTime) rank = position;
          prevTime = time;
        }

        row.cells[0].innerText = rank;

      });
    }


    /* ===========================
       BUILD TOP SECTION
       =========================== */

    function buildTopSection() {

      const rows = Array.from(table.querySelectorAll("tr")).slice(1);

      const playerPoints = {};
      const records = {};

      rows.forEach(row => {

        if (row.cells.length < 5) return;

        const dungeon = row.cells[1].innerText.trim();
        const player = row.cells[2].innerText.trim();
        const time = row.cells[4].innerText.trim();
        const rank = parseInt(row.cells[0].innerText);

        // Only require valid time (CRITICAL FIX)
        if (!time || !time.includes(":")) return;

        /* ===========================
           POINTS
           =========================== */

        if (!isNaN(rank)) {
          playerPoints[player] = (playerPoints[player] || 0) + getPoints(rank);
        }

        /* ===========================
           WORLD RECORDS (WORKING)
           =========================== */

        const seconds = parseTime(time);

        if (!records[dungeon]) {
          records[dungeon] = {
            time: time,
            seconds: seconds,
            players: [player]
          };
        } else {
          if (seconds < records[dungeon].seconds) {
            records[dungeon] = {
              time: time,
              seconds: seconds,
              players: [player]
            };
          } else if (seconds === records[dungeon].seconds) {
            records[dungeon].players.push(player);
          }
        }

      });


      /* REMOVE OLD */
      const existing = document.getElementById("topSection");
      if (existing) existing.remove();


      /* FLEX CONTAINER */
      const top = document.createElement("div");
      top.id = "topSection";
      top.style.display = "flex";
      top.style.gap = "30px";
      top.style.marginBottom = "25px";


      /* ===========================
         COMPETITION TABLE
         =========================== */

      const compBox = document.createElement("div");
      compBox.style.flex = "1";

      const compTitle = document.createElement("h2");
      compTitle.innerText = "🏆 Competition Leaderboard";
      compBox.appendChild(compTitle);

      const compTable = document.createElement("table");
      compTable.className = "wikitable";
      compTable.style.width = "100%";

      compTable.innerHTML = `
        <tr>
          <th>Rank</th>
          <th>Player</th>
          <th>Points</th>
        </tr>
      `;

      const leaderboard = Object.entries(playerPoints)
        .map(([player, points]) => ({player, points}))
        .sort((a,b)=>b.points-a.points);

      let prevPoints = null;
      let rank = 0;
      let position = 0;

      leaderboard.forEach(entry => {

        position++;
        if (entry.points !== prevPoints) rank = position;
        prevPoints = entry.points;

        const medal =
          rank===1?"🥇 1":
          rank===2?"🥈 2":
          rank===3?"🥉 3":
          rank;

        const row = document.createElement("tr");
        row.innerHTML = `
          <td>${medal}</td>
          <td>${entry.player}</td>
          <td>${entry.points}</td>
        `;
        compTable.appendChild(row);

      });

      compBox.appendChild(compTable);


      /* ===========================
         WORLD RECORD TABLE
         =========================== */

      const wrBox = document.createElement("div");
      wrBox.style.flex = "1";

      const wrTitle = document.createElement("h2");
      wrTitle.innerText = "🏁 Competition Dungeon Records";
      wrBox.appendChild(wrTitle);

      const wrTable = document.createElement("table");
      wrTable.className = "wikitable";
      wrTable.style.width = "100%";

      wrTable.innerHTML = `
        <tr>
          <th>Dungeon</th>
          <th>Time</th>
          <th>Player(s)</th>
        </tr>
      `;

      Object.entries(records).forEach(([d, data]) => {

        const row = document.createElement("tr");

        row.innerHTML = `
          <td>${d}</td>
          <td>${data.time}</td>
          <td>${data.players.join(", ")}</td>
        `;

        wrTable.appendChild(row);

      });

      wrBox.appendChild(wrTable);


      /* COMBINE */
      top.appendChild(compBox);
      top.appendChild(wrBox);

      table.parentNode.insertBefore(top, table);

      /* FILTER BELOW */
      table.parentNode.insertBefore(filterWrapper, table);
    }


    /* ===========================
       FILTER
       =========================== */

    function filterTable() {

      const dungeon = dungeonSelect.value;
      const player = playerInput.value.toLowerCase();

      const rows = Array.from(table.querySelectorAll("tr")).slice(1);

      rows.forEach(row => {

        if (row.cells.length < 5) return;

        const d = row.cells[1].innerText.trim();
        const p = row.cells[2].innerText.toLowerCase();

        row.style.display =
          (dungeon === "All" || d === dungeon) &&
          p.includes(player)
            ? ""
            : "none";
      });
    }

    dungeonSelect.addEventListener("change", filterTable);
    playerInput.addEventListener("input", filterTable);


    /* ===========================
       INIT
       =========================== */

    sortAndRank();
    buildTopSection();

  });

});