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
 
(109 intermediate revisions by the same user not shown)
Line 1: Line 1:
var selectors = [
var selectors = ["#games > table"];
    "#games > table",
var seriesSelectors = ["#series > table"];
    "[data-datatable='games'] > table",
var assetSelectors = ["#assets > table"];
    "[data-table='games'] > table",
var playerSelectors = ["#players table"];
    "[data-table-type='games'] > 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 17: Line 22:


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: /* css */ `
           
         table.dataTable > tbody > tr > td {
         table.dataTable > tbody > tr > td {
           padding-top: 0.25rem;
           padding-top: 0.25rem;
Line 35: Line 39:
         }
         }


table.dataTable, table.dataTable * {
        table.dataTable,
  border: 0 !important;
        table.dataTable * {
}
          border: 0 !important;
        }
 
         .dt-loading {
         .dt-loading {
           display: block !important;
           display: block !important;
Line 67: Line 72:
           }
           }
         }
         }
          
 
div.dtsp-panesContainer div.dtsp-title {
         div.dtsp-panesContainer div.dtsp-title {
padding: 2px 0 !important;
          padding: 2px 0 !important;
}
        }
 
         .dtsp-bordered, .dtsp-bordered:hover,
         .dtsp-bordered,
         .dtsp-searchPane, .dtsp-searchPane:hover {
        .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:hover,
.dtsp-topRow button:focus, .dtsp-topRow input:focus,
        .dtsp-topRow input:hover,
.dtsp-topRow button, .dtsp-topRow input {
        .dtsp-topRow button:focus,
margin: 0 !important;
        .dtsp-topRow input:focus,
}
        .dtsp-topRow button,
        .dtsp-topRow input {
          margin: 0 !important;
        }
 
         .dtsp-paneButton.clearButton {
         .dtsp-paneButton.clearButton {
           font-size: 2.5rem;
           font-size: 2.5rem;
  line-height: 0;
          line-height: 0;
           padding-bottom: 6px;
           padding-bottom: 6px;
         }
         }
Line 91: Line 101:
         .dtsp-paneButton.clearButton:not(:disabled) {
         .dtsp-paneButton.clearButton:not(:disabled) {
           color: #dc3545 !important;
           color: #dc3545 !important;
  font-size: 3rem;
          font-size: 3rem;
         }
         }


.dtsp-topRow {
        .dtsp-topRow {
  background-color: #1e2d59;
          background-color: #1e2d59;
           background-image: url(/w/extensions/wikivisor/images/embossed-leather-pattern.png);
           background-image: url(/w/extensions/wikivisor/images/embossed-leather-pattern.png);
           background-blend-mode: darken;
           background-blend-mode: darken;
           background-size: cover;
           background-size: cover;
           border: 1px solid rgba(255,255,255,0.2);
           border: 1px solid rgba(255, 255, 255, 0.2);
           box-shadow: inset 0 0 30px rgba(0,0,0,0.75);
           box-shadow: inset 0 0 30px rgba(0, 0, 0, 0.75);
}
        }
         .dtsp-caret {
         .dtsp-caret {
           align-items: end;
           align-items: end;
Line 112: Line 122:
           align-items: center;
           align-items: center;
         }
         }
       
 
         div.dtsp-topRow button.dtsp-collapseButton span.dtsp-caret {
         div.dtsp-topRow button.dtsp-collapseButton span.dtsp-caret {
top: unset !important;      
          top: unset !important;
         }
         }


Line 128: Line 138:
           content: "\\2303";
           content: "\\2303";
         }
         }
       
 
         div.dtsp-searchPane div.dtsp-topRow div.dtsp-searchCont input.dtsp-search {
         div.dtsp-searchPane div.dtsp-topRow div.dtsp-searchCont input.dtsp-search {
  padding-left: 8px;
          padding-left: 8px;
}
        }


         .dt-filter-toggle {
         .dt-filter-toggle {
Line 197: Line 207:


         .dt-filter-backdrop {
         .dt-filter-backdrop {
           background: rgba(0, 0, 0, 0.35);
           background: rgba(0, 0, 0, 0);
           inset: 0;
           inset: 0;
           opacity: 0;
           opacity: 0;
Line 234: Line 244:


         .dt-filter-panel.is-open {
         .dt-filter-panel.is-open {
           box-shadow: 0 0 20px rgba(255,255,255,0.2);
           box-shadow: 0 0 20px rgba(255, 255, 255, 0.2);
           transform: translateX(0);
           transform: translateX(0);
         }
         }
Line 257: Line 267:
           background: transparent;
           background: transparent;
           border: 0;
           border: 0;
           color: rgb(124,124,124);
           color: rgb(124, 124, 124);
           font-size: 2.5rem;
           font-size: 2.5rem;
           line-height: 1;
           line-height: 1;
Line 263: Line 273:
           width: 35px;
           width: 35px;
           height: 35px;
           height: 35px;
  display: flex;
          display: flex;
  align-items: center;
          align-items: center;
  justify-content: center;
          justify-content: center;
         }
         }


Line 290: Line 300:
           overflow: hidden;
           overflow: hidden;
         }
         }
 
aside.is-open .sidebar-drawer {
        /*
        * 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;
display: inline-block;
right: 0;
width: 6px;
left: 284px;
height: 6px;
border: 0;
content: '';
z-index: 3;
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");
    }).appendTo("head");
    }
  }


    $(selectors.join(",")).addClass("dt-loading").attr("aria-busy", "true");
  $(allSelectors.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 321: Line 463:


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) {
      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++;
      }
    });
  }


        if (headerRow) {
  function waitForTableToSettle(table, callback) {
            return headerRow.cells.length;
    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);
    }


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


     function normaliseDataTableRows(table, expectedColumnCount) {
     timeoutTimer = setTimeout(finish, 30000);
        $("tbody tr", table).each(function () {
    scheduleFinish();
            var row = this;
  }
            var cellCount = row.cells.length;
 
  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);
  }


            if (cellCount === 1 && row.cells[0].colSpan > 1) {
  function renderPlayerGames(html) {
                return;
    var games = getAcceptsValues(html);
            }


            while (cellCount < expectedColumnCount) {
    if (games.length < 2) {
                row.appendChild(document.createElement("td"));
      return games[0] || "";
                cellCount++;
            }
        });
     }
     }


     function waitForTableToSettle(table, callback) {
     return (
         var settleTimer;
      '<div class="player-games">' +
         var timeoutTimer;
      '<span class="player-games__first">' +
         var observer;
      games[0] +
        var settled = false;
      "</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 finish() {
  function getGameModeText(html) {
            if (settled) return;
    var container = document.createElement("div");
    var values = [];


            settled = true;
    container.innerHTML = html || "";
            clearTimeout(settleTimer);
    values.push(container.textContent || "");
            clearTimeout(timeoutTimer);


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


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


        function scheduleFinish() {
    return values.join(" ");
            clearTimeout(settleTimer);
  }
            settleTimer = setTimeout(finish, 500);
        }


        if (window.MutationObserver) {
  function getGameModeValue(html) {
            observer = new MutationObserver(scheduleFinish);
    var text = getGameModeText(html).toLowerCase().replace(/[_-]+/g, " ");
            observer.observe(table.tBodies[0] || table, {
                childList: true,
                subtree: true,
            });
        }


        timeoutTimer = setTimeout(finish, 30000);
    if (/\b(single\s*player|singleplayer|solo|sp)\b/.test(text)) {
        scheduleFinish();
      return "SP";
     }
     }


     function getAcceptsValues(html) {
     if (/\b(multi\s*player|multiplayer|mp)\b/.test(text)) {
        return (html || "")
      return "MP";
            .split(/<br\s*\/?>/i)
            .map((value) => value.trim())
            .filter(Boolean);
     }
     }


     function normaliseColumnName(value) {
     if (/\b(co\s*op|coop|cooperative)\b/.test(text)) {
        return String(value || "")
      return "Coop";
            .trim()
            .toLowerCase()
            .replace(/[\s_-]+/g, "");
     }
     }


     function getTableConfigElement(table) {
     return "";
        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 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;
     }
     }


     function getFirstColumnName(table) {
     link = link.cloneNode(false);
        var configElement = getTableConfigElement(table);
    link.textContent = mode;
        var columnName = configElement
 
            ? configElement.getAttribute("data-first-column-name") ||
    return link.outerHTML;
            configElement.getAttribute("data-first-column") ||
  }
            configElement.getAttribute("data-page-column-name") ||
 
            configElement.getAttribute("data-dt-first-column-name")
  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";
    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();
     }
     }


     function getHiddenColumnIndexes(table, titles) {
     titles.forEach(function (title, index) {
        var configElement = getTableConfigElement(table);
      titleIndexes[normaliseColumnName(title)] = index;
        var rawValue = configElement
    });
            ? configElement.getAttribute("data-hidden-columns") ||
 
            configElement.getAttribute("data-hide-columns") ||
    String(configElement.getAttribute(configuredAttribute) || "")
            configElement.getAttribute("data-dt-hidden-columns") ||
      .split(/[,\|]/)
            configElement.getAttribute("data-dt-hide-columns")
      .map(function (value) {
            : "";
        return value.trim();
         var titleIndexes = {};
      })
         var hiddenIndexes = new Set();
      .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;
  }


        titles.forEach(function (title, index) {
  function getTableData(table, columnCount) {
            titleIndexes[normaliseColumnName(title)] = index;
    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());


         String(rawValue || "")
         while (data.length < columnCount) {
            .split(/[,\|]/)
          data.push("");
            .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) {
        return data.slice(0, columnCount);
                    hiddenIndexes.add(numberValue);
      });
                    return;
  }
                }


                if (Object.prototype.hasOwnProperty.call(titleIndexes, normalisedValue)) {
  function buildTableLayout(table, titles) {
                    hiddenIndexes.add(titleIndexes[normalisedValue]);
    var headerRow = document.createElement("tr");
                }
    var tableHead = document.createElement("thead");
            });


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


     function getTableData(table, columnCount) {
     table.replaceChildren(tableHead, document.createElement("tbody"));
        return Array.from(table.tBodies[0] ? table.tBodies[0].rows : [])
    tableHead.appendChild(headerRow);
            .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) {
  function suppressSearchPaneBorders(container) {
                    data.push("");
    if (!container) {
                }
      return;
    }


                return data.slice(0, columnCount);
    function removeBorderClass() {
            });
      container.querySelectorAll(".dtsp-bordered").forEach(function (element) {
        element.classList.remove("dtsp-bordered");
      });
     }
     }


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


        titles.forEach((title) => {
    new MutationObserver(removeBorderClass).observe(container, {
            var headerCell = document.createElement("th");
      attributes: true,
            headerCell.textContent = title;
      attributeFilter: ["class"],
            headerRow.appendChild(headerCell);
      childList: true,
      subtree: true,
    });
  }
 
  function openFellowshipLinksInNewTab(dataTable) {
    dataTable
      .column(0)
      .nodes()
      .toArray()
      .forEach(function (cell) {
        cell.querySelectorAll("a").forEach(function (link) {
          link.target = "_blank";
         });
         });
      });
  }


        table.replaceChildren(tableHead, document.createElement("tbody"));
  function setupFilterToolbar(wrapper, toggle) {
        tableHead.appendChild(headerRow);
    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);
     }
     }


     function suppressSearchPaneBorders(container) {
     toolbar.appendChild(toggle);
        if (!container) {
 
            return;
    if (search) {
        }
      toolbar.appendChild(search);
    }


        function removeBorderClass() {
    if (
            container.querySelectorAll(".dtsp-bordered").forEach(function (element) {
      searchRow &&
                element.classList.remove("dtsp-bordered");
      searchRow.parentNode &&
            });
      !searchRow.querySelector(".dt-search")
        }
    ) {
      searchRow.remove();
    }
  }


        removeBorderClass();
  function getCurrentPageName() {
    var pageName = "";


        new MutationObserver(removeBorderClass).observe(container, {
    if (window.mw && mw.config) {
            attributes: true,
      pageName = mw.config.get("wgTitle") || mw.config.get("wgPageName") || "";
            attributeFilter: ["class"],
            childList: true,
            subtree: true,
        });
     }
     }


     function openFellowshipLinksInNewTab(dataTable) {
     if (!pageName) {
         dataTable
      pageName =
            .column(0)
         (document.querySelector("#firstHeading") || {}).textContent ||
            .nodes()
        document.title ||
            .toArray()
        "";
            .forEach(function (cell) {
                cell.querySelectorAll("a").forEach(function (link) {
                    link.target = "_blank";
                });
            });
     }
     }


     function setupFilterToolbar(wrapper, toggle) {
     return pageName.replace(/_/g, " ").trim();
        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";
  function getFilterPanelTitle(titles, hiddenColumnIndexes) {
    var pageName = getCurrentPageName();


        if (searchRow && searchRow.parentNode) {
    if (hiddenColumnIndexes && hiddenColumnIndexes.size === 1) {
            searchRow.parentNode.insertBefore(toolbar, searchRow);
      return titles[Array.from(hiddenColumnIndexes)[0]] + ": " + pageName;
        } else {
    }
            wrapper.insertBefore(toolbar, wrapper.firstChild);
 
        }
    return pageName;
  }


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


        if (search) {
    if (!wrapper || !panes) {
            toolbar.appendChild(search);
      return null;
        }
    }


        if (searchRow && searchRow.parentNode && !searchRow.querySelector(".dt-search")) {
    if (wrapper.querySelector(".dt-filter-toggle")) {
            searchRow.remove();
      return document.getElementById(
        }
        wrapper
          .querySelector(".dt-filter-toggle")
          .getAttribute("aria-controls"),
      );
     }
     }


     function getCurrentPageName() {
     var toggle = document.createElement("button");
        var pageName = "";
    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;


        if (window.mw && mw.config) {
    toggle.type = "button";
            pageName = mw.config.get("wgTitle") || mw.config.get("wgPageName") || "";
    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);


        if (!pageName) {
    setupFilterToolbar(wrapper, toggle);
            pageName = (document.querySelector("#firstHeading") || {}).textContent || document.title || "";
    document.body.append(backdrop, panel);
        }


        return pageName.replace(/_/g, " ").trim();
    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 getFilterPanelTitle(titles, hiddenColumnIndexes) {
     function closePanel() {
        var pageName = getCurrentPageName();
      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 (hiddenColumnIndexes && hiddenColumnIndexes.size === 1) {
      if (lastFocusedElement && document.contains(lastFocusedElement)) {
            return titles[Array.from(hiddenColumnIndexes)[0]] + ": " + pageName;
        lastFocusedElement.focus();
        }
      }


        return pageName;
      dataTable.columns.adjust();
     }
     }


     function setupFilterOffCanvas(dataTable, table, titles, hiddenColumnIndexes) {
     toggle.addEventListener("click", openPanel);
        var wrapper = table.closest(".dt-container") || table.parentNode;
    close.addEventListener("click", closePanel);
        var panes = wrapper ? wrapper.querySelector(".dtsp-panesContainer") : null;
    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;


         if (!wrapper || !panes) {
      waitForTableToSettle(table, function () {
            return null;
         if ($.fn.dataTable.isDataTable(table)) {
          return;
         }
         }


         if (wrapper.querySelector(".dt-filter-toggle")) {
         normaliseDataTableRows(table, getDataTableColumnCount(table));
            return document.getElementById(wrapper.querySelector(".dt-filter-toggle").getAttribute("aria-controls"));
 
        }
        $(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],
        };
      });


        var toggle = document.createElement("button");
      normaliseDataTableRows(table, titles.length);
        var panel = document.createElement("aside");
      buildTableLayout(table, titles);
        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";
      try {
         toggle.className = "dt-filter-toggle";
         var dataTable = new DataTable(table, {
        toggle.textContent = "Filters";
          data: data,
        toggle.setAttribute("aria-controls", panelId);
        toggle.setAttribute("aria-expanded", "false");


        panel.id = panelId;
          columns: [
        panel.className = "dt-filter-panel";
            {
        panel.setAttribute("aria-hidden", "true");
              data: "games",
        panel.setAttribute("aria-label", "Data filters");
              name: "Games",
              title: "Game",
            },
            {
              data: "series",
              name: "Series",
              title: "Series",
              visible: false,
              searchable: true,
              type: "string",
            },
          ],


        backdrop.className = "dt-filter-backdrop";
          layout: {
        drawer.className = "sidebar-drawer";
            topStart: null,
        drawer.setAttribute("type", "button");
            topEnd: null,
        drawer.setAttribute("tabindex", "0");
            top1: {
              searchBuilder: {
                columns: [1],


        header.className = "dt-filter-panel__header";
                preDefined: {
        title.className = "dt-filter-panel__title";
                  logic: "AND",
        title.textContent = getFilterPanelTitle(titles, hiddenColumnIndexes);
                  criteria: [
        close.type = "button";
                    {
        close.className = "dt-filter-panel__close";
                      data: "series",
        close.setAttribute("aria-label", "Close filters");
                      origData: "series",
        close.innerHTML = "&times;";
                      condition: "=",
                      type: "string",
                    },
                  ],
                },
              },
            },
            top2: {},
          },


        body.className = "dt-filter-panel__body";
          language: {
            searchBuilder: {
              data: "Series",
              value: "Select a series",
            },
          },


        header.append(title, close);
          order: [[0, "asc"]],
        body.appendChild(panes);
          paging: false,
        panel.append(drawer, header, body);


        setupFilterToolbar(wrapper, toggle);
          initComplete: function () {
        document.body.append(backdrop, panel);
            var api = this.api();
            var container = $(api.table().container());


        function openPanel() {
            function emptySeriesCriterion() {
            lastFocusedElement = document.activeElement;
              return {
            panel.classList.add("is-open");
                logic: "AND",
            backdrop.classList.add("is-open");
                criteria: [
            document.body.classList.add("dt-filter-panel-open");
                  {
            panel.setAttribute("aria-hidden", "false");
                    data: "series",
            toggle.setAttribute("aria-expanded", "true");
                    origData: "series",
            close.focus();
                    condition: "=",
             dataTable.columns.adjust();
                    type: "string",
        }
                  },
                ],
              };
             }


        function closePanel() {
            function enableAllSeriesOption() {
            panel.classList.remove("is-open");
              var valueSelect = container.find(".dtsb-value").first();
            backdrop.classList.remove("is-open");
              var emptyOption = valueSelect
            document.body.classList.remove("dt-filter-panel-open");
                .find('option[value="--valueTitle--"]')
            panel.setAttribute("aria-hidden", "true");
                .first();
            toggle.setAttribute("aria-expanded", "false");


            if (lastFocusedElement && document.contains(lastFocusedElement)) {
              emptyOption
                 lastFocusedElement.focus();
                .prop("disabled", false)
                .prop("hidden", false)
                 .text("All series");
             }
             }


             dataTable.columns.adjust();
             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;
         }
         }


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


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


         return panel;
      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");


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


            waitForTableToSettle(table, function () {
                  try {
                if ($.fn.dataTable.isDataTable(table)) {
                    hostname = new URL(link.href, window.location.href)
                     return;
                      .hostname;
                }
                  } catch (error) {
                     hostname = "";
                  }


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


                $(table).DataTable({
                  link.target = "_blank";
                    dom: "f",
                  link.rel = "noopener noreferrer nofollow";
                    language: {
                        search: "",
                        searchPlaceholder: "Search",
                        searchPanes: {
            title: {
                _: 'Filters: %d',
                0: ''
            }
        }
                    },
                    retrieve: true,
                    order: [[0, "asc"]],
                    pageLength: 1000,
                 });
                 });
             });
 
              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 : "";
         });
         });


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


        waitForTableToSettle(table, function () {
      buildTableLayout(table, titles);
             if ($.fn.dataTable.isDataTable(table)) {
 
      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;
                 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;
                }


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


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


             var dataTable = new DataTable(table, {
          return column;
                 columns: [
        }),
                    {
        layout: {
                        title: titles[0],
          topStart: null,
                        visible: !hiddenColumnIndexes.has(0),
          topEnd: null,
                        render: function (data, type) {
          top2: {
                            if (type === "display") {
             searchPanes: {
                                return (
              cascadePanes: true,
                                    '<span style="display: list-item; margin-left: 1.25rem; white-space: nowrap">' +
              dtOpts: {
                                    data +
                 scroller: false,
                                    "</span>"
              },
                                );
              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);


                            return data;
      buildTableLayout(table, titles);
                        },
                        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 data;
                            },
                            sp: function (data) {
                                return getAcceptsValues(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();
      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>"
                );
              }


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


                openFellowshipLinksInNewTab(dataTable);
      var lastGlobalSearch = dataTable.search();


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


                lastGlobalSearch = globalSearch;
        openFellowshipLinksInNewTab(dataTable);


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


            var filterPanel;
        lastGlobalSearch = globalSearch;


            dataTable.rows.add(data).draw();
        paneIndexes.forEach(function (paneIndex) {
            $(table)
          dataTable.searchPanes.rebuildPane(paneIndex, true);
                .removeClass("dt-loading")
                .removeAttr("aria-busy")
                .show();
            dataTable.searchPanes.rebuildPane();
            filterPanel = setupFilterOffCanvas(dataTable, table, titles, hiddenColumnIndexes);
            suppressSearchPaneBorders(filterPanel);
            dataTable.columns.adjust().draw(false);
         });
         });
      });
      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 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);
    });
  });
}