MediaWiki:Common.js

From Tuflow
Jump to navigation Jump to search

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.
/* Any JavaScript here will be loaded for all users on every page load. */

/* -------------------------------------------------------------------------
 * Hardware benchmark table search
 * ------------------------------------------------------------------------- */

mw.hook('wikipage.content').add(function ($content) {
    'use strict';

    var action = mw.config.get('wgAction');

    /* Do not run inside edit or preview screens */
    if (action === 'edit' || action === 'submit') {
        return;
    }

    $content.find('.benchmark-results-table').each(function () {
        var table = this;

        if (table.dataset.searchInitialised === 'true') {
            return;
        }

        table.dataset.searchInitialised = 'true';

        /* Create a wrapper that follows the table's natural width */
        var tableBlock = document.createElement('div');
        tableBlock.className = 'benchmark-table-block';

        table.parentNode.insertBefore(tableBlock, table);
        tableBlock.appendChild(table);

        var controls = document.createElement('div');
        controls.className = 'benchmark-table-controls';

        var searchWrapper = document.createElement('div');
        searchWrapper.className = 'benchmark-table-search';

        var input = document.createElement('input');
        input.type = 'search';
        input.className = 'benchmark-search-input';
        input.placeholder =
            'Search processor, system name, RAM, or benchmark result';
        input.setAttribute(
            'aria-label',
            'Search benchmark results'
        );

        var clearButton = document.createElement('button');
        clearButton.type = 'button';
        clearButton.className = 'benchmark-search-clear';
        clearButton.textContent = 'Clear';

        var resultCount = document.createElement('div');
        resultCount.className = 'benchmark-result-count';
        resultCount.setAttribute('aria-live', 'polite');

        searchWrapper.appendChild(input);
        searchWrapper.appendChild(clearButton);

        controls.appendChild(searchWrapper);
        controls.appendChild(resultCount);

        tableBlock.insertBefore(controls, table);

        var rows = Array.from(
            table.querySelectorAll('tr')
        ).filter(function (row) {
            return row.querySelectorAll('td').length > 0;
        });

        function normaliseText(value) {
            return String(value || '')
                .toLocaleLowerCase()
                .replace(/\s+/g, ' ')
                .trim();
        }

        function filterRows() {
            var query = normaliseText(input.value);
            var visibleCount = 0;

            rows.forEach(function (row) {
                var matches =
                    query === '' ||
                    normaliseText(row.textContent).includes(query);

                row.hidden = !matches;

                if (matches) {
                    visibleCount += 1;
                }
            });

            resultCount.textContent = query
                ? visibleCount +
                  ' matching result' +
                  (visibleCount === 1 ? '' : 's')
                : rows.length + ' benchmark results';

            clearButton.hidden = query === '';
        }

        input.addEventListener('input', filterRows);

        clearButton.addEventListener('click', function () {
            input.value = '';
            filterRows();
            input.focus();
        });

        filterRows();
    });
});


/* -------------------------------------------------------------------------
 * Category-aware Example Model Catalogue search
 * ------------------------------------------------------------------------- */

mw.hook('wikipage.content').add(function ($content) {
    'use strict';

    var action = mw.config.get('wgAction');

    /* Do not rearrange the table in edit or preview screens */
    if (action === 'edit') {
        return;
    }

    $content.find('.example-model-results-table').each(function () {
        var table = this;

        if (table.dataset.exampleSearchInitialised === 'true') {
            return;
        }

        table.dataset.exampleSearchInitialised = 'true';

        /*
         * Reuse the hardware-search classes so both search bars have
         * the same visual appearance.
         */
        var tableBlock = document.createElement('div');
        tableBlock.className = 'benchmark-table-block';

        table.parentNode.insertBefore(tableBlock, table);
        tableBlock.appendChild(table);

        var controls = document.createElement('div');
        controls.className = 'benchmark-table-controls';

        var searchWrapper = document.createElement('div');
        searchWrapper.className = 'benchmark-table-search';

        var input = document.createElement('input');
        input.type = 'search';
        input.className = 'benchmark-search-input';
        input.placeholder =
            'Search model category, description, or model name';
        input.setAttribute(
            'aria-label',
            'Search Example Model Catalogue'
        );

        var clearButton = document.createElement('button');
        clearButton.type = 'button';
        clearButton.className = 'benchmark-search-clear';
        clearButton.textContent = 'Clear';

        var resultCount = document.createElement('div');
        resultCount.className = 'benchmark-result-count';
        resultCount.setAttribute('aria-live', 'polite');

        searchWrapper.appendChild(input);
        searchWrapper.appendChild(clearButton);

        controls.appendChild(searchWrapper);
        controls.appendChild(resultCount);

        tableBlock.insertBefore(controls, table);

        /*
         * Catalogue logical columns:
         *
         * 0: Major category
         * 1: Subcategory
         * 2: Description
         * 3: Model name
         *
         * Major categories and subcategories may use rowspan.
         * Some major categories may span both category columns.
         */
        var originalRows = Array.from(
            table.querySelectorAll('tr')
        ).filter(function (row) {
            return row.querySelectorAll('td').length > 0;
        });

        if (!originalRows.length) {
            controls.remove();
            return;
        }

        var rowParent = originalRows[0].parentNode;
        var activeRowspans = [];
        var records = [];

        function normaliseText(value) {
            return String(value || '')
                .toLocaleLowerCase()
                .replace(/\s+/g, ' ')
                .trim();
        }

        function getCellText(cell) {
            return cell
                ? normaliseText(cell.textContent)
                : '';
        }

        /*
         * Convert each physical HTML row into a logical catalogue record.
         * Text from merged category cells is inherited by every row covered
         * by that rowspan.
         */
        originalRows.forEach(function (row, rowIndex) {
            var logicalCells = [];
            var physicalCells = Array.from(row.cells);
            var columnIndex = 0;

            activeRowspans.forEach(function (active, index) {
                if (active && active.endRow >= rowIndex) {
                    logicalCells[index] = active.cell;
                }
            });

            physicalCells.forEach(function (cell) {
                while (logicalCells[columnIndex]) {
                    columnIndex += 1;
                }

                var colspan = Math.max(cell.colSpan || 1, 1);
                var rowspan = Math.max(cell.rowSpan || 1, 1);

                for (
                    var offset = 0;
                    offset < colspan;
                    offset += 1
                ) {
                    var logicalColumn = columnIndex + offset;

                    logicalCells[logicalColumn] = cell;

                    if (rowspan > 1) {
                        activeRowspans[logicalColumn] = {
                            cell: cell,
                            endRow: rowIndex + rowspan - 1
                        };
                    }
                }

                columnIndex += colspan;
            });

            var categoryCell = logicalCells[0] || null;
            var subcategoryCell = logicalCells[1] || null;
            var descriptionCell = logicalCells[2] || null;
            var modelNameCell = logicalCells[3] || null;

            /*
             * When one cell spans both category columns, there is no
             * separate subcategory.
             */
            var categorySpansBoth =
                categoryCell &&
                subcategoryCell &&
                categoryCell === subcategoryCell;

            var category = getCellText(categoryCell);
            var subcategory = categorySpansBoth
                ? ''
                : getCellText(subcategoryCell);
            var description = getCellText(descriptionCell);
            var modelName = getCellText(modelNameCell);

            records.push({
                originalRow: row,
                categoryCell: categoryCell,
                subcategoryCell: categorySpansBoth
                    ? null
                    : subcategoryCell,
                descriptionCell: descriptionCell,
                modelNameCell: modelNameCell,
                category: category,
                subcategory: subcategory,
                categoryKey:
                    category || '__uncategorised__',
                subcategoryKey:
                    subcategory || '__no_subcategory__',
                searchableText: normaliseText(
                    [
                        category,
                        subcategory,
                        description,
                        modelName
                    ].join(' ')
                )
            });
        });

        function cloneCell(cell) {
            var clone;

            if (!cell) {
                return document.createElement('td');
            }

            clone = cell.cloneNode(true);
            clone.removeAttribute('rowspan');
            clone.removeAttribute('colspan');

            return clone;
        }

        function removeDisplayedDataRows() {
            Array.from(
                table.querySelectorAll('tr')
            ).forEach(function (row) {
                if (row.querySelectorAll('td').length > 0) {
                    row.remove();
                }
            });
        }

        function restoreOriginalRows() {
            removeDisplayedDataRows();

            records.forEach(function (record) {
                rowParent.appendChild(record.originalRow);
            });
        }

        function getRunLength(items, startIndex, keyFunction) {
            var startingKey = keyFunction(items[startIndex]);
            var count = 1;
            var index;

            for (
                index = startIndex + 1;
                index < items.length;
                index += 1
            ) {
                if (keyFunction(items[index]) !== startingKey) {
                    break;
                }

                count += 1;
            }

            return count;
        }

        /*
         * Rebuild only the matching rows and calculate new rowspan values
         * for the visible category and subcategory groups.
         */
        function renderFilteredRows(matches) {
            removeDisplayedDataRows();

            matches.forEach(function (record, index) {
                var previous = index > 0
                    ? matches[index - 1]
                    : null;

                var categoryStarts =
                    !previous ||
                    previous.categoryKey !== record.categoryKey;

                var subcategoryStarts =
                    categoryStarts ||
                    previous.subcategoryKey !==
                        record.subcategoryKey;

                var row = document.createElement('tr');

                if (categoryStarts) {
                    var categoryCell =
                        cloneCell(record.categoryCell);

                    var categoryRowspan = getRunLength(
                        matches,
                        index,
                        function (item) {
                            return item.categoryKey;
                        }
                    );

                    categoryCell.rowSpan = categoryRowspan;

                    if (!record.subcategory) {
                        categoryCell.colSpan = 2;
                    }

                    row.appendChild(categoryCell);
                }

                if (
                    record.subcategory &&
                    subcategoryStarts
                ) {
                    var subcategoryCell =
                        cloneCell(record.subcategoryCell);

                    var subcategoryRowspan = getRunLength(
                        matches,
                        index,
                        function (item) {
                            return (
                                item.categoryKey +
                                '::' +
                                item.subcategoryKey
                            );
                        }
                    );

                    subcategoryCell.rowSpan =
                        subcategoryRowspan;

                    row.appendChild(subcategoryCell);
                }

                row.appendChild(
                    cloneCell(record.descriptionCell)
                );

                row.appendChild(
                    cloneCell(record.modelNameCell)
                );

                rowParent.appendChild(row);
            });
        }

        function filterRows() {
            var query = normaliseText(input.value);
            var matches;

            if (!query) {
                restoreOriginalRows();

                resultCount.textContent =
                    records.length + ' example models';

                clearButton.hidden = true;
                return;
            }

            matches = records.filter(function (record) {
                return record.searchableText.includes(query);
            });

            renderFilteredRows(matches);

            resultCount.textContent =
                matches.length +
                ' matching model' +
                (matches.length === 1 ? '' : 's');

            clearButton.hidden = false;
        }

        input.addEventListener('input', filterRows);

        clearButton.addEventListener('click', function () {
            input.value = '';
            filterRows();
            input.focus();
        });

        filterRows();
    });
});