|
|
| Line 1: |
Line 1: |
| /* All JavaScript here will be loaded for users of the Citizen skin */ | | /* 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));
| |
| }
| |
|
| |
| })();
| |