MediaWiki:Common.js: Difference between revisions

From Tuflow
Jump to navigation Jump to search
Content deleted Content added
No edit summary
No edit summary
Line 1: Line 1:
/* Any JavaScript here will be loaded for all users on every page load. */
/* Any JavaScript here will be loaded for all users on every page load. */

/* -------------------------------------------------------------------------
* Shared helpers
* ------------------------------------------------------------------------- */

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

function tuflowEscapeRegExp(value) {
return String(value || '').replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}

function tuflowClearHighlights(root) {
if (!root) {
return;
}

Array.from(root.querySelectorAll('mark.search-highlight')).forEach(function (mark) {
var textNode = document.createTextNode(mark.textContent);
var parent = mark.parentNode;

if (!parent) {
return;
}

parent.replaceChild(textNode, mark);
parent.normalize();
});
}

function tuflowHighlightMatches(root, query) {
if (!root || !query) {
return;
}

tuflowClearHighlights(root);

var testPattern = new RegExp(tuflowEscapeRegExp(query), 'i');
var replacePattern = new RegExp(tuflowEscapeRegExp(query), 'ig');

var walker = document.createTreeWalker(
root,
NodeFilter.SHOW_TEXT,
{
acceptNode: function (node) {
if (!node.nodeValue || !node.nodeValue.trim()) {
return NodeFilter.FILTER_REJECT;
}

if (
node.parentNode &&
['SCRIPT', 'STYLE', 'MARK', 'TEXTAREA', 'INPUT'].indexOf(node.parentNode.nodeName) !== -1
) {
return NodeFilter.FILTER_REJECT;
}

return testPattern.test(node.nodeValue)
? NodeFilter.FILTER_ACCEPT
: NodeFilter.FILTER_REJECT;
}
}
);

var textNodes = [];
var currentNode;

while ((currentNode = walker.nextNode())) {
textNodes.push(currentNode);
}

textNodes.forEach(function (node) {
var text = node.nodeValue;
var fragment = document.createDocumentFragment();
var lastIndex = 0;

text.replace(replacePattern, function (match, offset) {
if (offset > lastIndex) {
fragment.appendChild(
document.createTextNode(text.slice(lastIndex, offset))
);
}

var mark = document.createElement('mark');
mark.className = 'search-highlight';
mark.textContent = match;
fragment.appendChild(mark);

lastIndex = offset + match.length;
return match;
});

if (lastIndex < text.length) {
fragment.appendChild(
document.createTextNode(text.slice(lastIndex))
);
}

if (node.parentNode) {
node.parentNode.replaceChild(fragment, node);
}
});
}



/* -------------------------------------------------------------------------
/* -------------------------------------------------------------------------
Line 24: Line 131:
table.dataset.searchInitialised = 'true';
table.dataset.searchInitialised = 'true';


/* Create a wrapper that follows the table's natural width */
var tableBlock = document.createElement('div');
var tableBlock = document.createElement('div');
tableBlock.className = 'benchmark-table-block';
tableBlock.className = 'benchmark-table-block';
Line 42: Line 148:
input.placeholder =
input.placeholder =
'Search processor, system name, RAM, or benchmark result';
'Search processor, system name, RAM, or benchmark result';
input.setAttribute(
input.setAttribute('aria-label', 'Search benchmark results');
'aria-label',
'Search benchmark results'
);


var clearButton = document.createElement('button');
var clearButton = document.createElement('button');
Line 64: Line 167:
tableBlock.insertBefore(controls, table);
tableBlock.insertBefore(controls, table);


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

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


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


rows.forEach(function (row) {
rows.forEach(function (row) {
tuflowClearHighlights(row);

var matches =
var matches =
query === '' ||
query === '' ||
normaliseText(row.textContent).includes(query);
tuflowNormaliseSearchText(row.textContent).includes(query);


row.hidden = !matches;
row.hidden = !matches;
Line 90: Line 186:
if (matches) {
if (matches) {
visibleCount += 1;
visibleCount += 1;

if (query !== '') {
tuflowHighlightMatches(row, query);
}
}
}
});
});


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


Line 138: Line 236:
table.dataset.exampleSearchInitialised = 'true';
table.dataset.exampleSearchInitialised = 'true';


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


/*
* Keep the catalogue full width even when no rows match.
*/
tableBlock.style.display = 'block';
tableBlock.style.display = 'block';
tableBlock.style.width = '100%';
tableBlock.style.width = '100%';
Line 174: Line 265:
input.placeholder =
input.placeholder =
'Search model category, description, or model name';
'Search model category, description, or model name';
input.setAttribute(
input.setAttribute('aria-label', 'Search Example Model Catalogue');
'aria-label',
'Search Example Model Catalogue'
);


var clearButton = document.createElement('button');
var clearButton = document.createElement('button');
Line 198: Line 286:
/*
/*
* Catalogue logical columns:
* Catalogue logical columns:
*
* 0: Major category
* 0: Major category
* 1: Subcategory
* 1: Subcategory
Line 204: Line 291:
* 3: Model name
* 3: Model name
*/
*/
var originalRows = Array.from(
var originalRows = Array.from(table.querySelectorAll('tr')).filter(function (row) {
table.querySelectorAll('tr')
).filter(function (row) {
return row.querySelectorAll('td').length > 0;
return row.querySelectorAll('td').length > 0;
});
});
Line 218: Line 303:
var activeRowspans = [];
var activeRowspans = [];
var records = [];
var records = [];

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


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


/*
* Convert each physical row into a logical catalogue record.
* Merged category text is inherited by each associated row.
*/
originalRows.forEach(function (row, rowIndex) {
originalRows.forEach(function (row, rowIndex) {
var logicalCells = [];
var logicalCells = [];
Line 255: Line 327:
var rowspan = Math.max(cell.rowSpan || 1, 1);
var rowspan = Math.max(cell.rowSpan || 1, 1);


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


Line 286: Line 354:


var category = getCellText(categoryCell);
var category = getCellText(categoryCell);
var subcategory = categorySpansBoth
var subcategory = categorySpansBoth ? '' : getCellText(subcategoryCell);
? ''
: getCellText(subcategoryCell);
var description = getCellText(descriptionCell);
var description = getCellText(descriptionCell);
var modelName = getCellText(modelNameCell);
var modelName = getCellText(modelNameCell);
Line 295: Line 361:
originalRow: row,
originalRow: row,
categoryCell: categoryCell,
categoryCell: categoryCell,
subcategoryCell: categorySpansBoth
subcategoryCell: categorySpansBoth ? null : subcategoryCell,
? null
: subcategoryCell,
descriptionCell: descriptionCell,
descriptionCell: descriptionCell,
modelNameCell: modelNameCell,
modelNameCell: modelNameCell,
category: category,
category: category,
subcategory: subcategory,
subcategory: subcategory,
categoryKey:
categoryKey: category || '__uncategorised__',
category || '__uncategorised__',
subcategoryKey: subcategory || '__no_subcategory__',
subcategoryKey:
searchableText: tuflowNormaliseSearchText(
subcategory || '__no_subcategory__',
[category, subcategory, description, modelName].join(' ')
searchableText: normaliseText(
[
category,
subcategory,
description,
modelName
].join(' ')
)
)
});
});
Line 332: Line 389:


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


records.forEach(function (record) {
records.forEach(function (record) {
tuflowClearHighlights(record.originalRow);
rowParent.appendChild(record.originalRow);
rowParent.appendChild(record.originalRow);
});
});
Line 354: Line 410:
var index;
var index;


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


function renderFilteredRows(matches, query) {
/*
* Rebuild matching rows and recalculate category rowspans.
*/
function renderFilteredRows(matches) {
removeDisplayedDataRows();
removeDisplayedDataRows();


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


var categoryStarts =
var categoryStarts =
Line 386: Line 433:
var subcategoryStarts =
var subcategoryStarts =
categoryStarts ||
categoryStarts ||
previous.subcategoryKey !==
previous.subcategoryKey !== record.subcategoryKey;
record.subcategoryKey;


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


if (categoryStarts) {
if (categoryStarts) {
var categoryCell =
var categoryCell = cloneCell(record.categoryCell);
cloneCell(record.categoryCell);
var categoryRowspan = getRunLength(matches, index, function (item) {
return item.categoryKey;

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


categoryCell.rowSpan = categoryRowspan;
categoryCell.rowSpan = categoryRowspan;
Line 412: Line 452:
}
}


if (
if (record.subcategory && subcategoryStarts) {
record.subcategory &&
var subcategoryCell = cloneCell(record.subcategoryCell);
subcategoryStarts
var subcategoryRowspan = getRunLength(matches, index, function (item) {
) {
return item.categoryKey + '::' + item.subcategoryKey;
var subcategoryCell =
});
cloneCell(record.subcategoryCell);

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

subcategoryCell.rowSpan =
subcategoryRowspan;


subcategoryCell.rowSpan = subcategoryRowspan;
row.appendChild(subcategoryCell);
row.appendChild(subcategoryCell);
}
}


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


row.appendChild(
if (query) {
cloneCell(record.modelNameCell)
tuflowHighlightMatches(row, query);
);
}


rowParent.appendChild(row);
rowParent.appendChild(row);
Line 450: Line 474:


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


Line 467: Line 491:
});
});


renderFilteredRows(matches);
renderFilteredRows(matches, query);


resultCount.textContent =
resultCount.textContent =

Revision as of 14:09, 14 July 2026

/* Any JavaScript here will be loaded for all users on every page load. */

/* -------------------------------------------------------------------------
 * Shared helpers
 * ------------------------------------------------------------------------- */

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

function tuflowEscapeRegExp(value) {
    return String(value || '').replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}

function tuflowClearHighlights(root) {
    if (!root) {
        return;
    }

    Array.from(root.querySelectorAll('mark.search-highlight')).forEach(function (mark) {
        var textNode = document.createTextNode(mark.textContent);
        var parent = mark.parentNode;

        if (!parent) {
            return;
        }

        parent.replaceChild(textNode, mark);
        parent.normalize();
    });
}

function tuflowHighlightMatches(root, query) {
    if (!root || !query) {
        return;
    }

    tuflowClearHighlights(root);

    var testPattern = new RegExp(tuflowEscapeRegExp(query), 'i');
    var replacePattern = new RegExp(tuflowEscapeRegExp(query), 'ig');

    var walker = document.createTreeWalker(
        root,
        NodeFilter.SHOW_TEXT,
        {
            acceptNode: function (node) {
                if (!node.nodeValue || !node.nodeValue.trim()) {
                    return NodeFilter.FILTER_REJECT;
                }

                if (
                    node.parentNode &&
                    ['SCRIPT', 'STYLE', 'MARK', 'TEXTAREA', 'INPUT'].indexOf(node.parentNode.nodeName) !== -1
                ) {
                    return NodeFilter.FILTER_REJECT;
                }

                return testPattern.test(node.nodeValue)
                    ? NodeFilter.FILTER_ACCEPT
                    : NodeFilter.FILTER_REJECT;
            }
        }
    );

    var textNodes = [];
    var currentNode;

    while ((currentNode = walker.nextNode())) {
        textNodes.push(currentNode);
    }

    textNodes.forEach(function (node) {
        var text = node.nodeValue;
        var fragment = document.createDocumentFragment();
        var lastIndex = 0;

        text.replace(replacePattern, function (match, offset) {
            if (offset > lastIndex) {
                fragment.appendChild(
                    document.createTextNode(text.slice(lastIndex, offset))
                );
            }

            var mark = document.createElement('mark');
            mark.className = 'search-highlight';
            mark.textContent = match;
            fragment.appendChild(mark);

            lastIndex = offset + match.length;
            return match;
        });

        if (lastIndex < text.length) {
            fragment.appendChild(
                document.createTextNode(text.slice(lastIndex))
            );
        }

        if (node.parentNode) {
            node.parentNode.replaceChild(fragment, node);
        }
    });
}


/* -------------------------------------------------------------------------
 * 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';

        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 filterRows() {
            var query = tuflowNormaliseSearchText(input.value);
            var visibleCount = 0;

            rows.forEach(function (row) {
                tuflowClearHighlights(row);

                var matches =
                    query === '' ||
                    tuflowNormaliseSearchText(row.textContent).includes(query);

                row.hidden = !matches;

                if (matches) {
                    visibleCount += 1;

                    if (query !== '') {
                        tuflowHighlightMatches(row, query);
                    }
                }
            });

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

    /* Allow preview testing, but not the initial edit screen */
    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';

        var tableBlock = document.createElement('div');
        tableBlock.className =
            'benchmark-table-block example-model-table-block';

        tableBlock.style.display = 'block';
        tableBlock.style.width = '100%';
        tableBlock.style.maxWidth = '100%';
        tableBlock.style.marginLeft = '0';
        tableBlock.style.marginRight = '0';

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

        table.style.boxSizing = 'border-box';
        table.style.width = '100%';
        table.style.minWidth = '100%';

        var controls = document.createElement('div');
        controls.className = 'benchmark-table-controls';
        controls.style.boxSizing = 'border-box';

        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
         */
        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 getCellText(cell) {
            return cell ? tuflowNormaliseSearchText(cell.textContent) : '';
        }

        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;

            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: tuflowNormaliseSearchText(
                    [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) {
                tuflowClearHighlights(record.originalRow);
                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;
        }

        function renderFilteredRows(matches, query) {
            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));

                if (query) {
                    tuflowHighlightMatches(row, query);
                }

                rowParent.appendChild(row);
            });
        }

        function filterRows() {
            var query = tuflowNormaliseSearchText(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, query);

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