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

MediaWiki:Datatables.js

From PC Gaming Shelter
Revision as of 16:05, 17 June 2026 by WikiVisor (talk | contribs)

Note: After publishing, you may have to bypass your browser's cache to see the changes.

  • Firefox / Safari: Hold Shift while clicking Reload, or press either Ctrl-F5 or Ctrl-R (⌘-R on a Mac)
  • Google Chrome: Press Ctrl-Shift-R (⌘-Shift-R on a Mac)
  • Edge: Hold Ctrl while clicking Refresh, or press Ctrl-F5.
var selectors = [
    "#games > table",
    "[data-datatable='games'] > table",
    "[data-table='games'] > table",
    "[data-table-type='games'] > table"
];

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

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

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

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

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

		table.dataTable, table.dataTable * {
		  border: 0 !important;
		}
		
        .dt-loading {
          display: block !important;
          min-height: 4rem;
          position: relative;
        }

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

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

        @keyframes dt-loading {
          to {
            transform: rotate(360deg);
          }
        }
        
		div.dtsp-panesContainer div.dtsp-title {
			padding: 2px 0 !important;
		}
		
        .dtsp-bordered, .dtsp-bordered:hover,
        .dtsp-searchPane, .dtsp-searchPane:hover {
          border: 0 !important;
        }
        
		.dtsp-topRow button:hover, .dtsp-topRow input:hover,
		.dtsp-topRow button:focus, .dtsp-topRow input:focus,
		.dtsp-topRow button, .dtsp-topRow input {
			margin: 0 !important;
		}
		
        .dtsp-paneButton.clearButton {
          font-size: 2.5rem;
		  line-height: 0;
          padding-bottom: 6px;
        }

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

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

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

        [aria-expanded="true"] .dtsp-caret::before,
        .dtsp-caret-u::before,
        .dtsp-caret-up::before {
          content: "\\2303";
        }
        
        div.dtsp-searchPane div.dtsp-topRow div.dtsp-searchCont input.dtsp-search {
		  padding-left: 8px;
		}

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

        body.dt-filter-panel-open {
          overflow: hidden;
        }
		
		aside.is-open .sidebar-drawer {
			display: inline-block;
			right: 0;
			left: 284px;
			border: 0;
			z-index: 3;
		}
		
		aside.dt-filter-panel {
			background-image: url(/w/images/d/de/File_Icon-Guardians-semitransparent.png);
			background-position: bottom;
			background-repeat: no-repeat;
		}
      `,
        }).appendTo("head");
    }

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

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

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

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

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

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

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

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

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

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

        function finish() {
            if (settled) return;

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

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

            callback();
        }

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

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

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

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

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

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

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

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

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

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

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

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

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

        return hiddenIndexes;
    }

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

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

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

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

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

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

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

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

        removeBorderClass();

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

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

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

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

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

        toolbar.appendChild(toggle);

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

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

    function getCurrentPageName() {
        var pageName = "";

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

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

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

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

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

        return pageName;
    }

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

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

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

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

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

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

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

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

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

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

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

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

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

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

            dataTable.columns.adjust();
        }

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

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

        return panel;
    }

    $(".dataTable")
        .each(function () {
            var table = this;

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

                normaliseDataTableRows(table, getDataTableColumnCount(table));

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

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

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

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

            buildTableLayout(table, titles);

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

                            return data;
                        },
                        searchPanes: {
                            show: false,
                        },
                    },
                    {
                        title: "Developer",
                        visible: !hiddenColumnIndexes.has(1),
                        searchPanes: {
                            show: !hiddenColumnIndexes.has(1),
                        },
                    },
                    {
                        title: "Publisher",
                        visible: !hiddenColumnIndexes.has(2),
                        searchPanes: {
                            show: !hiddenColumnIndexes.has(2),
                        },
                    },
                    {
                        title: "Genre",
                        visible: !hiddenColumnIndexes.has(3),
                        searchPanes: {
                            show: !hiddenColumnIndexes.has(3),
                        },
                    },
                    {
                        title: "Platform",
                        visible: !hiddenColumnIndexes.has(4),
                        searchPanes: {
                            show: !hiddenColumnIndexes.has(4),
                        },
                    },
                    {
                        title: "Mode",
                        visible: !hiddenColumnIndexes.has(5),
                        render: {
                            display: function (data) {
                                return 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();

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