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
Line 26: Line 26:
             id: "datatable-styles",
             id: "datatable-styles",
             text: `
             text: `
           
         table.dataTable > tbody > tr > td {
         table.dataTable > tbody > tr > td {
           padding-top: 0.25rem;
           padding-top: 0.25rem;
Line 121: Line 122:


         .dt-filter-panel {
         .dt-filter-panel {
           background: #0a0d14;
           background-color: #151515 !important;
           box-shadow: 0.5rem 0 1.5rem rgba(0, 0, 0, 0.18);
           box-shadow: 0.5rem 0 1.5rem rgba(0, 0, 0, 0.18);
           color: #fff;
           color: #fff;
Line 543: Line 544:
                             cascadePanes: true,
                             cascadePanes: true,
                             initCollapsed: true,
                             initCollapsed: true,
                            layout: "columns-1",
                             columns: [1, 2, 3, 4, 5],
                             columns: [1, 2, 3, 4, 5],
                         },
                         },

Revision as of 09:36, 17 June 2026

var selectors = [
    "#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;
        }

        .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);
          }
        }
        
        .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: 1.5rem;
          line-height: 1;
        }

        .dtsp-paneButton.clearButton:not(:disabled) {
          color: var(--bs-danger, #dc3545);
        }

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

        .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: #151515 !important;
          box-shadow: 0.5rem 0 1.5rem rgba(0, 0, 0, 0.18);
          color: #fff;
          display: flex;
          flex-direction: column;
          height: 100vh;
          left: 0;
          max-width: min(92vw, 24rem);
          overflow: hidden;
          position: fixed;
          top: 0;
          transform: translateX(-100%);
          transition: transform 220ms ease;
          width: 24rem;
          z-index: 1050;
        }

        .dt-filter-panel.is-open {
          transform: translateX(0);
        }

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

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

        .dt-filter-panel__close {
          background: transparent;
          border: 0;
          color: inherit;
          font-size: 1.75rem;
          line-height: 1;
          padding: 0.125rem 0.375rem;
        }

        .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;
        }
      `,
        }).appendTo("head");
    }

    $("#games > table").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 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 setupFilterOffCanvas(dataTable, table) {
        var wrapper = table.closest(".dt-container") || table.parentNode;
        var panes = wrapper ? wrapper.querySelector(".dtsp-panesContainer") : null;

        if (!wrapper || !panes || wrapper.querySelector(".dt-filter-toggle")) {
            return;
        }

        var toggle = document.createElement("button");
        var panel = document.createElement("aside");
        var backdrop = document.createElement("div");
        var header = document.createElement("div");
        var title = document.createElement("h2");
        var close = document.createElement("button");
        var body = document.createElement("div");
        var lastFocusedElement = null;

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

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

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

        header.className = "dt-filter-panel__header";
        title.className = "dt-filter-panel__title";
        title.textContent = "Filters";
        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(header, body);

        wrapper.insertBefore(toggle, wrapper.firstChild);
        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);
        backdrop.addEventListener("click", closePanel);

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

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

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

                normaliseDataTableRows(table, getDataTableColumnCount(table));

                $(table).DataTable({
                    dom: "f",
                    retrieve: true,
                    order: [[0, "asc"]],
                    pageLength: 1000,
                });
            });
        });

// ----- GAMES ------

    $("#games > table").each(function () {
        var table = this;

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

            var titles = [
                "Game", "Developer", "Publisher", "Genre", "Platform", "Mode", "Released"
            ];
            var data = getTableData(table, titles.length);

            buildTableLayout(table, titles);

            var dataTable = new DataTable(table, {
                columns: [
                    {
                        title: "Game",
                        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: true,
                        searchPanes: {
                            show: true,
                        },
                    },
                    {
                        title: "Publisher",
                        visible: true,
                        searchPanes: {
                            show: true,
                        },
                    },
                    {
                        title: "Genre",
                        visible: true,
                        searchPanes: {
                            show: true,
                        },
                    },
                    {
                        title: "Platform",
                        visible: true,
                        searchPanes: {
                            show: true,
                        },
                    },
                    {
                        title: "Mode",
                        visible: true,
                        render: {
                            display: function (data) {
                                return data;
                            },
                            sp: function (data) {
                                return getAcceptsValues(data);
                            },
                        },
                        searchPanes: {
                            show: true,
                            orthogonal: "sp",
                        },
                    },
                    {
                        title: "Released",
                        visible: true,
                        searchPanes: {
                            show: true,
                        },
                    },
                ],
                layout: {
                    topStart: null,
                    topEnd: null,
                    top2: {
                        searchPanes: {
                            cascadePanes: true,
                            initCollapsed: true,
                            layout: "columns-1",
                            columns: [1, 2, 3, 4, 5],
                        },
                    },
                    top1: {
                        search: {
                            className: "mx-0 w-100",
                        },
                    },
                },
                order: [[0, "asc"]],
                paging: false,
                searchDelay: 400,
            });

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

            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();
            $(".dataTable")
                .removeClass("dt-loading")
                .removeAttr("aria-busy")
                .show();
            dataTable.searchPanes.rebuildPane();
            setupFilterOffCanvas(dataTable, table);
            suppressSearchPaneBorders(document.querySelector(".dt-filter-panel"));
            dataTable.columns.adjust().draw(false);
        });
    });
}