MediaWiki:Common.js: Difference between revisions

Content deleted Content added
No edit summary
No edit summary
Tag: Manual revert
(8 intermediate revisions by the same user not shown)
Line 1:
/* 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();
}
 
 
/*
* Parse a query into OR groups containing AND terms.
*
* Examples:
* raster terrain
* raster AND terrain
* QuickOSM OR HCMGIS
* "repository install" AND raster
*/
function tuflowParseSearchQuery(rawQuery) {
var query = String(rawQuery || '').trim();
 
if (!query) {
return [];
}
 
return query.split(/\s+OR\s+/i).map(function (group) {
var terms = [];
var pattern = /"([^"]+)"|(\S+)/g;
var match;
 
while ((match = pattern.exec(group)) !== null) {
var term = match[1] || match[2];
 
if (!/^AND$/i.test(term)) {
term = tuflowNormaliseSearchText(term);
 
if (term) {
terms.push(term);
}
}
}
 
return terms;
}).filter(function (group) {
return group.length > 0;
});
}
 
function tuflowMatchesSearch(text, rawQuery) {
var normalisedText = tuflowNormaliseSearchText(text);
var groups = tuflowParseSearchQuery(rawQuery);
 
if (!groups.length) {
return true;
}
 
return groups.some(function (terms) {
return terms.every(function (term) {
return normalisedText.includes(term);
});
});
}
 
function tuflowGetSearchTerms(rawQuery) {
var terms = [];
 
tuflowParseSearchQuery(rawQuery).forEach(function (group) {
group.forEach(function (term) {
if (terms.indexOf(term) === -1) {
terms.push(term);
}
});
});
 
return terms.sort(function (a, b) {
return b.length - a.length;
});
}
 
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, rawQuery) {
if (!root || !rawQuery) {
return;
}
 
tuflowClearHighlights(root);
 
var terms = tuflowGetSearchTerms(rawQuery);
 
if (!terms.length) {
return;
}
 
/*
* Longer terms are checked first so that phrases such as
* "repository install" are highlighted before "repository".
*/
terms.sort(function (a, b) {
return b.length - a.length;
});
 
var escapedTerms = terms.map(function (term) {
return tuflowEscapeRegExp(term);
});
 
var testPattern = new RegExp(
escapedTerms.join('|'),
'i'
);
 
var replacePattern = new RegExp(
escapedTerms.join('|'),
'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',
'BUTTON'
].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 normalisedMatch =
tuflowNormaliseSearchText(match);
 
var termIndex = terms.findIndex(function (term) {
return term === normalisedMatch;
});
 
/*
* Cycle through five highlight colours.
*/
var colourNumber =
(Math.max(termIndex, 0) % 5) + 1;
 
var mark = document.createElement('mark');
 
mark.className =
'search-highlight search-highlight-' +
colourNumber;
 
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 ⟶ 258:
table.dataset.searchInitialised = 'true';
 
/* Create a wrapper that follows the table's natural width */
var tableBlock = document.createElement('div');
tableBlock.className = 'benchmark-table-block';
Line 42 ⟶ 275:
input.placeholder =
'Search processor, system name, RAM, or benchmark result';
input.setAttribute('aria-label', 'Search benchmark results');
'aria-label',
'Search benchmark results'
);
 
var clearButton = document.createElement('button');
Line 64 ⟶ 294:
tableBlock.insertBefore(controls, table);
 
var rows = Array.from(table.querySelectorAll('tr')).filter(function (row) {
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 queryrawQuery = normaliseText(input.value.trim();
var visibleCount = 0;
 
rows.forEach(function (row) {
var matches =tuflowClearHighlights(row);
 
query === '' ||
var matches = normaliseText(row.textContent).includestuflowMatchesSearch(query);
row.textContent,
rawQuery
);
 
row.hidden = !matches;
Line 90 ⟶ 314:
if (matches) {
visibleCount += 1;
 
if (rawQuery !== '') {
tuflowHighlightMatches(row, rawQuery);
}
}
});
 
resultCount.textContent = queryrawQuery
? visibleCount + ' matching result' + (visibleCount === 1 ? '' : 's')
' matching result' +
(visibleCount === 1 ? '' : 's')
: rows.length + ' benchmark results';
 
clearButton.hidden = queryrawQuery === '';
}
 
Line 124 ⟶ 350:
var action = mw.config.get('wgAction');
 
/* DoAllow preview testing, but not rearrange the table ininitial edit or preview screensscreen */
if (action === 'edit') {
return;
Line 139 ⟶ 365:
 
/*
* ReuseKeep the hardware-search classescontrols sooutside boththe searchresponsive barstable havewrapper.
* This prevents the samesearch field from being clipped visualon appearancedesktop.
*/
var tableWrapper = table.closest('.example-model-table-wrapper');
 
var tableBlock = document.createElement('div');
tableBlock.className =
'benchmark-table-block example-model-table-block';
 
tableBlock.style.display = 'block';
table.parentNode.insertBefore(tableBlock, table);
tableBlock.appendChild(table)style.width = '100%';
tableBlock.style.maxWidth = '100%';
tableBlock.style.marginLeft = '0';
tableBlock.style.marginRight = '0';
 
if (tableWrapper && tableWrapper.parentNode) {
tableWrapper.parentNode.insertBefore(
tableBlock,
tableWrapper
);
 
tableBlock.appendChild(tableWrapper);
} else {
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';
'benchmark-table-controls example-model-table-controls';
controls.style.boxSizing = 'border-box';
 
var searchWrapper = document.createElement('div');
Line 180 ⟶ 433:
controls.appendChild(resultCount);
 
if (tableWrapper) {
tableBlock.insertBefore(controls, table);
tableBlock.insertBefore(
controls,
tableWrapper
);
} else {
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(
Line 207 ⟶ 466:
var activeRowspans = [];
var records = [];
 
function normaliseText(value) {
return String(value || '')
.toLocaleLowerCase()
.replace(/\s+/g, ' ')
.trim();
}
 
function getCellText(cell) {
return cell
? normaliseTexttuflowNormaliseSearchText(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 = [];
Line 270 ⟶ 517:
var modelNameCell = logicalCells[3] || null;
 
/*
* When one cell spans both category columns, there is no
* separate subcategory.
*/
var categorySpansBoth =
categoryCell &&
Line 300 ⟶ 543:
subcategoryKey:
subcategory || '__no_subcategory__',
searchableText: normaliseTexttuflowNormaliseSearchText(
[
category,
Line 339 ⟶ 582:
 
records.forEach(function (record) {
tuflowClearHighlights(record.originalRow);
rowParent.appendChild(record.originalRow);
});
}
 
function getRunLength(items, startIndex, keyFunction) {
var startingKey = keyFunction(items[startIndex]);,
startIndex,
keyFunction
) {
var startingKey =
keyFunction(items[startIndex]);
var count = 1;
var index;
Line 353 ⟶ 602:
index += 1
) {
if (keyFunction(items[index]) !== startingKey) {
keyFunction(items[index]) !== startingKey
) {
break;
}
Line 363 ⟶ 614:
}
 
function renderFilteredRows(matches, query) {
/*
* 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
index > 0 ? matches[index - 1] : null;
: null;
 
var categoryStarts =
!previous ||
previous.categoryKey !== record.categoryKey;
record.categoryKey;
 
var subcategoryStarts =
Line 439 ⟶ 686:
cloneCell(record.modelNameCell)
);
 
if (query) {
tuflowHighlightMatches(row, query);
}
 
rowParent.appendChild(row);
Line 445 ⟶ 696:
 
function filterRows() {
var queryrawQuery = normaliseText(input.value.trim();
var matches;
 
if (!queryrawQuery) {
restoreOriginalRows();
 
Line 459 ⟶ 710:
 
matches = records.filter(function (record) {
return record.searchableText.includestuflowMatchesSearch(query);
record.searchableText,
rawQuery
);
});
 
renderFilteredRows(matches, rawQuery);
 
resultCount.textContent =
Line 474 ⟶ 728:
input.addEventListener('input', filterRows);
 
clearButton.addEventListener('click', function () {
input.value = 'click';,
filterRowsfunction (); {
input.focus()value = '';
filterRows();
input.focus();
}
);
 
filterRows();
});
});
 
/* -------------------------------------------------------------------------
* QGIS Useful Plugins table 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('.qgis-plugin-results-table').each(function () {
var table = this;
 
if (table.dataset.pluginSearchInitialised === 'true') {
return;
}
 
table.dataset.pluginSearchInitialised = 'true';
 
var tableBlock = document.createElement('div');
tableBlock.className =
'benchmark-table-block qgis-plugin-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 plugin name, description, or installation source';
input.setAttribute(
'aria-label',
'Search QGIS plugins'
);
 
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 clearHighlights(root) {
Array.from(
root.querySelectorAll('mark.search-highlight')
).forEach(function (mark) {
var parent = mark.parentNode;
 
if (!parent) {
return;
}
 
parent.replaceChild(
document.createTextNode(mark.textContent),
mark
);
 
parent.normalize();
});
}
 
function escapeRegExp(value) {
return String(value).replace(
/[.*+?^${}()|[\]\\]/g,
'\\$&'
);
}
 
function highlightMatches(root, query) {
var pattern;
var walker;
var matchingNodes = [];
var node;
 
if (!query) {
return;
}
 
pattern = new RegExp(
escapeRegExp(query),
'ig'
);
 
walker = document.createTreeWalker(
root,
NodeFilter.SHOW_TEXT,
{
acceptNode: function (textNode) {
if (
!textNode.nodeValue ||
!textNode.nodeValue.trim()
) {
return NodeFilter.FILTER_REJECT;
}
 
if (
textNode.parentNode &&
textNode.parentNode.closest(
'mark, script, style, input, button'
)
) {
return NodeFilter.FILTER_REJECT;
}
 
return pattern.test(textNode.nodeValue)
? NodeFilter.FILTER_ACCEPT
: NodeFilter.FILTER_REJECT;
}
}
);
 
while ((node = walker.nextNode())) {
matchingNodes.push(node);
}
 
matchingNodes.forEach(function (textNode) {
var text = textNode.nodeValue;
var fragment =
document.createDocumentFragment();
var lastIndex = 0;
 
text.replace(
pattern,
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)
)
);
}
 
textNode.parentNode.replaceChild(
fragment,
textNode
);
});
}
 
function filterRows() {
var rawQuery = input.value.trim();
var visibleCount = 0;
 
rows.forEach(function (row) {
tuflowClearHighlights(row);
 
var matches = tuflowMatchesSearch(
row.textContent,
rawQuery
);
 
row.hidden = !matches;
 
if (matches) {
visibleCount += 1;
 
if (rawQuery) {
tuflowHighlightMatches(row, rawQuery);
}
}
});
 
resultCount.textContent = rawQuery
? visibleCount +
' matching plugin' +
(visibleCount === 1 ? '' : 's')
: rows.length + ' QGIS plugins';
 
clearButton.hidden = rawQuery === '';
}
 
input.addEventListener('input', filterRows);
 
clearButton.addEventListener(
'click',
function () {
input.value = '';
filterRows();
input.focus();
}
);
 
filterRows();