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
 
(55 intermediate revisions by the same user not shown)
Line 1: Line 1:
var selectors = [
var selectors = [
"#games > table"
    "#games > table",
    "[data-datatable='games'] > table",
    "[data-table='games'] > table",
    "[data-table-type='games'] > table"
];
];


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


Line 14: Line 17:


if (found === true) {
if (found === true) {
  /* Load CSS. */
    /* Load CSS. */
  const dtsource =
    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";
        "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"]')) {
    if (!document.querySelector('link[href*="datatables.min.css"]')) {
    $("<link/>", { rel: "stylesheet", href: dtsource }).appendTo("head");
        $("<link/>", { rel: "stylesheet", href: dtsource }).appendTo("head");
  }
    }


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


table.dataTable, table.dataTable * {
  border: 0 !important;
}
         .dt-loading {
         .dt-loading {
           display: block !important;
           display: block !important;
Line 58: Line 66:
             transform: rotate(360deg);
             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;
         }
         }


         .dtsp-searchPane, .dtsp-searchPane:hover {
        [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: 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;
         }
         }


         .dtsp-paneButton.clearButton {
         .dt-filter-toolbar {
           font-size: 1.5rem;
          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;
           line-height: 1;
          padding: 0.125rem 0.375rem 6px 0.375rem;
          width: 35px;
          height: 35px;
  display: flex;
  align-items: center;
  justify-content: center;
         }
         }


         .dtsp-paneButton.clearButton:not(:disabled) {
         .dt-filter-panel__body {
           color: var(--bs-danger, #dc3545);
          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;
         }
         }
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;
}
       `,
       `,
    }).appendTo("head");
        }).appendTo("head");
  }
    }


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


  /* Load JavaScript. */
    /* Load JavaScript. */
  $.when(
    $.when(
    mw.loader.getScript(
        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",
            "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(
    ).then(
    () => {
        () => {
      window.datatablesLoaded = true;
            window.datatablesLoaded = true;
      initDataTable();
            initDataTable();
    },
        },
    (e) => mw.log.error(e.message),
        (e) => mw.log.error(e.message),
  );
    );
}
}


Line 95: Line 327:


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


    if (headerRow) {
        if (headerRow) {
      return headerRow.cells.length;
            return headerRow.cells.length;
        }
 
        return table.rows.length ? table.rows[0].cells.length : 0;
     }
     }


     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;
            }


  function normaliseDataTableRows(table, expectedColumnCount) {
            while (cellCount < expectedColumnCount) {
    $("tbody tr", table).each(function () {
                row.appendChild(document.createElement("td"));
      var row = this;
                cellCount++;
      var cellCount = row.cells.length;
            }
        });
    }


      if (cellCount === 1 && row.cells[0].colSpan > 1) {
    function waitForTableToSettle(table, callback) {
         return;
         var settleTimer;
      }
        var timeoutTimer;
        var observer;
        var settled = false;


      while (cellCount < expectedColumnCount) {
        function finish() {
        row.appendChild(document.createElement("td"));
            if (settled) return;
        cellCount++;
      }
    });
  }


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


    function finish() {
            if (observer) {
      if (settled) return;
                observer.disconnect();
            }


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


      if (observer) {
        function scheduleFinish() {
        observer.disconnect();
            clearTimeout(settleTimer);
      }
            settleTimer = setTimeout(finish, 500);
        }


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


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


     if (window.MutationObserver) {
     function getGameModeText(html) {
      observer = new MutationObserver(scheduleFinish);
        var container = document.createElement("div");
      observer.observe(table.tBodies[0] || table, {
        var values = [];
         childList: true,
 
         subtree: true,
        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(" ");
     }
     }


     timeoutTimer = setTimeout(finish, 30000);
     function getGameModeValue(html) {
    scheduleFinish();
        var text = getGameModeText(html)
  }
            .toLowerCase()
            .replace(/[_-]+/g, " ");


  function getAcceptsValues(html) {
        if (/\b(single\s*player|singleplayer|solo|sp)\b/.test(text)) {
    return (html || "")
            return "SP";
      .split(/<br\s*\/?>/i)
        }
      .map((value) => value.trim())
      .filter(Boolean);
  }


  function getTableData(table, columnCount) {
        if (/\b(multi\s*player|multiplayer|mp)\b/.test(text)) {
    return Array.from(table.tBodies[0] ? table.tBodies[0].rows : [])
            return "MP";
      .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) {
         if (/\b(co\s*op|coop|cooperative)\b/.test(text)) {
          data.push("");
            return "Coop";
         }
         }


         return data.slice(0, columnCount);
         return "";
      });
    }
  }
 
    function getGameModeValues(html) {
        var selectedModes = new Set(getAcceptsValues(html)
            .map(getGameModeValue)
            .filter(Boolean));


  function buildTableLayout(table, titles) {
        return ["SP", "MP", "Coop"].filter(function (mode) {
    var headerRow = document.createElement("tr");
            return selectedModes.has(mode);
    var tableHead = document.createElement("thead");
        });
    }


     titles.forEach((title) => {
     function getGameModeDisplayValue(html, mode) {
      var headerCell = document.createElement("th");
        var container = document.createElement("div");
      headerCell.textContent = title;
        var link;
      headerRow.appendChild(headerCell);
 
    });
        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-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;
                }


    table.replaceChildren(tableHead, document.createElement("tbody"));
                if (Object.prototype.hasOwnProperty.call(titleIndexes, normalisedValue)) {
    tableHead.appendChild(headerRow);
                    hiddenIndexes.add(titleIndexes[normalisedValue]);
  }
                }
            });


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


     removeBorderClass();
     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);
            });
    }


     new MutationObserver(removeBorderClass).observe(container, {
     function buildTableLayout(table, titles) {
      attributes: true,
        var headerRow = document.createElement("tr");
      attributeFilter: ["class"],
        var tableHead = document.createElement("thead");
      childList: true,
      subtree: true,
    });
  }


  function openFellowshipLinksInNewTab(dataTable) {
        titles.forEach((title) => {
    dataTable
            var headerCell = document.createElement("th");
      .column(0)
            headerCell.textContent = title;
      .nodes()
            headerRow.appendChild(headerCell);
      .toArray()
      .forEach(function (cell) {
        cell.querySelectorAll("a").forEach(function (link) {
          link.target = "_blank";
         });
         });
      });
  }


  $(".dataTable")
        table.replaceChildren(tableHead, document.createElement("tbody"));
    .each(function () {
        tableHead.appendChild(headerRow);
      var table = this;
    }


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


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


         $(table).DataTable({
         removeBorderClass();
          dom: "f",
 
          retrieve: true,
        new MutationObserver(removeBorderClass).observe(container, {
          order: [[0, "asc"]],
            attributes: true,
          pageLength: 1000,
            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;


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


$("#games > table").each(function () {
        panel.id = panelId;
    var table = this;
        panel.className = "dt-filter-panel";
        panel.setAttribute("aria-hidden", "true");
        panel.setAttribute("aria-label", "Data filters");


    waitForTableToSettle(table, function () {
        backdrop.className = "dt-filter-backdrop";
      if ($.fn.dataTable.isDataTable(table)) {
        drawer.className = "sidebar-drawer";
        return;
        drawer.setAttribute("type", "button");
      }
        drawer.setAttribute("tabindex", "0");


      var titles = [
        header.className = "dt-filter-panel__header";
      "Game", "Developer", "Publisher", "Genre", "Platform", "Mode", "Released"
        title.className = "dt-filter-panel__title";
      ];
        title.textContent = getFilterPanelTitle(titles, hiddenColumnIndexes);
      var data = getTableData(table, titles.length);
        close.type = "button";
        close.className = "dt-filter-panel__close";
        close.setAttribute("aria-label", "Close filters");
        close.innerHTML = "&times;";


      buildTableLayout(table, titles);
        body.className = "dt-filter-panel__body";


      var dataTable = new DataTable(table, {
        header.append(title, close);
        columns: [
        body.appendChild(panes);
          {
        panel.append(drawer, header, body);
            title: "Game",
            render: function (data, type) {
              if (type === "display") {
                return (
                  '<span style="display: list-item; margin-left: 1.25rem;">' +
                  data +
                  "</span>"
                );
              }


              return data;
        setupFilterToolbar(wrapper, toggle);
            },
         document.body.append(backdrop, panel);
            searchPanes: {
              show: false,
            },
          },
          {
            title: "Developer",
            visible: true,
            searchPanes: {
              show: true,
            },
          },
          {
            title: "Publisher",
            visible: true,
            searchPanes: {
              show: true,
            },
          },
          {
            title: "Genre",
            visible: true,
            searchPanes: {
              show: true,
            },
          },
          {
            title: "Platform",
            visible: true,
            searchPanes: {
              show: true,
            },
          },         
          {
            title: "Mode",
            visible: true,
            render: {
              display: function (data) {
                return data;
              },
              sp: function (data) {
                return getAcceptsValues(data);
              },
            },
            searchPanes: {
              show: true,
              orthogonal: "sp",
            },
          },
          {
            title: "Released",
            visible: true,
            searchPanes: {
              show: true,
            },
          },
         ],
        layout: {
          topStart: null,
          topEnd: null,
          top2: {
            searchPanes: {
              cascadePanes: true,
              initCollapsed: true,
              columns: [1, 2, 3, 4, 5],
            },
          },
          top1: {
            search: {
              className: "mx-0 w-100",
            },
          },
        },
        order: [[0, "asc"]],
        paging: false,
        searchDelay: 400,
      });


      var lastGlobalSearch = dataTable.search();
        function openPanel() {
      var paneIndexes = [1, 2, 3, 4, 5];
            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();
        }


      dataTable.on("draw.dt", function () {
        function closePanel() {
        var globalSearch = dataTable.search();
            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");


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


        if (globalSearch === lastGlobalSearch) {
            dataTable.columns.adjust();
          return;
         }
         }


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


         paneIndexes.forEach(function (paneIndex) {
         document.addEventListener("keydown", function (event) {
          dataTable.searchPanes.rebuildPane(paneIndex, true);
            if (event.key === "Escape" && panel.classList.contains("is-open")) {
                closePanel();
            }
         });
         });
      });


      dataTable.rows.add(data).draw();
        return panel;
      $(".dataTable")
    }
        .removeClass("dt-loading")
 
        .removeAttr("aria-busy")
    $(".dataTable")
        .show();
        .each(function () {
      dataTable.searchPanes.rebuildPane();
            var table = this;
      suppressSearchPaneBorders(document.querySelector(".dataTable"));
 
      dataTable.columns.adjust().draw(false);
            waitForTableToSettle(table, function () {
                if ($.fn.dataTable.isDataTable(table)) {
                    return;
                }
 
                normaliseDataTableRows(table, getDataTableColumnCount(table));
 
                $(table).DataTable({
                    dom: "f",
                    language: {
                        search: "",
                        searchPlaceholder: "Search",
                        searchPanes: {
                            title: {
                                _: 'Filters: %d',
                                0: ''
                            }
                        }
                    },
                    retrieve: true,
                    order: [[0, "asc"]],
                    pageLength: 1000,
                });
            });
        });
 
    $(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),
                        searchPanes: {
                            show: !hiddenColumnIndexes.has(1),
                        },
                    },
                    {
                        title: "Publisher",
                        visible: !hiddenColumnIndexes.has(2),
                        searchPanes: {
                            show: !hiddenColumnIndexes.has(2),
                        },
                    },
                    {
                        title: "Genre",
                        visible: !hiddenColumnIndexes.has(3),
                        searchPanes: {
                            show: !hiddenColumnIndexes.has(3),
                        },
                    },
                    {
                        title: "Platform",
                        visible: !hiddenColumnIndexes.has(4),
                        searchPanes: {
                            show: !hiddenColumnIndexes.has(4),
                        },
                    },
                    {
                        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: {
                            display: function (data) {
                                return data;
                            },
                            sp: function (data) {
                                return getAcceptsValues(data);
                            },
                        },
                        searchPanes: {
                            show: true,
                            orthogonal: "sp",
                        },
                    },
                ],
                layout: {
                    topStart: null,
                    topEnd: null,
                    top2: {
                        searchPanes: {
                            cascadePanes: true,
                            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);
        });
     });
     });
  });
}
}

Latest revision as of 08:27, 23 June 2026

var selectors = [
    "#games > table",
    "[data-datatable='games'] > table",
    "[data-table='games'] > table",
    "[data-table-type='games'] > table"
];

var found = false;
for (var i = 0; i < selectors.length; i++) {
    if (document.querySelector(selectors[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: `
            
        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;
        }
		
		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;
		}
      `,
        }).appendTo("head");
    }

    $(selectors.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 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-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 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")
        .each(function () {
            var table = this;

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

                normaliseDataTableRows(table, getDataTableColumnCount(table));

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

    $(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),
                        searchPanes: {
                            show: !hiddenColumnIndexes.has(1),
                        },
                    },
                    {
                        title: "Publisher",
                        visible: !hiddenColumnIndexes.has(2),
                        searchPanes: {
                            show: !hiddenColumnIndexes.has(2),
                        },
                    },
                    {
                        title: "Genre",
                        visible: !hiddenColumnIndexes.has(3),
                        searchPanes: {
                            show: !hiddenColumnIndexes.has(3),
                        },
                    },
                    {
                        title: "Platform",
                        visible: !hiddenColumnIndexes.has(4),
                        searchPanes: {
                            show: !hiddenColumnIndexes.has(4),
                        },
                    },
                    {
                        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: {
                            display: function (data) {
                                return data;
                            },
                            sp: function (data) {
                                return getAcceptsValues(data);
                            },
                        },
                        searchPanes: {
                            show: true,
                            orthogonal: "sp",
                        },
                    },
                ],
                layout: {
                    topStart: null,
                    topEnd: null,
                    top2: {
                        searchPanes: {
                            cascadePanes: true,
                            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);
        });
    });
}