MediaWiki:Citizen.js: Difference between revisions
MediaWiki interface page
More actions
Joeeasterly (talk | contribs) Created page with "→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;..." |
Joeeasterly (talk | contribs) No edit summary |
||
| Line 1: | Line 1: | ||
/** | /** | ||
* Dashboard Quick-Jump ( | * Dashboard Quick-Jump (v3 - The "Shy" Version) | ||
* Listens for typing on the Dashboard page and jumps to matching cards. | * Listens for typing on the Dashboard page and jumps to matching cards. | ||
* | * Aggressively disables itself if ANY editing interface is detected. | ||
*/ | */ | ||
(function() { | (function() { | ||
// 1. | // 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 (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 | // 2. CONTEXT GUARD | ||
// If there are no dashboard cards, don't even add the listener. | |||
if (!document.querySelector('.dashboard-card')) return; | if (!document.querySelector('.dashboard-card')) return; | ||
| Line 16: | Line 19: | ||
selectorCard: '.dashboard-card', | selectorCard: '.dashboard-card', | ||
selectorLabel: '.dashboard-label', | selectorLabel: '.dashboard-label', | ||
selectorDesc: '.dashboard-desc', | selectorDesc: '.dashboard-desc', | ||
selectorLink: 'a', | selectorLink: 'a', | ||
classActive: 'dashboard-jump-active', | classActive: 'dashboard-jump-active', | ||
| Line 26: | Line 29: | ||
document.addEventListener('keydown', function(e) { | document.addEventListener('keydown', function(e) { | ||
// 3. | // 3. DYNAMIC EDITOR DETECTION (The Fix) | ||
// If the user is | // 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 targetTag = e.target.tagName.toLowerCase(); | ||
const isInput = ['input', 'textarea', 'select'].includes(targetTag); | const isInput = ['input', 'textarea', 'select'].includes(targetTag); | ||
const isEditable = e.target.isContentEditable; | const isEditable = e.target.isContentEditable; | ||
if (isInput || isEditable | if (isInput || isEditable) { | ||
return; | return; | ||
} | } | ||
// | // 5. MODIFIER GUARD | ||
if (e.ctrlKey || e.altKey || e.metaKey) { | if (e.ctrlKey || e.altKey || e.metaKey) { | ||
return; | return; | ||
} | } | ||
// | // --- LOGIC STARTS HERE --- | ||
if (e.key === 'Escape') { | if (e.key === 'Escape') { | ||
resetSearch(); | resetSearch(); | ||
| Line 52: | Line 65: | ||
const active = document.querySelector(`.${CONFIG.classActive} ${CONFIG.selectorLink}`); | const active = document.querySelector(`.${CONFIG.classActive} ${CONFIG.selectorLink}`); | ||
if (active) { | if (active) { | ||
// Only prevent default if we actually have a card selected | |||
e.preventDefault(); | e.preventDefault(); | ||
e.stopPropagation(); | |||
active.click(); | active.click(); | ||
} | } | ||
| Line 67: | Line 82: | ||
// Capture typing (Single character keys only) | // Capture typing (Single character keys only) | ||
if (e.key.length === 1) { | if (e.key.length === 1) { | ||
searchBuffer += e.key; | searchBuffer += e.key; | ||
updateSelection(); | updateSelection(); | ||
| Line 80: | Line 92: | ||
function updateSelection() { | function updateSelection() { | ||
// | // Clean up previous highlights | ||
document.querySelectorAll(`.${CONFIG.classActive}`).forEach(el => el.classList.remove(CONFIG.classActive)); | document.querySelectorAll(`.${CONFIG.classActive}`).forEach(el => el.classList.remove(CONFIG.classActive)); | ||
| Line 86: | Line 98: | ||
const cards = document.querySelectorAll(CONFIG.selectorCard); | const cards = document.querySelectorAll(CONFIG.selectorCard); | ||
// Escape regex to prevent crashes on special chars | |||
// Escape regex | |||
const safeBuffer = searchBuffer.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); | const safeBuffer = searchBuffer.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); | ||
const regex = new RegExp(`^${safeBuffer}`, 'i'); | const regex = new RegExp(`^${safeBuffer}`, 'i'); | ||
for (const card of cards) { | for (const card of cards) { | ||
const labelEl = card.querySelector(CONFIG.selectorLabel); | const labelEl = card.querySelector(CONFIG.selectorLabel); | ||
const descEl = card.querySelector(CONFIG.selectorDesc); | const descEl = card.querySelector(CONFIG.selectorDesc); | ||
| Line 99: | Line 109: | ||
const descText = descEl ? descEl.innerText.trim() : ''; | const descText = descEl ? descEl.innerText.trim() : ''; | ||
// Priority 1: | // Priority 1: Label | ||
if (labelText && regex.test(labelText)) { | if (labelText && regex.test(labelText)) { | ||
activateCard(card); | activateCard(card); | ||
return; | return; | ||
} | } | ||
// Priority 2: Description | |||
// Priority 2: | |||
if (descText && regex.test(descText)) { | if (descText && regex.test(descText)) { | ||
activateCard(card); | activateCard(card); | ||
Revision as of 00:16, 2 February 2026
/**
* 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));
}
})();