MediaWiki:Citizen.js: Difference between revisions
MediaWiki interface page
More actions
Joeeasterly (talk | contribs) No edit summary |
Joeeasterly (talk | contribs) No edit summary |
| (One intermediate revision by the same user not shown) | |
(No difference)
| |
Latest revision as of 22:56, 2 February 2026
/* All JavaScript here will be loaded for users of the Citizen skin */
/**
* Dashboard Quick-Jump (v2)
* Listens for typing on the Dashboard page and jumps to matching cards.
* Now with description searching and strict edit-mode discipline.
*/
(function() {
// 1. GLOBAL GUARD: If we aren't in 'view' mode, kill the script immediately.
// We do not want this running on ?action=edit, ?action=history, etc.
if (mw.config.get('wgAction') !== 'view') return;
// 2. CONTEXT GUARD: If there are no dashboard cards, go back to sleep.
if (!document.querySelector('.dashboard-card')) return;
const CONFIG = {
selectorCard: '.dashboard-card',
selectorLabel: '.dashboard-label',
selectorDesc: '.dashboard-desc', // New: Search descriptions too
selectorLink: 'a',
classActive: 'dashboard-jump-active',
timeout: 1500
};
let searchBuffer = '';
let clearTimer = null;
document.addEventListener('keydown', function(e) {
// 3. EDITING GUARD:
// If the user is typing in a form field, a contentEditable div (VisualEditor),
// or if the html has the 've-active' class (VisualEditor active state), ignore them.
const targetTag = e.target.tagName.toLowerCase();
const isInput = ['input', 'textarea', 'select'].includes(targetTag);
const isEditable = e.target.isContentEditable;
const isVEActive = document.documentElement.classList.contains('ve-active');
if (isInput || isEditable || isVEActive) {
return;
}
// 4. Modifier Key Guard: Ctrl, Alt, Meta (Command) are strictly off-limits.
if (e.ctrlKey || e.altKey || e.metaKey) {
return;
}
// Logic handling
if (e.key === 'Escape') {
resetSearch();
return;
}
if (e.key === 'Enter') {
const active = document.querySelector(`.${CONFIG.classActive} ${CONFIG.selectorLink}`);
if (active) {
e.preventDefault();
active.click();
}
return;
}
if (e.key === 'Backspace') {
searchBuffer = searchBuffer.slice(0, -1);
if (searchBuffer.length === 0) resetSearch();
else updateSelection();
return;
}
// Capture typing (Single character keys only)
if (e.key.length === 1) {
// Optional: If you want to prevent Citizen's shortcuts (like '/')
// if (e.key === '/') e.stopPropagation();
searchBuffer += e.key;
updateSelection();
// Reset the "clear buffer" timer
clearTimeout(clearTimer);
clearTimer = setTimeout(resetSearch, CONFIG.timeout);
}
});
function updateSelection() {
// Clear previous highlighting
document.querySelectorAll(`.${CONFIG.classActive}`).forEach(el => el.classList.remove(CONFIG.classActive));
if (!searchBuffer) return;
const cards = document.querySelectorAll(CONFIG.selectorCard);
// Escape regex characters to prevent syntax errors if you type a bracket
const safeBuffer = searchBuffer.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
const regex = new RegExp(`^${safeBuffer}`, 'i'); // Case-insensitive, starts-with
for (const card of cards) {
// Get text content
const labelEl = card.querySelector(CONFIG.selectorLabel);
const descEl = card.querySelector(CONFIG.selectorDesc);
const labelText = labelEl ? labelEl.innerText.trim() : '';
const descText = descEl ? descEl.innerText.trim() : '';
// Priority 1: Check the Label (Title)
if (labelText && regex.test(labelText)) {
activateCard(card);
return;
}
// Priority 2: Check the Description
if (descText && regex.test(descText)) {
activateCard(card);
return;
}
}
}
function activateCard(card) {
card.classList.add(CONFIG.classActive);
card.scrollIntoView({ behavior: 'smooth', block: 'center' });
}
function resetSearch() {
searchBuffer = '';
document.querySelectorAll(`.${CONFIG.classActive}`).forEach(el => el.classList.remove(CONFIG.classActive));
}
})();