Toggle menu
Toggle preferences menu
Toggle personal menu
Not logged in
Your IP address will be publicly visible if you make any edits.

MediaWiki:Citizen.js: Difference between revisions

MediaWiki interface page
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;..."
 
No edit summary
Line 1: Line 1:
/* All JavaScript here will be loaded for users of the Citizen skin */
/**
/**
  * Dashboard Quick-Jump (v2)
  * 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.
  * Now with description searching and strict edit-mode discipline.
  * Aggressively disables itself if ANY editing interface is detected.
  */
  */
(function() {
(function() {
     // 1. GLOBAL GUARD: If we aren't in 'view' mode, kill the script immediately.
     // 1. INITIAL LOAD GUARD
     // We do not want this running on ?action=edit, ?action=history, etc.
    // 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: If there are no dashboard cards, go back to sleep.
     // 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', // New: Search descriptions too
         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. EDITING GUARD:
         // 3. DYNAMIC EDITOR DETECTION (The Fix)
         // If the user is typing in a form field, a contentEditable div (VisualEditor),
        // Check for VisualEditor, NWE, or Source Editor surfaces.
         // or if the html has the 've-active' class (VisualEditor active state), ignore them.
         // 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;
        const isVEActive = document.documentElement.classList.contains('ve-active');


         if (isInput || isEditable || isVEActive) {
         if (isInput || isEditable) {
             return;
             return;
         }
         }


         // 4. Modifier Key Guard: Ctrl, Alt, Meta (Command) are strictly off-limits.
         // 5. MODIFIER GUARD
         if (e.ctrlKey || e.altKey || e.metaKey) {
         if (e.ctrlKey || e.altKey || e.metaKey) {
             return;
             return;
         }
         }


         // Logic handling
         // --- 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) {
            // Optional: If you want to prevent Citizen's shortcuts (like '/')
            // if (e.key === '/') e.stopPropagation();
             searchBuffer += e.key;
             searchBuffer += e.key;
             updateSelection();
             updateSelection();
Line 80: Line 92:


     function updateSelection() {
     function updateSelection() {
         // Clear previous highlighting
         // 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 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'); // Case-insensitive, starts-with
         const regex = new RegExp(`^${safeBuffer}`, 'i');  


         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 99: Line 109:
             const descText = descEl ? descEl.innerText.trim() : '';
             const descText = descEl ? descEl.innerText.trim() : '';


             // Priority 1: Check the Label (Title)
             // Priority 1: Label
             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);

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));
    }

})();