MediaWiki:Citizen.js
MediaWiki interface page
More actions
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.
/**
* Dashboard Quick-Jump (v3 - The "Shy" Version)
* Listens for typing on the Dashboard page and jumps to matching cards.
* Aggressively disables itself if ANY editing interface is detected.
*/
(function() {
// 1. INITIAL LOAD GUARD
// If we are already in a non-view action, or the URL indicates an edit intent, stop immediately.
const uri = new URL(window.location.href);
if (mw.config.get('wgAction') !== 'view') return;
if (uri.searchParams.has('veaction')) return;
if (uri.searchParams.has('action') && uri.searchParams.get('action') !== 'view') return;
// 2. CONTEXT GUARD
// If there are no dashboard cards, don't even add the listener.
if (!document.querySelector('.dashboard-card')) return;
const CONFIG = {
selectorCard: '.dashboard-card',
selectorLabel: '.dashboard-label',
selectorDesc: '.dashboard-desc',
selectorLink: 'a',
classActive: 'dashboard-jump-active',
timeout: 1500
};
let searchBuffer = '';
let clearTimer = null;
document.addEventListener('keydown', function(e) {
// 3. DYNAMIC EDITOR DETECTION (The Fix)
// Check for VisualEditor, NWE, or Source Editor surfaces.
// If these exist, the user is editing. Do not interfere.
const isVEActive = document.documentElement.classList.contains('ve-active');
const hasVESurface = document.querySelector('.ve-ui-surface');
const hasWikiEditor = document.querySelector('.wikiEditor-ui');
if (isVEActive || hasVESurface || hasWikiEditor) {
return;
}
// 4. INPUT GUARD
// Standard check for typing in search bars, forms, etc.
const targetTag = e.target.tagName.toLowerCase();
const isInput = ['input', 'textarea', 'select'].includes(targetTag);
const isEditable = e.target.isContentEditable;
if (isInput || isEditable) {
return;
}
// 5. MODIFIER GUARD
if (e.ctrlKey || e.altKey || e.metaKey) {
return;
}
// --- LOGIC STARTS HERE ---
if (e.key === 'Escape') {
resetSearch();
return;
}
if (e.key === 'Enter') {
const active = document.querySelector(`.${CONFIG.classActive} ${CONFIG.selectorLink}`);
if (active) {
// Only prevent default if we actually have a card selected
e.preventDefault();
e.stopPropagation();
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) {
searchBuffer += e.key;
updateSelection();
// Reset the "clear buffer" timer
clearTimeout(clearTimer);
clearTimer = setTimeout(resetSearch, CONFIG.timeout);
}
});
function updateSelection() {
// Clean up previous highlights
document.querySelectorAll(`.${CONFIG.classActive}`).forEach(el => el.classList.remove(CONFIG.classActive));
if (!searchBuffer) return;
const cards = document.querySelectorAll(CONFIG.selectorCard);
// Escape regex to prevent crashes on special chars
const safeBuffer = searchBuffer.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
const regex = new RegExp(`^${safeBuffer}`, 'i');
for (const card of cards) {
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: Label
if (labelText && regex.test(labelText)) {
activateCard(card);
return;
}
// Priority 2: 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));
}
})();