MediaWiki:Citizen.js: Difference between revisions
MediaWiki interface page
More actions
Joeeasterly (talk | contribs) No edit summary |
Joeeasterly (talk | contribs) No edit summary |
||
| (3 intermediate revisions by the same user not shown) | |||
| Line 1: | Line 1: | ||
/* All JavaScript here will be loaded for users of the Citizen skin */ | |||
/** | /** | ||
* Dashboard Quick-Jump ( | * Dashboard Quick-Jump (v2) | ||
* Listens for typing on the Dashboard page and jumps to matching cards. | * 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}`); | |||
const | if (active) { | ||
if ( | e.preventDefault(); | ||
active.click(); | |||
} | } | ||
return; | |||
} | |||
if (e.key === 'Backspace') { | |||
if ( | searchBuffer = searchBuffer.slice(0, -1); | ||
if (searchBuffer.length === 0) resetSearch(); | |||
else updateSelection(); | |||
return; | |||
} | |||
// Capture typing (Single character keys only) | |||
if (e.key.length === 1) { | |||
if (e.key === ' | // 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 | |||
const | for (const card of cards) { | ||
// Get text content | |||
const | const labelEl = card.querySelector(CONFIG.selectorLabel); | ||
const | 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)); | |||
} | } | ||
}); | |||
})(); | |||
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));
}
})();