MediaWiki:Citizen.js: Difference between revisions
MediaWiki interface page
More actions
Joeeasterly (talk | contribs) No edit summary |
Joeeasterly (talk | contribs) No edit summary |
||
| (4 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() { | (function() { | ||
// 1. | // 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; | if (mw.config.get('wgAction') !== 'view') return; | ||
// 2. CONTEXT GUARD | // 2. CONTEXT GUARD: If there are no dashboard cards, go back to sleep. | ||
if (!document.querySelector('.dashboard-card')) return; | if (!document.querySelector('.dashboard-card')) return; | ||
| Line 19: | Line 16: | ||
selectorCard: '.dashboard-card', | selectorCard: '.dashboard-card', | ||
selectorLabel: '.dashboard-label', | selectorLabel: '.dashboard-label', | ||
selectorDesc: '.dashboard-desc', | selectorDesc: '.dashboard-desc', // New: Search descriptions too | ||
selectorLink: 'a', | selectorLink: 'a', | ||
classActive: 'dashboard-jump-active', | classActive: 'dashboard-jump-active', | ||
| Line 29: | Line 26: | ||
document.addEventListener('keydown', function(e) { | document.addEventListener('keydown', function(e) { | ||
// 3. | // 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 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; | ||
const isVEActive = document.documentElement.classList.contains('ve-active'); | |||
if (isInput || isEditable) { | if (isInput || isEditable || isVEActive) { | ||
return; | return; | ||
} | } | ||
// | // 4. Modifier Key Guard: Ctrl, Alt, Meta (Command) are strictly off-limits. | ||
if (e.ctrlKey || e.altKey || e.metaKey) { | if (e.ctrlKey || e.altKey || e.metaKey) { | ||
return; | return; | ||
} | } | ||
// | // Logic handling | ||
if (e.key === 'Escape') { | if (e.key === 'Escape') { | ||
resetSearch(); | resetSearch(); | ||
| Line 65: | Line 52: | ||
const active = document.querySelector(`.${CONFIG.classActive} ${CONFIG.selectorLink}`); | const active = document.querySelector(`.${CONFIG.classActive} ${CONFIG.selectorLink}`); | ||
if (active) { | if (active) { | ||
e.preventDefault(); | e.preventDefault(); | ||
active.click(); | active.click(); | ||
} | } | ||
| Line 82: | Line 67: | ||
// Capture typing (Single character keys only) | // Capture typing (Single character keys only) | ||
if (e.key.length === 1) { | if (e.key.length === 1) { | ||
// Optional: If you want to prevent Citizen's shortcuts (like '/') | |||
// if (e.key === '/') e.stopPropagation(); | |||
searchBuffer += e.key; | searchBuffer += e.key; | ||
updateSelection(); | updateSelection(); | ||
| Line 92: | Line 80: | ||
function updateSelection() { | function updateSelection() { | ||
// | // Clear previous highlighting | ||
document.querySelectorAll(`.${CONFIG.classActive}`).forEach(el => el.classList.remove(CONFIG.classActive)); | document.querySelectorAll(`.${CONFIG.classActive}`).forEach(el => el.classList.remove(CONFIG.classActive)); | ||
| Line 98: | Line 86: | ||
const cards = document.querySelectorAll(CONFIG.selectorCard); | const cards = document.querySelectorAll(CONFIG.selectorCard); | ||
// Escape regex to prevent | |||
// Escape regex characters to prevent syntax errors if you type a bracket | |||
const safeBuffer = searchBuffer.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); | const safeBuffer = searchBuffer.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); | ||
const regex = new RegExp(`^${safeBuffer}`, 'i'); | const regex = new RegExp(`^${safeBuffer}`, 'i'); // Case-insensitive, starts-with | ||
for (const card of cards) { | for (const card of cards) { | ||
// Get text content | |||
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 109: | Line 99: | ||
const descText = descEl ? descEl.innerText.trim() : ''; | const descText = descEl ? descEl.innerText.trim() : ''; | ||
// Priority 1: Label | // Priority 1: Check the Label (Title) | ||
if (labelText && regex.test(labelText)) { | if (labelText && regex.test(labelText)) { | ||
activateCard(card); | activateCard(card); | ||
return; | return; | ||
} | } | ||
// Priority 2: Description | |||
// Priority 2: Check the Description | |||
if (descText && regex.test(descText)) { | if (descText && regex.test(descText)) { | ||
activateCard(card); | activateCard(card); | ||
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));
}
})();