PC Gaming Shelter
An archive dedicated to preserving PC Gaming history and more

MediaWiki:Datatables.js: Difference between revisions

From PC Gaming Shelter
No edit summary
No edit summary
 
(158 intermediate revisions by the same user not shown)
Line 1: Line 1:
var selectors = [
var selectors = ["#games > table"];
"#games > table"
var seriesSelectors = ["#series > table"];
];
var assetSelectors = ["#assets > table"];
var playerSelectors = ["#players table"];
var clanSelectors = ["#clans table"];
var allSelectors = selectors.concat(
  seriesSelectors,
  assetSelectors,
  playerSelectors,
  clanSelectors,
);


var found = false;
var found = false;
for (var i = 0; i < selectors.length; i++) {
for (var i = 0; i < allSelectors.length; i++) {
   if (document.querySelector(selectors[i])) {
   if (document.querySelector(allSelectors[i])) {
     found = true;
     found = true;
     break;
     break;
Line 25: Line 33:
     $("<style/>", {
     $("<style/>", {
       id: "datatable-styles",
       id: "datatable-styles",
       text: `
       text: /* css */ `
         table.dataTable > tbody > tr > td {
         table.dataTable > tbody > tr > td {
           padding-top: 0.25rem;
           padding-top: 0.25rem;
           padding-bottom: 0.25rem;
           padding-bottom: 0.25rem;
        }
        table.dataTable,
        table.dataTable * {
          border: 0 !important;
         }
         }


Line 59: Line 72:
           }
           }
         }
         }
         .dtsp-bordered, .dtsp-bordered:hover,
 
         .dtsp-searchPane, .dtsp-searchPane:hover {
        div.dtsp-panesContainer div.dtsp-title {
          padding: 2px 0 !important;
        }
 
         .dtsp-bordered,
        .dtsp-bordered:hover,
         .dtsp-searchPane,
        .dtsp-searchPane:hover {
           border: 0 !important;
           border: 0 !important;
        }
        .dtsp-topRow button:hover,
        .dtsp-topRow input:hover,
        .dtsp-topRow button:focus,
        .dtsp-topRow input:focus,
        .dtsp-topRow button,
        .dtsp-topRow input {
          margin: 0 !important;
         }
         }


         .dtsp-paneButton.clearButton {
         .dtsp-paneButton.clearButton {
           font-size: 1.5rem;
           font-size: 2.5rem;
          line-height: 0;
          padding-bottom: 6px;
        }
 
        .dtsp-paneButton.clearButton:not(:disabled) {
          color: #dc3545 !important;
          font-size: 3rem;
        }
 
        .dtsp-topRow {
          background-color: #1e2d59;
          background-image: url(/w/extensions/wikivisor/images/embossed-leather-pattern.png);
          background-blend-mode: darken;
          background-size: cover;
          border: 1px solid rgba(255, 255, 255, 0.2);
          box-shadow: inset 0 0 30px rgba(0, 0, 0, 0.75);
        }
        .dtsp-caret {
          align-items: end;
          display: inline-flex !important;
          font-size: 0 !important;
          height: 1rem;
          justify-content: center;
          line-height: 1;
          width: 1rem;
          align-items: center;
        }
 
        div.dtsp-topRow button.dtsp-collapseButton span.dtsp-caret {
          top: unset !important;
        }
 
        .dtsp-caret::before {
          content: "\\2335";
          font-size: 1rem;
           line-height: 1;
           line-height: 1;
         }
         }


         .dtsp-paneButton.clearButton:not(:disabled) {
         [aria-expanded="true"] .dtsp-caret::before,
           color: var(--bs-danger, #dc3545);
        .dtsp-caret-u::before,
        .dtsp-caret-up::before {
          content: "\\2303";
        }
 
        div.dtsp-searchPane div.dtsp-topRow div.dtsp-searchCont input.dtsp-search {
          padding-left: 8px;
        }
 
        .dt-filter-toggle {
          align-items: center;
          background: var(--bs-primary, #0d6efd);
          border: 0;
          border-radius: 0.25rem;
          color: #fff;
          display: inline-flex;
          flex: 0 0 auto;
          font-weight: 600;
          gap: 0.375rem;
          line-height: 1.25;
          padding: 0.5rem 0.75rem;
        }
 
        .dt-filter-toolbar {
          align-items: center;
          display: flex;
          gap: 0.75rem;
          justify-content: space-between;
          margin-bottom: 0.75rem;
          width: 100%;
        }
 
        .dt-filter-toolbar .dt-search {
          align-items: center;
          display: flex;
          flex: 1 1 auto;
          gap: 0.5rem;
          justify-content: flex-end;
          margin: 0 !important;
        }
 
        .dt-filter-toolbar .dt-search input {
          margin-left: 0 !important;
          max-width: 24rem;
          width: 100%;
        }
 
        @media (max-width: 576px) {
          .dt-filter-toolbar {
            align-items: stretch;
            flex-direction: column;
          }
 
          .dt-filter-toolbar .dt-filter-toggle,
          .dt-filter-toolbar .dt-search,
          .dt-filter-toolbar .dt-search input {
            max-width: none;
            width: 100%;
          }
        }
 
        .dt-filter-toggle:focus {
          box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.25);
          outline: 0;
        }
 
        .dt-filter-toggle::before {
          content: "\\2630";
          font-size: 1rem;
          line-height: 1;
        }
 
        .dt-filter-backdrop {
          background: rgba(0, 0, 0, 0);
          inset: 0;
          opacity: 0;
          pointer-events: none;
          position: fixed;
          transition: opacity 180ms ease;
          z-index: 1040;
        }
 
        .dt-filter-backdrop.is-open {
          opacity: 1;
          pointer-events: auto;
        }
 
        .dt-filter-panel {
          background-color: #11161b !important;
          border-color: var(--bg-content);
          border-style: solid;
          border-width: 0.5rem;
          border-bottom-right-radius: 0.625rem;
          border-top-right-radius: 0.625rem;
          color: #fff;
          display: flex;
          flex-direction: column;
          height: 100vh;
          left: 0;
          max-width: min(92vw, 20rem);
          overflow: hidden;
          position: fixed;
          top: 0;
          transform: translateX(-100%);
          transition: transform 220ms ease;
          width: 20rem;
          z-index: 1050;
        }
 
        .dt-filter-panel.is-open {
          box-shadow: 0 0 20px rgba(255, 255, 255, 0.2);
          transform: translateX(0);
        }
 
        .dt-filter-panel__header {
          align-items: center;
          border-bottom: 1px solid var(--accent);
          display: flex;
          flex: 0 0 auto;
          justify-content: space-between;
          padding: 0.75rem 1rem;
        }
 
        .dt-filter-panel__title {
          font-size: 1.4rem;
          font-weight: 600;
          margin: 0;
          border-bottom: 0;
        }
 
        .dt-filter-panel__close {
          background: transparent;
          border: 0;
          color: rgb(124, 124, 124);
          font-size: 2.5rem;
          line-height: 1;
          padding: 0.125rem 0.375rem 6px 0.375rem;
          width: 35px;
          height: 35px;
          display: flex;
          align-items: center;
          justify-content: center;
        }
 
        .dt-filter-panel__body {
          flex: 1 1 auto;
          overflow: auto;
          padding: 0.75rem;
        }
 
        .dt-filter-panel__body .dtsp-panesContainer {
          margin: 0 !important;
        }
 
        .dt-filter-panel__body .dtsp-searchPanes {
          display: block !important;
        }
 
        .dt-filter-panel__body .dtsp-searchPane {
          margin-bottom: 0.75rem !important;
          width: 100% !important;
        }
 
        body.dt-filter-panel-open {
          overflow: hidden;
        }
 
        /*
        * The series table uses SearchBuilder as a single, fixed selector.
        * Keep only the criterion's value dropdown visible.
        */
        #series .dt-container .dtsb-titleRow,
        #series .dt-container .dtsb-logicContainer,
        #series .dt-container .dtsb-data,
        #series .dt-container .dtsb-condition,
        #series .dt-container .dtsb-add,
        #series .dt-container .dtsb-clearAll,
        #series .dt-container .dtsb-clearGroup,
        #series .dt-container .dtsb-search,
        #series .dt-container .dtsb-buttonContainer,
        #series .dt-container .dtsb-joiner,
        #series .dt-container .dtsb-delete,
        #series .dt-container .dtsb-left,
        #series .dt-container .dtsb-right {
          display: none !important;
        }
 
        #series .dt-container .dtsb-searchBuilder > :not(.dtsb-group),
        #series .dt-container .dtsb-group > :not(.dtsb-criteria),
        #series .dt-container .dtsb-criteria > :not(.dtsb-inputCont),
        #series .dt-container .dtsb-inputCont > :not(select.dtsb-value) {
          display: none !important;
        }
 
        #series .dt-container .dtsb-searchBuilder,
        #series .dt-container .dtsb-group,
        #series .dt-container .dtsb-criteria {
          border: 0 !important;
          margin: 0 !important;
          padding: 0 !important;
        }
 
        #series .dt-container .dtsb-criteria {
          display: block !important;
        }
 
        #series .dt-container .dtsb-inputCont {
          display: block !important;
          width: min(100%, 24rem) !important;
        }
 
        #series .dt-container .dtsb-inputCont > :not(.dtsb-value) {
          display: none !important;
        }
 
        #series .dt-container .dtsb-value {
          display: block !important;
          width: 100% !important;
        }
 
        aside.is-open .sidebar-drawer {
          display: inline-block;
          right: 0;
          left: 284px;
          border: 0;
          z-index: 3;
        }
 
        aside.dt-filter-panel {
          background-image: url(/w/images/d/de/File_Icon-Guardians-semitransparent.png);
          background-position: bottom;
          background-repeat: no-repeat;
        }
 
        .dt-search label {
          display: none;
        }
 
        #players tbody tr td:last-child {
          color: var(--bg-shade-6);
        }
 
        #players .player-games {
          align-items: baseline;
          display: flex;
          flex-wrap: wrap;
          gap: 0.25rem 0.5rem;
          position: relative;
        }
 
        #players .player-games__more {
          display: inline-block;
          position: relative;
          margin-left: auto;
        }
 
        #players .player-games__toggle {
          background: none;
          color: var(--bs-link-color, #6ea8fe);
          cursor: pointer;
          font-size: 0.875em;
          list-style: none;
          padding: 0;
          white-space: nowrap;
        }
 
        #players .player-games__toggle::-webkit-details-marker {
          display: none;
        }
 
        #players .player-games__toggle:hover,
        #players .player-games__toggle:focus {
          text-decoration: underline;
        }
 
        #players .player-games__popup {
          background-color: var(--bg-shade-4, #11161b);
          border: 1px solid rgba(255, 255, 255, 0.125) !important;
          border-radius: 0.25rem;
          box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.35);
           color: var(--bs-body-color, #fff);
          display: flex;
          flex-direction: column;
          gap: 0.25rem;
          left: 0;
          line-height: 1.5;
          min-width: max-content;
          padding: 0.5rem 0.75rem;
          position: absolute;
          top: calc(100% + 0.25rem);
          white-space: nowrap;
          z-index: 20;
        }
       
        .player-games__popup::before {
display: inline-block;
width: 6px;
height: 6px;
content: '';
border-left: 3px solid rgba(255, 255, 255, .25);
border-top: 3px solid rgba(255, 255, 255, .25);
border-right: 3px solid transparent;
border-bottom: 3px solid transparent;
transform: rotate(45deg);
margin-top: -12px;
         }
         }
       `,
       `,
Line 76: Line 444:
   }
   }


   $("#games > table").addClass("dt-loading").attr("aria-busy", "true");
   $(allSelectors.join(",")).addClass("dt-loading").attr("aria-busy", "true");


   /* Load JavaScript. */
   /* Load JavaScript. */
Line 166: Line 534:
       .map((value) => value.trim())
       .map((value) => value.trim())
       .filter(Boolean);
       .filter(Boolean);
  }
  function getPlainAcceptsValues(html) {
    return getAcceptsValues(html)
      .map(function (value) {
        var container = document.createElement("div");
        container.innerHTML = value;
        return (container.textContent || "").trim();
      })
      .filter(Boolean);
  }
  function renderPlayerGames(html) {
    var games = getAcceptsValues(html);
    if (games.length < 2) {
      return games[0] || "";
    }
    return (
      '<div class="player-games">' +
      '<span class="player-games__first">' +
      games[0] +
      "</span>" +
      '<details class="player-games__more">' +
      '<summary class="player-games__toggle">+' +
      (games.length - 1) +
      " more</summary>" +
      '<div class="player-games__popup" role="group" aria-label="Other games">' +
      games
        .slice(1)
        .map(function (game) {
          return "<div>" + game + "</div>";
        })
        .join("") +
      "</div></details></div>"
    );
  }
  function getPlainSearchPaneRender() {
    return {
      display: function (data) {
        return data;
      },
      sp: function (data) {
        return getPlainAcceptsValues(data);
      },
    };
  }
  function getGameModeText(html) {
    var container = document.createElement("div");
    var values = [];
    container.innerHTML = html || "";
    values.push(container.textContent || "");
    container.querySelectorAll("*").forEach(function (element) {
      ["alt", "title", "aria-label", "class", "src"].forEach(
        function (attribute) {
          var value = element.getAttribute(attribute);
          if (value) {
            values.push(value);
          }
        },
      );
    });
    return values.join(" ");
  }
  function getGameModeValue(html) {
    var text = getGameModeText(html).toLowerCase().replace(/[_-]+/g, " ");
    if (/\b(single\s*player|singleplayer|solo|sp)\b/.test(text)) {
      return "SP";
    }
    if (/\b(multi\s*player|multiplayer|mp)\b/.test(text)) {
      return "MP";
    }
    if (/\b(co\s*op|coop|cooperative)\b/.test(text)) {
      return "Coop";
    }
    return "";
  }
  function getGameModeValues(html) {
    var selectedModes = new Set(
      getAcceptsValues(html).map(getGameModeValue).filter(Boolean),
    );
    return ["SP", "MP", "Coop"].filter(function (mode) {
      return selectedModes.has(mode);
    });
  }
  function getGameModeDisplayValue(html, mode) {
    var container = document.createElement("div");
    var link;
    container.innerHTML = html || "";
    link = container.querySelector("a[href]");
    if (!link) {
      return mode;
    }
    link = link.cloneNode(false);
    link.textContent = mode;
    return link.outerHTML;
  }
  function getGameModeDisplayValues(html) {
    var valuesByMode = {};
    getAcceptsValues(html).forEach(function (value) {
      var mode = getGameModeValue(value);
      if (mode && !valuesByMode[mode]) {
        valuesByMode[mode] = getGameModeDisplayValue(value, mode);
      }
    });
    return ["SP", "MP", "Coop"]
      .filter(function (mode) {
        return Object.prototype.hasOwnProperty.call(valuesByMode, mode);
      })
      .map(function (mode) {
        return valuesByMode[mode];
      });
  }
  function normaliseColumnName(value) {
    return String(value || "")
      .trim()
      .toLowerCase()
      .replace(/[\s_-]+/g, "");
  }
  function getTableConfigElement(table) {
    return (
      table.closest(
        "[data-hidden-columns], [data-hide-columns], [data-dt-hidden-columns], [data-dt-hide-columns], [data-pane-columns], [data-search-pane-columns], [data-dt-pane-columns], [data-first-column-name], [data-first-column], [data-page-column-name], [data-dt-first-column-name]",
      ) || table.parentElement
    );
  }
  function getFirstColumnName(table) {
    var configElement = getTableConfigElement(table);
    var columnName = configElement
      ? configElement.getAttribute("data-first-column-name") ||
        configElement.getAttribute("data-first-column") ||
        configElement.getAttribute("data-page-column-name") ||
        configElement.getAttribute("data-dt-first-column-name")
      : "";
    return String(columnName || "Page").trim() || "Page";
  }
  function getHiddenColumnIndexes(table, titles) {
    var configElement = getTableConfigElement(table);
    var rawValue = configElement
      ? configElement.getAttribute("data-hidden-columns") ||
        configElement.getAttribute("data-hide-columns") ||
        configElement.getAttribute("data-dt-hidden-columns") ||
        configElement.getAttribute("data-dt-hide-columns")
      : "";
    var titleIndexes = {};
    var hiddenIndexes = new Set();
    titles.forEach(function (title, index) {
      titleIndexes[normaliseColumnName(title)] = index;
    });
    String(rawValue || "")
      .split(/[,\|]/)
      .map(function (value) {
        return value.trim();
      })
      .filter(Boolean)
      .forEach(function (value) {
        var numberValue = Number(value);
        var normalisedValue = normaliseColumnName(value);
        if (
          Number.isInteger(numberValue) &&
          numberValue >= 0 &&
          numberValue < titles.length
        ) {
          hiddenIndexes.add(numberValue);
          return;
        }
        if (
          Object.prototype.hasOwnProperty.call(titleIndexes, normalisedValue)
        ) {
          hiddenIndexes.add(titleIndexes[normalisedValue]);
        }
      });
    return hiddenIndexes;
  }
  function getPaneColumnIndexes(table, titles, fallbackIndexes) {
    var configElement = getTableConfigElement(table);
    var attributes = [
      "data-pane-columns",
      "data-search-pane-columns",
      "data-dt-pane-columns",
    ];
    var configuredAttribute = attributes.find(function (attribute) {
      return configElement && configElement.hasAttribute(attribute);
    });
    var titleIndexes = {};
    var paneIndexes = [];
    if (!configuredAttribute) {
      return fallbackIndexes.slice();
    }
    titles.forEach(function (title, index) {
      titleIndexes[normaliseColumnName(title)] = index;
    });
    String(configElement.getAttribute(configuredAttribute) || "")
      .split(/[,\|]/)
      .map(function (value) {
        return value.trim();
      })
      .filter(Boolean)
      .forEach(function (value) {
        var numberValue = Number(value);
        var normalisedValue = normaliseColumnName(value);
        var index = Number.isInteger(numberValue)
          ? numberValue
          : titleIndexes[normalisedValue];
        if (
          Number.isInteger(index) &&
          index >= 0 &&
          index < titles.length &&
          paneIndexes.indexOf(index) === -1
        ) {
          paneIndexes.push(index);
        }
      });
    return paneIndexes;
   }
   }


Line 197: Line 820:


   function suppressSearchPaneBorders(container) {
   function suppressSearchPaneBorders(container) {
    if (!container) {
      return;
    }
     function removeBorderClass() {
     function removeBorderClass() {
       container.querySelectorAll(".dtsp-bordered").forEach(function (element) {
       container.querySelectorAll(".dtsp-bordered").forEach(function (element) {
Line 223: Line 850:
         });
         });
       });
       });
  }
  function setupFilterToolbar(wrapper, toggle) {
    var toolbar = document.createElement("div");
    var search = wrapper.querySelector(".dt-search");
    var searchRow = search ? search.closest(".dt-layout-row") : null;
    toolbar.className = "dt-filter-toolbar";
    if (searchRow && searchRow.parentNode) {
      searchRow.parentNode.insertBefore(toolbar, searchRow);
    } else {
      wrapper.insertBefore(toolbar, wrapper.firstChild);
    }
    toolbar.appendChild(toggle);
    if (search) {
      toolbar.appendChild(search);
    }
    if (
      searchRow &&
      searchRow.parentNode &&
      !searchRow.querySelector(".dt-search")
    ) {
      searchRow.remove();
    }
  }
  function getCurrentPageName() {
    var pageName = "";
    if (window.mw && mw.config) {
      pageName = mw.config.get("wgTitle") || mw.config.get("wgPageName") || "";
    }
    if (!pageName) {
      pageName =
        (document.querySelector("#firstHeading") || {}).textContent ||
        document.title ||
        "";
    }
    return pageName.replace(/_/g, " ").trim();
  }
  function getFilterPanelTitle(titles, hiddenColumnIndexes) {
    var pageName = getCurrentPageName();
    if (hiddenColumnIndexes && hiddenColumnIndexes.size === 1) {
      return titles[Array.from(hiddenColumnIndexes)[0]] + ": " + pageName;
    }
    return pageName;
  }
  function setupFilterOffCanvas(dataTable, table, titles, hiddenColumnIndexes) {
    var wrapper = table.closest(".dt-container") || table.parentNode;
    var panes = wrapper ? wrapper.querySelector(".dtsp-panesContainer") : null;
    if (!wrapper || !panes) {
      return null;
    }
    if (wrapper.querySelector(".dt-filter-toggle")) {
      return document.getElementById(
        wrapper
          .querySelector(".dt-filter-toggle")
          .getAttribute("aria-controls"),
      );
    }
    var toggle = document.createElement("button");
    var panel = document.createElement("aside");
    var backdrop = document.createElement("div");
    var drawer = document.createElement("button");
    var header = document.createElement("div");
    var title = document.createElement("h2");
    var close = document.createElement("button");
    var body = document.createElement("div");
    var panelId = "dt-filter-panel-" + $(".dt-filter-panel").length;
    var lastFocusedElement = null;
    toggle.type = "button";
    toggle.className = "dt-filter-toggle";
    toggle.textContent = "Filters";
    toggle.setAttribute("aria-controls", panelId);
    toggle.setAttribute("aria-expanded", "false");
    panel.id = panelId;
    panel.className = "dt-filter-panel";
    panel.setAttribute("aria-hidden", "true");
    panel.setAttribute("aria-label", "Data filters");
    backdrop.className = "dt-filter-backdrop";
    drawer.className = "sidebar-drawer";
    drawer.setAttribute("type", "button");
    drawer.setAttribute("tabindex", "0");
    header.className = "dt-filter-panel__header";
    title.className = "dt-filter-panel__title";
    title.textContent = getFilterPanelTitle(titles, hiddenColumnIndexes);
    close.type = "button";
    close.className = "dt-filter-panel__close";
    close.setAttribute("aria-label", "Close filters");
    close.innerHTML = "&times;";
    body.className = "dt-filter-panel__body";
    header.append(title, close);
    body.appendChild(panes);
    panel.append(drawer, header, body);
    setupFilterToolbar(wrapper, toggle);
    document.body.append(backdrop, panel);
    function openPanel() {
      lastFocusedElement = document.activeElement;
      panel.classList.add("is-open");
      backdrop.classList.add("is-open");
      document.body.classList.add("dt-filter-panel-open");
      panel.setAttribute("aria-hidden", "false");
      toggle.setAttribute("aria-expanded", "true");
      close.focus();
      dataTable.columns.adjust();
    }
    function closePanel() {
      panel.classList.remove("is-open");
      backdrop.classList.remove("is-open");
      document.body.classList.remove("dt-filter-panel-open");
      panel.setAttribute("aria-hidden", "true");
      toggle.setAttribute("aria-expanded", "false");
      if (lastFocusedElement && document.contains(lastFocusedElement)) {
        lastFocusedElement.focus();
      }
      dataTable.columns.adjust();
    }
    toggle.addEventListener("click", openPanel);
    close.addEventListener("click", closePanel);
    drawer.addEventListener("click", closePanel);
    backdrop.addEventListener("click", closePanel);
    document.addEventListener("keydown", function (event) {
      if (event.key === "Escape" && panel.classList.contains("is-open")) {
        closePanel();
      }
    });
    return panel;
   }
   }


   $(".dataTable")
   $(".dataTable")
    .not(allSelectors.join(","))
     .each(function () {
     .each(function () {
       var table = this;
       var table = this;
Line 237: Line 1,019:


         $(table).DataTable({
         $(table).DataTable({
           dom: "f",
           language: {
            search: "",
            searchPlaceholder: "Search",
            searchPanes: {
              title: {
                _: "Filters: %d",
                0: "",
              },
            },
          },
           retrieve: true,
           retrieve: true,
           order: [[0, "asc"]],
           order: [[0, "asc"]],
Line 245: Line 1,036:
     });
     });


// ----- GAMES ------
  $(seriesSelectors.join(",")).each(function () {
    var table = this;
 
    waitForTableToSettle(table, function () {
      if ($.fn.dataTable.isDataTable(table)) {
        return;
      }
 
      var titles = ["Series", "Games"];
 
      /*
      * The source table is emitted as Games, Series.
      * DataTables uses Series, Games, so Series is column 0.
      */
      var data = getTableData(table, titles.length).map(function (row) {
        return {
          series: row[1],
          games: row[0],
        };
      });
 
      normaliseDataTableRows(table, titles.length);
      buildTableLayout(table, titles);
 
      try {
        var dataTable = new DataTable(table, {
          data: data,
 
          columns: [
            {
              data: "games",
              name: "Games",
              title: "Game",
            },
            {
              data: "series",
              name: "Series",
              title: "Series",
              visible: false,
              searchable: true,
              type: "string",
            },
          ],
 
          layout: {
            topStart: null,
            topEnd: null,
            top1: {
              searchBuilder: {
                columns: [1],
 
                preDefined: {
                  logic: "AND",
                  criteria: [
                    {
                      data: "series",
                      origData: "series",
                      condition: "=",
                      type: "string",
                    },
                  ],
                },
              },
            },
            top2: {},
          },
 
          language: {
            searchBuilder: {
              data: "Series",
              value: "Select a series",
            },
          },
 
          order: [[0, "asc"]],
          paging: false,
 
          initComplete: function () {
            var api = this.api();
            var container = $(api.table().container());
 
            function emptySeriesCriterion() {
              return {
                logic: "AND",
                criteria: [
                  {
                    data: "series",
                    origData: "series",
                    condition: "=",
                    type: "string",
                  },
                ],
              };
            }
 
            function enableAllSeriesOption() {
              var valueSelect = container.find(".dtsb-value").first();
              var emptyOption = valueSelect
                .find('option[value="--valueTitle--"]')
                .first();
 
              emptyOption
                .prop("disabled", false)
                .prop("hidden", false)
                .text("All series");
            }
 
            container[0].addEventListener(
              "change",
              function (event) {
                var select = event.target;
 
                if (
                  !select.matches(".dtsb-value") ||
                  select.value !== "--valueTitle--"
                ) {
                  return;
                }
 
                /*
                * The placeholder has no SearchBuilder value.
                * Stop its listener before it calls toString()
                * on undefined, then reset the criterion here.
                */
                event.stopImmediatePropagation();
 
                api.searchBuilder.rebuild(emptySeriesCriterion(), false);
                api.draw(false);
                setTimeout(enableAllSeriesOption, 0);
              },
              true,
            );
 
            setTimeout(enableAllSeriesOption, 0);
          },
        });
 
        dataTable.columns.adjust().draw(false);
      } catch (error) {
        mw.log.error("Could not initialise the Series DataTable:", error);
      } finally {
        $(table).removeClass("dt-loading").removeAttr("aria-busy").show();
      }
    });
  });
 
  $(assetSelectors.join(",")).each(function () {
    var table = this;
 
    waitForTableToSettle(table, function () {
      if ($.fn.dataTable.isDataTable(table)) {
        return;
      }
 
      var titles = [
        "Asset",
        "Type",
        "Subtype",
        "Mode",
        "Developer",
        "Released",
        "Status",
        "Description",
        "Version",
        "Files",
        "Game",
      ];
 
      var paneIndexes = [1, 2, 3, 4, 5, 6, 8];
      if (mw.config.get("wgPageName") === "Mods") {
        paneIndexes = [10, 1, 2, 3, 4, 5, 6, 8];
      }
 
      var hiddenColumns = [2, 3, 5, 6, 7, 8, 10];
      var data = getTableData(table, titles.length);
 
      buildTableLayout(table, titles);
 
      var dataTable = new DataTable(table, {
        columns: titles.map(function (title, index) {
          var column = {
            title: title,
            visible: hiddenColumns.indexOf(index) === -1,
            searchPanes: {
              show: paneIndexes.indexOf(index) !== -1,
            },
          };
 
          if (index === 0) {
            column.render = function (cellData, type) {
              if (type === "display") {
                return (
                  '<span style="display: list-item; margin-left: 1.25rem; white-space: nowrap">' +
                  cellData +
                  "</span>"
                );
              }
 
              return cellData;
            };
          }
 
          if ([1, 2, 3, 4, 6, 8, 10].indexOf(index) !== -1) {
            column.render = {
              display: function (cellData) {
                if (index === 4) {
                  return (
                    '<span style="white-space: nowrap">' +
                    getAcceptsValues(cellData).join(", ") +
                    "</span>"
                  );
                }
 
                return cellData;
              },
              sp: function (cellData) {
                return getPlainAcceptsValues(cellData);
              },
            };
            column.searchPanes.orthogonal = "sp";
          }
 
          if (index === 9) {
            column.render = function (cellData, type) {
              if (type !== "display") {
                return cellData;
              }
 
              var wrapper = document.createElement("div");
              wrapper.innerHTML = cellData;
 
              var links = Array.from(wrapper.querySelectorAll("a"));
 
              if (!links.length) {
                return "";
              }
 
              var downloads = document.createElement("div");
 
              links.forEach(function (sourceLink, linkIndex) {
                var link = sourceLink.cloneNode(true);
 
                link.textContent =
                  links.length === 1
                    ? "Download"
                    : "Download " + (linkIndex + 1);
                link.target = "_blank";
                link.rel = "noopener noreferrer nofollow";
 
                downloads.appendChild(link);
 
                if (linkIndex < links.length - 1) {
                  downloads.appendChild(document.createElement("br"));
                }
              });
 
              return downloads.innerHTML;
            };
          }
 
          return column;
        }),
        layout: {
          topStart: null,
          topEnd: null,
          top2: {
            searchPanes: {
              cascadePanes: true,
              dtOpts: {
                scroller: false,
              },
              initCollapsed: true,
              layout: "columns-1",
              columns: paneIndexes,
            },
          },
          top1: {
            search: {
              className: "mx-0 w-100",
            },
          },
        },
        order: [[0, "asc"]],
        language: {
          search: "",
          searchPlaceholder: "Search assets",
          searchPanes: {
            title: {
              _: "Filters: %d",
              0: "",
            },
          },
        },
        paging: false,
        searchDelay: 400,
      });
 
      var lastGlobalSearch = dataTable.search();
 
      dataTable.on("draw.dt", function () {
        var globalSearch = dataTable.search();
 
        openFellowshipLinksInNewTab(dataTable);
 
        if (globalSearch === lastGlobalSearch) {
          return;
        }
 
        lastGlobalSearch = globalSearch;
 
        paneIndexes.forEach(function (paneIndex) {
          dataTable.searchPanes.rebuildPane(paneIndex, true);
        });
      });
 
      dataTable.rows.add(data).draw();
      $(table).removeClass("dt-loading").removeAttr("aria-busy").show();
      dataTable.searchPanes.rebuildPane();
 
      var filterPanel = setupFilterOffCanvas(
        dataTable,
        table,
        titles,
        new Set(),
      );
 
      suppressSearchPaneBorders(filterPanel);
      dataTable.columns.adjust().draw(false);
    });
  });
 
  $(clanSelectors.join(",")).each(function () {
    var table = this;
 
    waitForTableToSettle(table, function () {
      if ($.fn.dataTable.isDataTable(table)) {
        return;
      }
 
      var titles = ["Clan", "Games", "Links", "Description", "Status"];
      var paneIndexes = [1, 4];
      var clanColorClasses = Array.from(
        table.tBodies[0] ? table.tBodies[0].rows : [],
      )
        .filter(function (row) {
          return !(row.cells.length === 1 && row.cells[0].colSpan > 1);
        })
        .map(function (row) {
          return row.cells[0] ? row.cells[0].className : "";
        });
      var data = getTableData(table, titles.length);
 
      buildTableLayout(table, titles);
 
      var dataTable = new DataTable(table, {
        data: data,
        columns: [
          {
            title: "Clan",
            createdCell: function (cell, cellData, rowData, rowIndex) {
              var colorClasses = clanColorClasses[rowIndex];
 
              if (!colorClasses) {
                return;
              }
 
              colorClasses
                .split(/\s+/)
                .filter(Boolean)
                .forEach(function (className) {
                  cell.classList.add(className);
                });
            },
            render: function (cellData, type) {
              if (type === "display") {
                return (
                  '<span style="display: list-item; margin-left: 1.25rem; white-space: nowrap">' +
                  cellData +
                  "</span>"
                );
              }
 
              return cellData;
            },
            searchPanes: {
              show: false,
            },
          },
          {
            title: "Games",
            visible: false,
            render: getPlainSearchPaneRender(),
            searchPanes: {
              show: true,
              orthogonal: "sp",
            },
          },
          {
            title: "Links",
            render: function (cellData, type) {
              if (type !== "display") {
                return cellData;
              }
 
              var container = document.createElement("div");
 
              container.innerHTML = cellData || "";
              container
                .querySelectorAll("a.external[href]")
                .forEach(function (link) {
                  var hostname;
 
                  try {
                    hostname = new URL(link.href, window.location.href)
                      .hostname;
                  } catch (error) {
                    hostname = "";
                  }
 
                  if (hostname) {
                    link.textContent = hostname.replace(/^www\./i, "");
                  }
 
                  link.target = "_blank";
                  link.rel = "noopener noreferrer nofollow";
                });
 
              return container.innerHTML;
            },
            searchPanes: {
              show: false,
            },
          },
          {
            title: "Description",
            searchPanes: {
              show: false,
            },
          },
          {
            title: "Status",
            visible: false,
            render: getPlainSearchPaneRender(),
            searchPanes: {
              show: true,
              orthogonal: "sp",
            },
          },
        ],
        layout: {
          topStart: null,
          topEnd: null,
          top2: {
            searchPanes: {
              cascadePanes: true,
              dtOpts: {
                scroller: false,
              },
              initCollapsed: true,
              layout: "columns-1",
              columns: paneIndexes,
            },
          },
          top1: {
            search: {
              className: "mx-0 w-100",
            },
          },
        },
        order: [[0, "asc"]],
        language: {
          search: "",
          searchPlaceholder: "Search clans",
          searchPanes: {
            title: {
              _: "Filters: %d",
              0: "",
            },
          },
        },
        paging: false,
        searchDelay: 400,
      });
 
      var lastGlobalSearch = dataTable.search();
 
      dataTable.on("draw.dt", function () {
        var globalSearch = dataTable.search();
 
        openFellowshipLinksInNewTab(dataTable);
 
        if (globalSearch === lastGlobalSearch) {
          return;
        }
 
        lastGlobalSearch = globalSearch;
        paneIndexes.forEach(function (paneIndex) {
          dataTable.searchPanes.rebuildPane(paneIndex, true);
        });
      });
 
      $(table).removeClass("dt-loading").removeAttr("aria-busy").show();
      dataTable.searchPanes.rebuildPane();
 
      var filterPanel = setupFilterOffCanvas(
        dataTable,
        table,
        titles,
        new Set(),
      );
 
      suppressSearchPaneBorders(filterPanel);
      dataTable.columns.adjust().draw(false);
    });
  });
 
  $(playerSelectors.join(",")).each(function () {
    var table = this;
 
    waitForTableToSettle(table, function () {
      if ($.fn.dataTable.isDataTable(table)) {
        return;
      }
 
      var fallbackTitles = [
        "Player",
        "Games",
        "Clans",
        "Roles",
        "Clan (Role)",
        "Status",
      ];
      var headerCells =
        table.tHead && table.tHead.rows.length
          ? Array.from(table.tHead.rows[table.tHead.rows.length - 1].cells)
          : [];
      var titles = fallbackTitles.map(function (fallbackTitle, index) {
        var title = headerCells[index]
          ? headerCells[index].textContent.trim()
          : "";
 
        return title || fallbackTitle;
      });
 
      var hiddenColumnIndexes = new Set([2, 3, 5]);
      var paneIndexes = getPaneColumnIndexes(table, titles, [2, 3, 5]);
      var playerColorClasses = Array.from(
        table.tBodies[0] ? table.tBodies[0].rows : [],
      )
        .filter(function (row) {
          return !(row.cells.length === 1 && row.cells[0].colSpan > 1);
        })
        .map(function (row) {
          return row.cells[0] ? row.cells[0].className : "";
        });
 
      var data = getTableData(table, titles.length);
 
      buildTableLayout(table, titles);
 
      var dataTable = new DataTable(table, {
        columns: titles.map(function (title, index) {
          var column = {
            title: title,
            visible: !hiddenColumnIndexes.has(index),
            searchPanes: {
              show: paneIndexes.indexOf(index) !== -1,
            },
          };
 
          if (index === 0) {
            column.createdCell = function (cell, cellData, rowData, rowIndex) {
              var colorClasses = playerColorClasses[rowIndex];
 
              if (!colorClasses) {
                return;
              }
 
              colorClasses
                .split(/\s+/)
                .filter(Boolean)
                .forEach(function (className) {
                  cell.classList.add(className);
                });
            };
            column.render = function (cellData, type) {
              if (type === "display") {
                return (
                  '<span style="display: list-item; margin-left: 1.25rem; white-space: nowrap">' +
                  cellData +
                  "</span>"
                );
              }
 
              return cellData;
            };
          } else if (index === 1) {
            column.render = function (cellData, type) {
              if (type === "display") {
                return renderPlayerGames(cellData);
              }
 
              if (type === "sp") {
                return getPlainAcceptsValues(cellData);
              }
 
              return cellData;
            };
 
            if (paneIndexes.indexOf(index) !== -1) {
              column.searchPanes.orthogonal = "sp";
            }
          } else if (paneIndexes.indexOf(index) !== -1) {
            column.render = {
              display: function (cellData) {
                return cellData;
              },
              sp: function (cellData) {
                var container = document.createElement("div");
                var linkedValues;
                var blockValues;
 
                container.innerHTML = cellData || "";
                linkedValues = Array.from(
                  container.querySelectorAll("a"),
                  function (link) {
                    return link.textContent.trim();
                  },
                ).filter(Boolean);
 
                if (linkedValues.length) {
                  return linkedValues;
                }
 
                blockValues = Array.from(
                  container.children,
                  function (element) {
                    return element.textContent.trim();
                  },
                ).filter(Boolean);
 
                return blockValues.length
                  ? blockValues
                  : getAcceptsValues(cellData);
              },
            };
            column.searchPanes.orthogonal = "sp";
          }
 
          return column;
        }),
        layout: {
          topStart: null,
          topEnd: null,
          top2: {
            searchPanes: {
              cascadePanes: true,
              dtOpts: {
                scroller: false,
              },
              initCollapsed: true,
              layout: "columns-1",
              columns: paneIndexes,
            },
          },
          top1: {
            search: {
              className: "mx-0 w-100",
            },
          },
        },
        order: [[0, "asc"]],
        language: {
          search: "",
          searchPlaceholder: "Search players",
          searchPanes: {
            title: {
              _: "Filters: %d",
              0: "",
            },
          },
        },
        paging: false,
        searchDelay: 400,
      });
 
      var lastGlobalSearch = dataTable.search();


$("#games > table").each(function () {
      dataTable.on("draw.dt", function () {
        var globalSearch = dataTable.search();
 
        openFellowshipLinksInNewTab(dataTable);
 
        if (globalSearch === lastGlobalSearch) {
          return;
        }
 
        lastGlobalSearch = globalSearch;
 
        paneIndexes.forEach(function (paneIndex) {
          dataTable.searchPanes.rebuildPane(paneIndex, true);
        });
      });
 
      dataTable.rows.add(data).draw();
      $(table).removeClass("dt-loading").removeAttr("aria-busy").show();
      dataTable.searchPanes.rebuildPane();
 
      var filterPanel = setupFilterOffCanvas(
        dataTable,
        table,
        titles,
        hiddenColumnIndexes,
      );
 
      suppressSearchPaneBorders(filterPanel);
      dataTable.columns.adjust().draw(false);
    });
  });
 
  $(selectors.join(",")).each(function () {
     var table = this;
     var table = this;


Line 256: Line 1,765:


       var titles = [
       var titles = [
      "Game", "Developer", "Publisher", "Genre", "Platform", "Mode", "Released"
        getFirstColumnName(table),
        "Developer",
        "Publisher",
        "Genre",
        "Platform",
        "Mode",
        "Released",
        "Category",
       ];
       ];
      var hiddenColumnIndexes = getHiddenColumnIndexes(table, titles);
      var paneIndexes = [1, 2, 3, 4, 5, 7].filter(function (paneIndex) {
        return !hiddenColumnIndexes.has(paneIndex);
      });
       var data = getTableData(table, titles.length);
       var data = getTableData(table, titles.length);


Line 265: Line 1,785:
         columns: [
         columns: [
           {
           {
             title: "Game",
             title: titles[0],
            visible: !hiddenColumnIndexes.has(0),
             render: function (data, type) {
             render: function (data, type) {
               if (type === "display") {
               if (type === "display") {
                 return (
                 return (
                   '<span style="display: list-item; margin-left: 1.25rem;">' +
                   '<span style="display: list-item; margin-left: 1.25rem; white-space: nowrap">' +
                   data +
                   data +
                   "</span>"
                   "</span>"
Line 283: Line 1,804:
           {
           {
             title: "Developer",
             title: "Developer",
             visible: true,
             visible: !hiddenColumnIndexes.has(1),
            render: getPlainSearchPaneRender(),
             searchPanes: {
             searchPanes: {
               show: true,
               show: !hiddenColumnIndexes.has(1),
              orthogonal: "sp",
             },
             },
           },
           },
           {
           {
             title: "Publisher",
             title: "Publisher",
             visible: true,
             visible: !hiddenColumnIndexes.has(2),
            render: getPlainSearchPaneRender(),
             searchPanes: {
             searchPanes: {
               show: true,
               show: !hiddenColumnIndexes.has(2),
              orthogonal: "sp",
             },
             },
           },
           },
           {
           {
             title: "Genre",
             title: "Genre",
             visible: true,
             visible: !hiddenColumnIndexes.has(3),
            render: getPlainSearchPaneRender(),
             searchPanes: {
             searchPanes: {
               show: true,
               show: !hiddenColumnIndexes.has(3),
              orthogonal: "sp",
             },
             },
           },
           },
           {
           {
             title: "Platform",
             title: "Platform",
             visible: true,
             visible: !hiddenColumnIndexes.has(4),
            render: getPlainSearchPaneRender(),
             searchPanes: {
             searchPanes: {
               show: true,
               show: !hiddenColumnIndexes.has(4),
              orthogonal: "sp",
             },
             },
           },        
           },
           {
           {
             title: "Mode",
             title: "Mode",
             visible: true,
             visible: !hiddenColumnIndexes.has(5),
             render: {
             render: {
               display: function (data) {
               display: function (data) {
                 return data;
                 return getGameModeDisplayValues(data).join(", ");
               },
               },
               sp: function (data) {
               sp: function (data) {
                 return getAcceptsValues(data);
                 return getGameModeValues(data);
               },
               },
             },
             },
             searchPanes: {
             searchPanes: {
               show: true,
               show: !hiddenColumnIndexes.has(5),
               orthogonal: "sp",
               orthogonal: "sp",
             },
             },
Line 327: Line 1,856:
           {
           {
             title: "Released",
             title: "Released",
             visible: true,
             visible: !hiddenColumnIndexes.has(6),
            searchPanes: {
              show: !hiddenColumnIndexes.has(6),
            },
          },
          {
            title: "Category",
            visible: false,
            render: getPlainSearchPaneRender(),
             searchPanes: {
             searchPanes: {
               show: true,
               show: true,
              orthogonal: "sp",
             },
             },
           },  
           },
         ],
         ],
         layout: {
         layout: {
Line 339: Line 1,877:
             searchPanes: {
             searchPanes: {
               cascadePanes: true,
               cascadePanes: true,
              dtOpts: {
                scroller: false,
              },
               initCollapsed: true,
               initCollapsed: true,
               columns: [1, 2, 3, 4, 5],
               layout: "columns-1",
              columns: paneIndexes,
             },
             },
           },
           },
Line 350: Line 1,892:
         },
         },
         order: [[0, "asc"]],
         order: [[0, "asc"]],
        language: {
          search: "",
          searchPlaceholder: "Search",
          searchPanes: {
            title: {
              _: "Filters: %d",
              0: "",
            },
          },
        },
         paging: false,
         paging: false,
         searchDelay: 400,
         searchDelay: 400,
Line 355: Line 1,907:


       var lastGlobalSearch = dataTable.search();
       var lastGlobalSearch = dataTable.search();
      var paneIndexes = [1, 2, 3, 4, 5];


       dataTable.on("draw.dt", function () {
       dataTable.on("draw.dt", function () {
Line 372: Line 1,923:
         });
         });
       });
       });
      var filterPanel;


       dataTable.rows.add(data).draw();
       dataTable.rows.add(data).draw();
       $(".dataTable")
       $(table).removeClass("dt-loading").removeAttr("aria-busy").show();
        .removeClass("dt-loading")
        .removeAttr("aria-busy")
        .show();
       dataTable.searchPanes.rebuildPane();
       dataTable.searchPanes.rebuildPane();
       suppressSearchPaneBorders(document.querySelector(".dataTable"));
       filterPanel = setupFilterOffCanvas(
        dataTable,
        table,
        titles,
        hiddenColumnIndexes,
      );
      suppressSearchPaneBorders(filterPanel);
       dataTable.columns.adjust().draw(false);
       dataTable.columns.adjust().draw(false);
     });
     });
   });
   });
}
}

Latest revision as of 07:54, 12 August 2026

var selectors = ["#games > table"];
var seriesSelectors = ["#series > table"];
var assetSelectors = ["#assets > table"];
var playerSelectors = ["#players table"];
var clanSelectors = ["#clans table"];
var allSelectors = selectors.concat(
  seriesSelectors,
  assetSelectors,
  playerSelectors,
  clanSelectors,
);

var found = false;
for (var i = 0; i < allSelectors.length; i++) {
  if (document.querySelector(allSelectors[i])) {
    found = true;
    break;
  }
}

/* ------------- DataTables Loader and initialisation ---------------- */

if (found === true) {
  /* Load CSS. */
  const dtsource =
    "https://cdn.datatables.net/v/dt/moment-2.29.4/dt-2.3.7/b-3.2.6/b-colvis-3.2.6/b-html5-3.2.6/b-print-3.2.6/cc-1.2.0/date-1.6.3/fh-4.0.5/r-3.0.8/sc-2.4.3/sb-1.8.4/sp-2.3.5/sl-3.1.3/sr-1.4.3/datatables.min.css";

  if (!document.querySelector('link[href*="datatables.min.css"]')) {
    $("<link/>", { rel: "stylesheet", href: dtsource }).appendTo("head");
  }

  if (!document.querySelector("#datatable-styles")) {
    $("<style/>", {
      id: "datatable-styles",
      text: /* css */ `
        table.dataTable > tbody > tr > td {
          padding-top: 0.25rem;
          padding-bottom: 0.25rem;
        }

        table.dataTable,
        table.dataTable * {
          border: 0 !important;
        }

        .dt-loading {
          display: block !important;
          min-height: 4rem;
          position: relative;
        }

        .dt-loading > * {
          display: none !important;
        }

        .dt-loading::before {
          animation: dt-loading 0.8s linear infinite;
          border: 0.25rem solid #dee2e6;
          border-radius: 50%;
          border-top-color: #6c757d;
          content: "";
          height: 2rem;
          left: calc(50% - 1rem);
          position: absolute;
          top: 1rem;
          width: 2rem;
        }

        @keyframes dt-loading {
          to {
            transform: rotate(360deg);
          }
        }

        div.dtsp-panesContainer div.dtsp-title {
          padding: 2px 0 !important;
        }

        .dtsp-bordered,
        .dtsp-bordered:hover,
        .dtsp-searchPane,
        .dtsp-searchPane:hover {
          border: 0 !important;
        }

        .dtsp-topRow button:hover,
        .dtsp-topRow input:hover,
        .dtsp-topRow button:focus,
        .dtsp-topRow input:focus,
        .dtsp-topRow button,
        .dtsp-topRow input {
          margin: 0 !important;
        }

        .dtsp-paneButton.clearButton {
          font-size: 2.5rem;
          line-height: 0;
          padding-bottom: 6px;
        }

        .dtsp-paneButton.clearButton:not(:disabled) {
          color: #dc3545 !important;
          font-size: 3rem;
        }

        .dtsp-topRow {
          background-color: #1e2d59;
          background-image: url(/w/extensions/wikivisor/images/embossed-leather-pattern.png);
          background-blend-mode: darken;
          background-size: cover;
          border: 1px solid rgba(255, 255, 255, 0.2);
          box-shadow: inset 0 0 30px rgba(0, 0, 0, 0.75);
        }
        .dtsp-caret {
          align-items: end;
          display: inline-flex !important;
          font-size: 0 !important;
          height: 1rem;
          justify-content: center;
          line-height: 1;
          width: 1rem;
          align-items: center;
        }

        div.dtsp-topRow button.dtsp-collapseButton span.dtsp-caret {
          top: unset !important;
        }

        .dtsp-caret::before {
          content: "\\2335";
          font-size: 1rem;
          line-height: 1;
        }

        [aria-expanded="true"] .dtsp-caret::before,
        .dtsp-caret-u::before,
        .dtsp-caret-up::before {
          content: "\\2303";
        }

        div.dtsp-searchPane div.dtsp-topRow div.dtsp-searchCont input.dtsp-search {
          padding-left: 8px;
        }

        .dt-filter-toggle {
          align-items: center;
          background: var(--bs-primary, #0d6efd);
          border: 0;
          border-radius: 0.25rem;
          color: #fff;
          display: inline-flex;
          flex: 0 0 auto;
          font-weight: 600;
          gap: 0.375rem;
          line-height: 1.25;
          padding: 0.5rem 0.75rem;
        }

        .dt-filter-toolbar {
          align-items: center;
          display: flex;
          gap: 0.75rem;
          justify-content: space-between;
          margin-bottom: 0.75rem;
          width: 100%;
        }

        .dt-filter-toolbar .dt-search {
          align-items: center;
          display: flex;
          flex: 1 1 auto;
          gap: 0.5rem;
          justify-content: flex-end;
          margin: 0 !important;
        }

        .dt-filter-toolbar .dt-search input {
          margin-left: 0 !important;
          max-width: 24rem;
          width: 100%;
        }

        @media (max-width: 576px) {
          .dt-filter-toolbar {
            align-items: stretch;
            flex-direction: column;
          }

          .dt-filter-toolbar .dt-filter-toggle,
          .dt-filter-toolbar .dt-search,
          .dt-filter-toolbar .dt-search input {
            max-width: none;
            width: 100%;
          }
        }

        .dt-filter-toggle:focus {
          box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.25);
          outline: 0;
        }

        .dt-filter-toggle::before {
          content: "\\2630";
          font-size: 1rem;
          line-height: 1;
        }

        .dt-filter-backdrop {
          background: rgba(0, 0, 0, 0);
          inset: 0;
          opacity: 0;
          pointer-events: none;
          position: fixed;
          transition: opacity 180ms ease;
          z-index: 1040;
        }

        .dt-filter-backdrop.is-open {
          opacity: 1;
          pointer-events: auto;
        }

        .dt-filter-panel {
          background-color: #11161b !important;
          border-color: var(--bg-content);
          border-style: solid;
          border-width: 0.5rem;
          border-bottom-right-radius: 0.625rem;
          border-top-right-radius: 0.625rem;
          color: #fff;
          display: flex;
          flex-direction: column;
          height: 100vh;
          left: 0;
          max-width: min(92vw, 20rem);
          overflow: hidden;
          position: fixed;
          top: 0;
          transform: translateX(-100%);
          transition: transform 220ms ease;
          width: 20rem;
          z-index: 1050;
        }

        .dt-filter-panel.is-open {
          box-shadow: 0 0 20px rgba(255, 255, 255, 0.2);
          transform: translateX(0);
        }

        .dt-filter-panel__header {
          align-items: center;
          border-bottom: 1px solid var(--accent);
          display: flex;
          flex: 0 0 auto;
          justify-content: space-between;
          padding: 0.75rem 1rem;
        }

        .dt-filter-panel__title {
          font-size: 1.4rem;
          font-weight: 600;
          margin: 0;
          border-bottom: 0;
        }

        .dt-filter-panel__close {
          background: transparent;
          border: 0;
          color: rgb(124, 124, 124);
          font-size: 2.5rem;
          line-height: 1;
          padding: 0.125rem 0.375rem 6px 0.375rem;
          width: 35px;
          height: 35px;
          display: flex;
          align-items: center;
          justify-content: center;
        }

        .dt-filter-panel__body {
          flex: 1 1 auto;
          overflow: auto;
          padding: 0.75rem;
        }

        .dt-filter-panel__body .dtsp-panesContainer {
          margin: 0 !important;
        }

        .dt-filter-panel__body .dtsp-searchPanes {
          display: block !important;
        }

        .dt-filter-panel__body .dtsp-searchPane {
          margin-bottom: 0.75rem !important;
          width: 100% !important;
        }

        body.dt-filter-panel-open {
          overflow: hidden;
        }

        /*
         * The series table uses SearchBuilder as a single, fixed selector.
         * Keep only the criterion's value dropdown visible.
         */
        #series .dt-container .dtsb-titleRow,
        #series .dt-container .dtsb-logicContainer,
        #series .dt-container .dtsb-data,
        #series .dt-container .dtsb-condition,
        #series .dt-container .dtsb-add,
        #series .dt-container .dtsb-clearAll,
        #series .dt-container .dtsb-clearGroup,
        #series .dt-container .dtsb-search,
        #series .dt-container .dtsb-buttonContainer,
        #series .dt-container .dtsb-joiner,
        #series .dt-container .dtsb-delete,
        #series .dt-container .dtsb-left,
        #series .dt-container .dtsb-right {
          display: none !important;
        }

        #series .dt-container .dtsb-searchBuilder > :not(.dtsb-group),
        #series .dt-container .dtsb-group > :not(.dtsb-criteria),
        #series .dt-container .dtsb-criteria > :not(.dtsb-inputCont),
        #series .dt-container .dtsb-inputCont > :not(select.dtsb-value) {
          display: none !important;
        }

        #series .dt-container .dtsb-searchBuilder,
        #series .dt-container .dtsb-group,
        #series .dt-container .dtsb-criteria {
          border: 0 !important;
          margin: 0 !important;
          padding: 0 !important;
        }

        #series .dt-container .dtsb-criteria {
          display: block !important;
        }

        #series .dt-container .dtsb-inputCont {
          display: block !important;
          width: min(100%, 24rem) !important;
        }

        #series .dt-container .dtsb-inputCont > :not(.dtsb-value) {
          display: none !important;
        }

        #series .dt-container .dtsb-value {
          display: block !important;
          width: 100% !important;
        }

        aside.is-open .sidebar-drawer {
          display: inline-block;
          right: 0;
          left: 284px;
          border: 0;
          z-index: 3;
        }

        aside.dt-filter-panel {
          background-image: url(/w/images/d/de/File_Icon-Guardians-semitransparent.png);
          background-position: bottom;
          background-repeat: no-repeat;
        }

        .dt-search label {
          display: none;
        }

        #players tbody tr td:last-child {
          color: var(--bg-shade-6);
        }

        #players .player-games {
          align-items: baseline;
          display: flex;
          flex-wrap: wrap;
          gap: 0.25rem 0.5rem;
          position: relative;
        }

        #players .player-games__more {
          display: inline-block;
          position: relative;
          margin-left: auto;
        }

        #players .player-games__toggle {
          background: none;
          color: var(--bs-link-color, #6ea8fe);
          cursor: pointer;
          font-size: 0.875em;
          list-style: none;
          padding: 0;
          white-space: nowrap;
        }

        #players .player-games__toggle::-webkit-details-marker {
          display: none;
        }

        #players .player-games__toggle:hover,
        #players .player-games__toggle:focus {
          text-decoration: underline;
        }

        #players .player-games__popup {
          background-color: var(--bg-shade-4, #11161b);
          border: 1px solid rgba(255, 255, 255, 0.125) !important;
          border-radius: 0.25rem;
          box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.35);
          color: var(--bs-body-color, #fff);
          display: flex;
          flex-direction: column;
          gap: 0.25rem;
          left: 0;
          line-height: 1.5;
          min-width: max-content;
          padding: 0.5rem 0.75rem;
          position: absolute;
          top: calc(100% + 0.25rem);
          white-space: nowrap;
          z-index: 20;
        }
        
        .player-games__popup::before {
			display: inline-block;
			width: 6px;
			height: 6px;
			content: '';
			border-left: 3px solid rgba(255, 255, 255, .25);
			border-top: 3px solid rgba(255, 255, 255, .25);
			border-right: 3px solid transparent;
			border-bottom: 3px solid transparent;
			transform: rotate(45deg);
			margin-top: -12px;
        }
      `,
    }).appendTo("head");
  }

  $(allSelectors.join(",")).addClass("dt-loading").attr("aria-busy", "true");

  /* Load JavaScript. */
  $.when(
    mw.loader.getScript(
      "https://cdn.datatables.net/v/dt/moment-2.29.4/dt-2.3.7/b-3.2.6/b-colvis-3.2.6/b-html5-3.2.6/b-print-3.2.6/cc-1.2.0/date-1.6.3/fh-4.0.5/r-3.0.8/sc-2.4.3/sb-1.8.4/sp-2.3.5/sl-3.1.3/sr-1.4.3/datatables.min.js",
    ),
  ).then(
    () => {
      window.datatablesLoaded = true;
      initDataTable();
    },
    (e) => mw.log.error(e.message),
  );
}

/* ------------- DataTables configuration ---------------------------- */

function initDataTable() {
  function getDataTableColumnCount(table) {
    var headerRow =
      table.tHead && table.tHead.rows.length
        ? table.tHead.rows[table.tHead.rows.length - 1]
        : null;

    if (headerRow) {
      return headerRow.cells.length;
    }

    return table.rows.length ? table.rows[0].cells.length : 0;
  }

  function normaliseDataTableRows(table, expectedColumnCount) {
    $("tbody tr", table).each(function () {
      var row = this;
      var cellCount = row.cells.length;

      if (cellCount === 1 && row.cells[0].colSpan > 1) {
        return;
      }

      while (cellCount < expectedColumnCount) {
        row.appendChild(document.createElement("td"));
        cellCount++;
      }
    });
  }

  function waitForTableToSettle(table, callback) {
    var settleTimer;
    var timeoutTimer;
    var observer;
    var settled = false;

    function finish() {
      if (settled) return;

      settled = true;
      clearTimeout(settleTimer);
      clearTimeout(timeoutTimer);

      if (observer) {
        observer.disconnect();
      }

      callback();
    }

    function scheduleFinish() {
      clearTimeout(settleTimer);
      settleTimer = setTimeout(finish, 500);
    }

    if (window.MutationObserver) {
      observer = new MutationObserver(scheduleFinish);
      observer.observe(table.tBodies[0] || table, {
        childList: true,
        subtree: true,
      });
    }

    timeoutTimer = setTimeout(finish, 30000);
    scheduleFinish();
  }

  function getAcceptsValues(html) {
    return (html || "")
      .split(/<br\s*\/?>/i)
      .map((value) => value.trim())
      .filter(Boolean);
  }

  function getPlainAcceptsValues(html) {
    return getAcceptsValues(html)
      .map(function (value) {
        var container = document.createElement("div");

        container.innerHTML = value;

        return (container.textContent || "").trim();
      })
      .filter(Boolean);
  }

  function renderPlayerGames(html) {
    var games = getAcceptsValues(html);

    if (games.length < 2) {
      return games[0] || "";
    }

    return (
      '<div class="player-games">' +
      '<span class="player-games__first">' +
      games[0] +
      "</span>" +
      '<details class="player-games__more">' +
      '<summary class="player-games__toggle">+' +
      (games.length - 1) +
      " more</summary>" +
      '<div class="player-games__popup" role="group" aria-label="Other games">' +
      games
        .slice(1)
        .map(function (game) {
          return "<div>" + game + "</div>";
        })
        .join("") +
      "</div></details></div>"
    );
  }

  function getPlainSearchPaneRender() {
    return {
      display: function (data) {
        return data;
      },
      sp: function (data) {
        return getPlainAcceptsValues(data);
      },
    };
  }

  function getGameModeText(html) {
    var container = document.createElement("div");
    var values = [];

    container.innerHTML = html || "";
    values.push(container.textContent || "");

    container.querySelectorAll("*").forEach(function (element) {
      ["alt", "title", "aria-label", "class", "src"].forEach(
        function (attribute) {
          var value = element.getAttribute(attribute);

          if (value) {
            values.push(value);
          }
        },
      );
    });

    return values.join(" ");
  }

  function getGameModeValue(html) {
    var text = getGameModeText(html).toLowerCase().replace(/[_-]+/g, " ");

    if (/\b(single\s*player|singleplayer|solo|sp)\b/.test(text)) {
      return "SP";
    }

    if (/\b(multi\s*player|multiplayer|mp)\b/.test(text)) {
      return "MP";
    }

    if (/\b(co\s*op|coop|cooperative)\b/.test(text)) {
      return "Coop";
    }

    return "";
  }

  function getGameModeValues(html) {
    var selectedModes = new Set(
      getAcceptsValues(html).map(getGameModeValue).filter(Boolean),
    );

    return ["SP", "MP", "Coop"].filter(function (mode) {
      return selectedModes.has(mode);
    });
  }

  function getGameModeDisplayValue(html, mode) {
    var container = document.createElement("div");
    var link;

    container.innerHTML = html || "";
    link = container.querySelector("a[href]");

    if (!link) {
      return mode;
    }

    link = link.cloneNode(false);
    link.textContent = mode;

    return link.outerHTML;
  }

  function getGameModeDisplayValues(html) {
    var valuesByMode = {};

    getAcceptsValues(html).forEach(function (value) {
      var mode = getGameModeValue(value);

      if (mode && !valuesByMode[mode]) {
        valuesByMode[mode] = getGameModeDisplayValue(value, mode);
      }
    });

    return ["SP", "MP", "Coop"]
      .filter(function (mode) {
        return Object.prototype.hasOwnProperty.call(valuesByMode, mode);
      })
      .map(function (mode) {
        return valuesByMode[mode];
      });
  }

  function normaliseColumnName(value) {
    return String(value || "")
      .trim()
      .toLowerCase()
      .replace(/[\s_-]+/g, "");
  }

  function getTableConfigElement(table) {
    return (
      table.closest(
        "[data-hidden-columns], [data-hide-columns], [data-dt-hidden-columns], [data-dt-hide-columns], [data-pane-columns], [data-search-pane-columns], [data-dt-pane-columns], [data-first-column-name], [data-first-column], [data-page-column-name], [data-dt-first-column-name]",
      ) || table.parentElement
    );
  }

  function getFirstColumnName(table) {
    var configElement = getTableConfigElement(table);
    var columnName = configElement
      ? configElement.getAttribute("data-first-column-name") ||
        configElement.getAttribute("data-first-column") ||
        configElement.getAttribute("data-page-column-name") ||
        configElement.getAttribute("data-dt-first-column-name")
      : "";

    return String(columnName || "Page").trim() || "Page";
  }

  function getHiddenColumnIndexes(table, titles) {
    var configElement = getTableConfigElement(table);
    var rawValue = configElement
      ? configElement.getAttribute("data-hidden-columns") ||
        configElement.getAttribute("data-hide-columns") ||
        configElement.getAttribute("data-dt-hidden-columns") ||
        configElement.getAttribute("data-dt-hide-columns")
      : "";
    var titleIndexes = {};
    var hiddenIndexes = new Set();

    titles.forEach(function (title, index) {
      titleIndexes[normaliseColumnName(title)] = index;
    });

    String(rawValue || "")
      .split(/[,\|]/)
      .map(function (value) {
        return value.trim();
      })
      .filter(Boolean)
      .forEach(function (value) {
        var numberValue = Number(value);
        var normalisedValue = normaliseColumnName(value);

        if (
          Number.isInteger(numberValue) &&
          numberValue >= 0 &&
          numberValue < titles.length
        ) {
          hiddenIndexes.add(numberValue);
          return;
        }

        if (
          Object.prototype.hasOwnProperty.call(titleIndexes, normalisedValue)
        ) {
          hiddenIndexes.add(titleIndexes[normalisedValue]);
        }
      });

    return hiddenIndexes;
  }

  function getPaneColumnIndexes(table, titles, fallbackIndexes) {
    var configElement = getTableConfigElement(table);
    var attributes = [
      "data-pane-columns",
      "data-search-pane-columns",
      "data-dt-pane-columns",
    ];
    var configuredAttribute = attributes.find(function (attribute) {
      return configElement && configElement.hasAttribute(attribute);
    });
    var titleIndexes = {};
    var paneIndexes = [];

    if (!configuredAttribute) {
      return fallbackIndexes.slice();
    }

    titles.forEach(function (title, index) {
      titleIndexes[normaliseColumnName(title)] = index;
    });

    String(configElement.getAttribute(configuredAttribute) || "")
      .split(/[,\|]/)
      .map(function (value) {
        return value.trim();
      })
      .filter(Boolean)
      .forEach(function (value) {
        var numberValue = Number(value);
        var normalisedValue = normaliseColumnName(value);
        var index = Number.isInteger(numberValue)
          ? numberValue
          : titleIndexes[normalisedValue];

        if (
          Number.isInteger(index) &&
          index >= 0 &&
          index < titles.length &&
          paneIndexes.indexOf(index) === -1
        ) {
          paneIndexes.push(index);
        }
      });

    return paneIndexes;
  }

  function getTableData(table, columnCount) {
    return Array.from(table.tBodies[0] ? table.tBodies[0].rows : [])
      .filter((row) => !(row.cells.length === 1 && row.cells[0].colSpan > 1))
      .map((row) => {
        var data = Array.from(row.cells, (cell) => cell.innerHTML.trim());

        while (data.length < columnCount) {
          data.push("");
        }

        return data.slice(0, columnCount);
      });
  }

  function buildTableLayout(table, titles) {
    var headerRow = document.createElement("tr");
    var tableHead = document.createElement("thead");

    titles.forEach((title) => {
      var headerCell = document.createElement("th");
      headerCell.textContent = title;
      headerRow.appendChild(headerCell);
    });

    table.replaceChildren(tableHead, document.createElement("tbody"));
    tableHead.appendChild(headerRow);
  }

  function suppressSearchPaneBorders(container) {
    if (!container) {
      return;
    }

    function removeBorderClass() {
      container.querySelectorAll(".dtsp-bordered").forEach(function (element) {
        element.classList.remove("dtsp-bordered");
      });
    }

    removeBorderClass();

    new MutationObserver(removeBorderClass).observe(container, {
      attributes: true,
      attributeFilter: ["class"],
      childList: true,
      subtree: true,
    });
  }

  function openFellowshipLinksInNewTab(dataTable) {
    dataTable
      .column(0)
      .nodes()
      .toArray()
      .forEach(function (cell) {
        cell.querySelectorAll("a").forEach(function (link) {
          link.target = "_blank";
        });
      });
  }

  function setupFilterToolbar(wrapper, toggle) {
    var toolbar = document.createElement("div");
    var search = wrapper.querySelector(".dt-search");
    var searchRow = search ? search.closest(".dt-layout-row") : null;

    toolbar.className = "dt-filter-toolbar";

    if (searchRow && searchRow.parentNode) {
      searchRow.parentNode.insertBefore(toolbar, searchRow);
    } else {
      wrapper.insertBefore(toolbar, wrapper.firstChild);
    }

    toolbar.appendChild(toggle);

    if (search) {
      toolbar.appendChild(search);
    }

    if (
      searchRow &&
      searchRow.parentNode &&
      !searchRow.querySelector(".dt-search")
    ) {
      searchRow.remove();
    }
  }

  function getCurrentPageName() {
    var pageName = "";

    if (window.mw && mw.config) {
      pageName = mw.config.get("wgTitle") || mw.config.get("wgPageName") || "";
    }

    if (!pageName) {
      pageName =
        (document.querySelector("#firstHeading") || {}).textContent ||
        document.title ||
        "";
    }

    return pageName.replace(/_/g, " ").trim();
  }

  function getFilterPanelTitle(titles, hiddenColumnIndexes) {
    var pageName = getCurrentPageName();

    if (hiddenColumnIndexes && hiddenColumnIndexes.size === 1) {
      return titles[Array.from(hiddenColumnIndexes)[0]] + ": " + pageName;
    }

    return pageName;
  }

  function setupFilterOffCanvas(dataTable, table, titles, hiddenColumnIndexes) {
    var wrapper = table.closest(".dt-container") || table.parentNode;
    var panes = wrapper ? wrapper.querySelector(".dtsp-panesContainer") : null;

    if (!wrapper || !panes) {
      return null;
    }

    if (wrapper.querySelector(".dt-filter-toggle")) {
      return document.getElementById(
        wrapper
          .querySelector(".dt-filter-toggle")
          .getAttribute("aria-controls"),
      );
    }

    var toggle = document.createElement("button");
    var panel = document.createElement("aside");
    var backdrop = document.createElement("div");
    var drawer = document.createElement("button");
    var header = document.createElement("div");
    var title = document.createElement("h2");
    var close = document.createElement("button");
    var body = document.createElement("div");
    var panelId = "dt-filter-panel-" + $(".dt-filter-panel").length;
    var lastFocusedElement = null;

    toggle.type = "button";
    toggle.className = "dt-filter-toggle";
    toggle.textContent = "Filters";
    toggle.setAttribute("aria-controls", panelId);
    toggle.setAttribute("aria-expanded", "false");

    panel.id = panelId;
    panel.className = "dt-filter-panel";
    panel.setAttribute("aria-hidden", "true");
    panel.setAttribute("aria-label", "Data filters");

    backdrop.className = "dt-filter-backdrop";
    drawer.className = "sidebar-drawer";
    drawer.setAttribute("type", "button");
    drawer.setAttribute("tabindex", "0");

    header.className = "dt-filter-panel__header";
    title.className = "dt-filter-panel__title";
    title.textContent = getFilterPanelTitle(titles, hiddenColumnIndexes);
    close.type = "button";
    close.className = "dt-filter-panel__close";
    close.setAttribute("aria-label", "Close filters");
    close.innerHTML = "&times;";

    body.className = "dt-filter-panel__body";

    header.append(title, close);
    body.appendChild(panes);
    panel.append(drawer, header, body);

    setupFilterToolbar(wrapper, toggle);
    document.body.append(backdrop, panel);

    function openPanel() {
      lastFocusedElement = document.activeElement;
      panel.classList.add("is-open");
      backdrop.classList.add("is-open");
      document.body.classList.add("dt-filter-panel-open");
      panel.setAttribute("aria-hidden", "false");
      toggle.setAttribute("aria-expanded", "true");
      close.focus();
      dataTable.columns.adjust();
    }

    function closePanel() {
      panel.classList.remove("is-open");
      backdrop.classList.remove("is-open");
      document.body.classList.remove("dt-filter-panel-open");
      panel.setAttribute("aria-hidden", "true");
      toggle.setAttribute("aria-expanded", "false");

      if (lastFocusedElement && document.contains(lastFocusedElement)) {
        lastFocusedElement.focus();
      }

      dataTable.columns.adjust();
    }

    toggle.addEventListener("click", openPanel);
    close.addEventListener("click", closePanel);
    drawer.addEventListener("click", closePanel);
    backdrop.addEventListener("click", closePanel);

    document.addEventListener("keydown", function (event) {
      if (event.key === "Escape" && panel.classList.contains("is-open")) {
        closePanel();
      }
    });

    return panel;
  }

  $(".dataTable")
    .not(allSelectors.join(","))
    .each(function () {
      var table = this;

      waitForTableToSettle(table, function () {
        if ($.fn.dataTable.isDataTable(table)) {
          return;
        }

        normaliseDataTableRows(table, getDataTableColumnCount(table));

        $(table).DataTable({
          language: {
            search: "",
            searchPlaceholder: "Search",
            searchPanes: {
              title: {
                _: "Filters: %d",
                0: "",
              },
            },
          },
          retrieve: true,
          order: [[0, "asc"]],
          pageLength: 1000,
        });
      });
    });

  $(seriesSelectors.join(",")).each(function () {
    var table = this;

    waitForTableToSettle(table, function () {
      if ($.fn.dataTable.isDataTable(table)) {
        return;
      }

      var titles = ["Series", "Games"];

      /*
       * The source table is emitted as Games, Series.
       * DataTables uses Series, Games, so Series is column 0.
       */
      var data = getTableData(table, titles.length).map(function (row) {
        return {
          series: row[1],
          games: row[0],
        };
      });

      normaliseDataTableRows(table, titles.length);
      buildTableLayout(table, titles);

      try {
        var dataTable = new DataTable(table, {
          data: data,

          columns: [
            {
              data: "games",
              name: "Games",
              title: "Game",
            },
            {
              data: "series",
              name: "Series",
              title: "Series",
              visible: false,
              searchable: true,
              type: "string",
            },
          ],

          layout: {
            topStart: null,
            topEnd: null,
            top1: {
              searchBuilder: {
                columns: [1],

                preDefined: {
                  logic: "AND",
                  criteria: [
                    {
                      data: "series",
                      origData: "series",
                      condition: "=",
                      type: "string",
                    },
                  ],
                },
              },
            },
            top2: {},
          },

          language: {
            searchBuilder: {
              data: "Series",
              value: "Select a series",
            },
          },

          order: [[0, "asc"]],
          paging: false,

          initComplete: function () {
            var api = this.api();
            var container = $(api.table().container());

            function emptySeriesCriterion() {
              return {
                logic: "AND",
                criteria: [
                  {
                    data: "series",
                    origData: "series",
                    condition: "=",
                    type: "string",
                  },
                ],
              };
            }

            function enableAllSeriesOption() {
              var valueSelect = container.find(".dtsb-value").first();
              var emptyOption = valueSelect
                .find('option[value="--valueTitle--"]')
                .first();

              emptyOption
                .prop("disabled", false)
                .prop("hidden", false)
                .text("All series");
            }

            container[0].addEventListener(
              "change",
              function (event) {
                var select = event.target;

                if (
                  !select.matches(".dtsb-value") ||
                  select.value !== "--valueTitle--"
                ) {
                  return;
                }

                /*
                 * The placeholder has no SearchBuilder value.
                 * Stop its listener before it calls toString()
                 * on undefined, then reset the criterion here.
                 */
                event.stopImmediatePropagation();

                api.searchBuilder.rebuild(emptySeriesCriterion(), false);
                api.draw(false);
                setTimeout(enableAllSeriesOption, 0);
              },
              true,
            );

            setTimeout(enableAllSeriesOption, 0);
          },
        });

        dataTable.columns.adjust().draw(false);
      } catch (error) {
        mw.log.error("Could not initialise the Series DataTable:", error);
      } finally {
        $(table).removeClass("dt-loading").removeAttr("aria-busy").show();
      }
    });
  });

  $(assetSelectors.join(",")).each(function () {
    var table = this;

    waitForTableToSettle(table, function () {
      if ($.fn.dataTable.isDataTable(table)) {
        return;
      }

      var titles = [
        "Asset",
        "Type",
        "Subtype",
        "Mode",
        "Developer",
        "Released",
        "Status",
        "Description",
        "Version",
        "Files",
        "Game",
      ];

      var paneIndexes = [1, 2, 3, 4, 5, 6, 8];
      if (mw.config.get("wgPageName") === "Mods") {
        paneIndexes = [10, 1, 2, 3, 4, 5, 6, 8];
      }

      var hiddenColumns = [2, 3, 5, 6, 7, 8, 10];
      var data = getTableData(table, titles.length);

      buildTableLayout(table, titles);

      var dataTable = new DataTable(table, {
        columns: titles.map(function (title, index) {
          var column = {
            title: title,
            visible: hiddenColumns.indexOf(index) === -1,
            searchPanes: {
              show: paneIndexes.indexOf(index) !== -1,
            },
          };

          if (index === 0) {
            column.render = function (cellData, type) {
              if (type === "display") {
                return (
                  '<span style="display: list-item; margin-left: 1.25rem; white-space: nowrap">' +
                  cellData +
                  "</span>"
                );
              }

              return cellData;
            };
          }

          if ([1, 2, 3, 4, 6, 8, 10].indexOf(index) !== -1) {
            column.render = {
              display: function (cellData) {
                if (index === 4) {
                  return (
                    '<span style="white-space: nowrap">' +
                    getAcceptsValues(cellData).join(", ") +
                    "</span>"
                  );
                }

                return cellData;
              },
              sp: function (cellData) {
                return getPlainAcceptsValues(cellData);
              },
            };
            column.searchPanes.orthogonal = "sp";
          }

          if (index === 9) {
            column.render = function (cellData, type) {
              if (type !== "display") {
                return cellData;
              }

              var wrapper = document.createElement("div");
              wrapper.innerHTML = cellData;

              var links = Array.from(wrapper.querySelectorAll("a"));

              if (!links.length) {
                return "";
              }

              var downloads = document.createElement("div");

              links.forEach(function (sourceLink, linkIndex) {
                var link = sourceLink.cloneNode(true);

                link.textContent =
                  links.length === 1
                    ? "Download"
                    : "Download " + (linkIndex + 1);
                link.target = "_blank";
                link.rel = "noopener noreferrer nofollow";

                downloads.appendChild(link);

                if (linkIndex < links.length - 1) {
                  downloads.appendChild(document.createElement("br"));
                }
              });

              return downloads.innerHTML;
            };
          }

          return column;
        }),
        layout: {
          topStart: null,
          topEnd: null,
          top2: {
            searchPanes: {
              cascadePanes: true,
              dtOpts: {
                scroller: false,
              },
              initCollapsed: true,
              layout: "columns-1",
              columns: paneIndexes,
            },
          },
          top1: {
            search: {
              className: "mx-0 w-100",
            },
          },
        },
        order: [[0, "asc"]],
        language: {
          search: "",
          searchPlaceholder: "Search assets",
          searchPanes: {
            title: {
              _: "Filters: %d",
              0: "",
            },
          },
        },
        paging: false,
        searchDelay: 400,
      });

      var lastGlobalSearch = dataTable.search();

      dataTable.on("draw.dt", function () {
        var globalSearch = dataTable.search();

        openFellowshipLinksInNewTab(dataTable);

        if (globalSearch === lastGlobalSearch) {
          return;
        }

        lastGlobalSearch = globalSearch;

        paneIndexes.forEach(function (paneIndex) {
          dataTable.searchPanes.rebuildPane(paneIndex, true);
        });
      });

      dataTable.rows.add(data).draw();
      $(table).removeClass("dt-loading").removeAttr("aria-busy").show();
      dataTable.searchPanes.rebuildPane();

      var filterPanel = setupFilterOffCanvas(
        dataTable,
        table,
        titles,
        new Set(),
      );

      suppressSearchPaneBorders(filterPanel);
      dataTable.columns.adjust().draw(false);
    });
  });

  $(clanSelectors.join(",")).each(function () {
    var table = this;

    waitForTableToSettle(table, function () {
      if ($.fn.dataTable.isDataTable(table)) {
        return;
      }

      var titles = ["Clan", "Games", "Links", "Description", "Status"];
      var paneIndexes = [1, 4];
      var clanColorClasses = Array.from(
        table.tBodies[0] ? table.tBodies[0].rows : [],
      )
        .filter(function (row) {
          return !(row.cells.length === 1 && row.cells[0].colSpan > 1);
        })
        .map(function (row) {
          return row.cells[0] ? row.cells[0].className : "";
        });
      var data = getTableData(table, titles.length);

      buildTableLayout(table, titles);

      var dataTable = new DataTable(table, {
        data: data,
        columns: [
          {
            title: "Clan",
            createdCell: function (cell, cellData, rowData, rowIndex) {
              var colorClasses = clanColorClasses[rowIndex];

              if (!colorClasses) {
                return;
              }

              colorClasses
                .split(/\s+/)
                .filter(Boolean)
                .forEach(function (className) {
                  cell.classList.add(className);
                });
            },
            render: function (cellData, type) {
              if (type === "display") {
                return (
                  '<span style="display: list-item; margin-left: 1.25rem; white-space: nowrap">' +
                  cellData +
                  "</span>"
                );
              }

              return cellData;
            },
            searchPanes: {
              show: false,
            },
          },
          {
            title: "Games",
            visible: false,
            render: getPlainSearchPaneRender(),
            searchPanes: {
              show: true,
              orthogonal: "sp",
            },
          },
          {
            title: "Links",
            render: function (cellData, type) {
              if (type !== "display") {
                return cellData;
              }

              var container = document.createElement("div");

              container.innerHTML = cellData || "";
              container
                .querySelectorAll("a.external[href]")
                .forEach(function (link) {
                  var hostname;

                  try {
                    hostname = new URL(link.href, window.location.href)
                      .hostname;
                  } catch (error) {
                    hostname = "";
                  }

                  if (hostname) {
                    link.textContent = hostname.replace(/^www\./i, "");
                  }

                  link.target = "_blank";
                  link.rel = "noopener noreferrer nofollow";
                });

              return container.innerHTML;
            },
            searchPanes: {
              show: false,
            },
          },
          {
            title: "Description",
            searchPanes: {
              show: false,
            },
          },
          {
            title: "Status",
            visible: false,
            render: getPlainSearchPaneRender(),
            searchPanes: {
              show: true,
              orthogonal: "sp",
            },
          },
        ],
        layout: {
          topStart: null,
          topEnd: null,
          top2: {
            searchPanes: {
              cascadePanes: true,
              dtOpts: {
                scroller: false,
              },
              initCollapsed: true,
              layout: "columns-1",
              columns: paneIndexes,
            },
          },
          top1: {
            search: {
              className: "mx-0 w-100",
            },
          },
        },
        order: [[0, "asc"]],
        language: {
          search: "",
          searchPlaceholder: "Search clans",
          searchPanes: {
            title: {
              _: "Filters: %d",
              0: "",
            },
          },
        },
        paging: false,
        searchDelay: 400,
      });

      var lastGlobalSearch = dataTable.search();

      dataTable.on("draw.dt", function () {
        var globalSearch = dataTable.search();

        openFellowshipLinksInNewTab(dataTable);

        if (globalSearch === lastGlobalSearch) {
          return;
        }

        lastGlobalSearch = globalSearch;
        paneIndexes.forEach(function (paneIndex) {
          dataTable.searchPanes.rebuildPane(paneIndex, true);
        });
      });

      $(table).removeClass("dt-loading").removeAttr("aria-busy").show();
      dataTable.searchPanes.rebuildPane();

      var filterPanel = setupFilterOffCanvas(
        dataTable,
        table,
        titles,
        new Set(),
      );

      suppressSearchPaneBorders(filterPanel);
      dataTable.columns.adjust().draw(false);
    });
  });

  $(playerSelectors.join(",")).each(function () {
    var table = this;

    waitForTableToSettle(table, function () {
      if ($.fn.dataTable.isDataTable(table)) {
        return;
      }

      var fallbackTitles = [
        "Player",
        "Games",
        "Clans",
        "Roles",
        "Clan (Role)",
        "Status",
      ];
      var headerCells =
        table.tHead && table.tHead.rows.length
          ? Array.from(table.tHead.rows[table.tHead.rows.length - 1].cells)
          : [];
      var titles = fallbackTitles.map(function (fallbackTitle, index) {
        var title = headerCells[index]
          ? headerCells[index].textContent.trim()
          : "";

        return title || fallbackTitle;
      });

      var hiddenColumnIndexes = new Set([2, 3, 5]);
      var paneIndexes = getPaneColumnIndexes(table, titles, [2, 3, 5]);
      var playerColorClasses = Array.from(
        table.tBodies[0] ? table.tBodies[0].rows : [],
      )
        .filter(function (row) {
          return !(row.cells.length === 1 && row.cells[0].colSpan > 1);
        })
        .map(function (row) {
          return row.cells[0] ? row.cells[0].className : "";
        });

      var data = getTableData(table, titles.length);

      buildTableLayout(table, titles);

      var dataTable = new DataTable(table, {
        columns: titles.map(function (title, index) {
          var column = {
            title: title,
            visible: !hiddenColumnIndexes.has(index),
            searchPanes: {
              show: paneIndexes.indexOf(index) !== -1,
            },
          };

          if (index === 0) {
            column.createdCell = function (cell, cellData, rowData, rowIndex) {
              var colorClasses = playerColorClasses[rowIndex];

              if (!colorClasses) {
                return;
              }

              colorClasses
                .split(/\s+/)
                .filter(Boolean)
                .forEach(function (className) {
                  cell.classList.add(className);
                });
            };
            column.render = function (cellData, type) {
              if (type === "display") {
                return (
                  '<span style="display: list-item; margin-left: 1.25rem; white-space: nowrap">' +
                  cellData +
                  "</span>"
                );
              }

              return cellData;
            };
          } else if (index === 1) {
            column.render = function (cellData, type) {
              if (type === "display") {
                return renderPlayerGames(cellData);
              }

              if (type === "sp") {
                return getPlainAcceptsValues(cellData);
              }

              return cellData;
            };

            if (paneIndexes.indexOf(index) !== -1) {
              column.searchPanes.orthogonal = "sp";
            }
          } else if (paneIndexes.indexOf(index) !== -1) {
            column.render = {
              display: function (cellData) {
                return cellData;
              },
              sp: function (cellData) {
                var container = document.createElement("div");
                var linkedValues;
                var blockValues;

                container.innerHTML = cellData || "";
                linkedValues = Array.from(
                  container.querySelectorAll("a"),
                  function (link) {
                    return link.textContent.trim();
                  },
                ).filter(Boolean);

                if (linkedValues.length) {
                  return linkedValues;
                }

                blockValues = Array.from(
                  container.children,
                  function (element) {
                    return element.textContent.trim();
                  },
                ).filter(Boolean);

                return blockValues.length
                  ? blockValues
                  : getAcceptsValues(cellData);
              },
            };
            column.searchPanes.orthogonal = "sp";
          }

          return column;
        }),
        layout: {
          topStart: null,
          topEnd: null,
          top2: {
            searchPanes: {
              cascadePanes: true,
              dtOpts: {
                scroller: false,
              },
              initCollapsed: true,
              layout: "columns-1",
              columns: paneIndexes,
            },
          },
          top1: {
            search: {
              className: "mx-0 w-100",
            },
          },
        },
        order: [[0, "asc"]],
        language: {
          search: "",
          searchPlaceholder: "Search players",
          searchPanes: {
            title: {
              _: "Filters: %d",
              0: "",
            },
          },
        },
        paging: false,
        searchDelay: 400,
      });

      var lastGlobalSearch = dataTable.search();

      dataTable.on("draw.dt", function () {
        var globalSearch = dataTable.search();

        openFellowshipLinksInNewTab(dataTable);

        if (globalSearch === lastGlobalSearch) {
          return;
        }

        lastGlobalSearch = globalSearch;

        paneIndexes.forEach(function (paneIndex) {
          dataTable.searchPanes.rebuildPane(paneIndex, true);
        });
      });

      dataTable.rows.add(data).draw();
      $(table).removeClass("dt-loading").removeAttr("aria-busy").show();
      dataTable.searchPanes.rebuildPane();

      var filterPanel = setupFilterOffCanvas(
        dataTable,
        table,
        titles,
        hiddenColumnIndexes,
      );

      suppressSearchPaneBorders(filterPanel);
      dataTable.columns.adjust().draw(false);
    });
  });

  $(selectors.join(",")).each(function () {
    var table = this;

    waitForTableToSettle(table, function () {
      if ($.fn.dataTable.isDataTable(table)) {
        return;
      }

      var titles = [
        getFirstColumnName(table),
        "Developer",
        "Publisher",
        "Genre",
        "Platform",
        "Mode",
        "Released",
        "Category",
      ];
      var hiddenColumnIndexes = getHiddenColumnIndexes(table, titles);
      var paneIndexes = [1, 2, 3, 4, 5, 7].filter(function (paneIndex) {
        return !hiddenColumnIndexes.has(paneIndex);
      });
      var data = getTableData(table, titles.length);

      buildTableLayout(table, titles);

      var dataTable = new DataTable(table, {
        columns: [
          {
            title: titles[0],
            visible: !hiddenColumnIndexes.has(0),
            render: function (data, type) {
              if (type === "display") {
                return (
                  '<span style="display: list-item; margin-left: 1.25rem; white-space: nowrap">' +
                  data +
                  "</span>"
                );
              }

              return data;
            },
            searchPanes: {
              show: false,
            },
          },
          {
            title: "Developer",
            visible: !hiddenColumnIndexes.has(1),
            render: getPlainSearchPaneRender(),
            searchPanes: {
              show: !hiddenColumnIndexes.has(1),
              orthogonal: "sp",
            },
          },
          {
            title: "Publisher",
            visible: !hiddenColumnIndexes.has(2),
            render: getPlainSearchPaneRender(),
            searchPanes: {
              show: !hiddenColumnIndexes.has(2),
              orthogonal: "sp",
            },
          },
          {
            title: "Genre",
            visible: !hiddenColumnIndexes.has(3),
            render: getPlainSearchPaneRender(),
            searchPanes: {
              show: !hiddenColumnIndexes.has(3),
              orthogonal: "sp",
            },
          },
          {
            title: "Platform",
            visible: !hiddenColumnIndexes.has(4),
            render: getPlainSearchPaneRender(),
            searchPanes: {
              show: !hiddenColumnIndexes.has(4),
              orthogonal: "sp",
            },
          },
          {
            title: "Mode",
            visible: !hiddenColumnIndexes.has(5),
            render: {
              display: function (data) {
                return getGameModeDisplayValues(data).join(", ");
              },
              sp: function (data) {
                return getGameModeValues(data);
              },
            },
            searchPanes: {
              show: !hiddenColumnIndexes.has(5),
              orthogonal: "sp",
            },
          },
          {
            title: "Released",
            visible: !hiddenColumnIndexes.has(6),
            searchPanes: {
              show: !hiddenColumnIndexes.has(6),
            },
          },
          {
            title: "Category",
            visible: false,
            render: getPlainSearchPaneRender(),
            searchPanes: {
              show: true,
              orthogonal: "sp",
            },
          },
        ],
        layout: {
          topStart: null,
          topEnd: null,
          top2: {
            searchPanes: {
              cascadePanes: true,
              dtOpts: {
                scroller: false,
              },
              initCollapsed: true,
              layout: "columns-1",
              columns: paneIndexes,
            },
          },
          top1: {
            search: {
              className: "mx-0 w-100",
            },
          },
        },
        order: [[0, "asc"]],
        language: {
          search: "",
          searchPlaceholder: "Search",
          searchPanes: {
            title: {
              _: "Filters: %d",
              0: "",
            },
          },
        },
        paging: false,
        searchDelay: 400,
      });

      var lastGlobalSearch = dataTable.search();

      dataTable.on("draw.dt", function () {
        var globalSearch = dataTable.search();

        openFellowshipLinksInNewTab(dataTable);

        if (globalSearch === lastGlobalSearch) {
          return;
        }

        lastGlobalSearch = globalSearch;

        paneIndexes.forEach(function (paneIndex) {
          dataTable.searchPanes.rebuildPane(paneIndex, true);
        });
      });

      var filterPanel;

      dataTable.rows.add(data).draw();
      $(table).removeClass("dt-loading").removeAttr("aria-busy").show();
      dataTable.searchPanes.rebuildPane();
      filterPanel = setupFilterOffCanvas(
        dataTable,
        table,
        titles,
        hiddenColumnIndexes,
      );
      suppressSearchPaneBorders(filterPanel);
      dataTable.columns.adjust().draw(false);
    });
  });
}