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
No edit summary
No edit summary
Line 1: Line 1:
/* All JavaScript here will be loaded for users of the Citizen skin */
/**
/**
  * Dashboard Quick-Jump (v5 - ES5 Compatible)
  * 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.
  * Replaced modern syntax to appease the MediaWiki ResourceLoader gods.
  * Now with description searching and strict edit-mode discipline.
  */
  */
$(function() {  
(function() {
     try {
     // 1. GLOBAL GUARD: If we aren't in 'view' mode, kill the script immediately.
        // 1. EDIT MODE GUARD (URL Check)
    // We do not want this running on ?action=edit, ?action=history, etc.
        // Check if we are trying to edit.
    if (mw.config.get('wgAction') !== 'view') return;
        var search = window.location.search;
        if (search.indexOf('veaction') !== -1) return;
        if (search.indexOf('action=edit') !== -1) return;
        if (search.indexOf('action=submit') !== -1) return;


        // 2. CONTEXT GUARD
    // 2. CONTEXT GUARD: If there are no dashboard cards, go back to sleep.
        // If there are no dashboard cards, exit.
    if (!document.querySelector('.dashboard-card')) return;
        var cards = document.querySelectorAll('.dashboard-card');
        if (cards.length === 0) return;


        var CONFIG = {
    const CONFIG = {
            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',
            timeout: 1500
        timeout: 1500
        };
    };


        var searchBuffer = '';
    let searchBuffer = '';
        var clearTimer = null;
    let clearTimer = null;


        document.addEventListener('keydown', function(e) {
    document.addEventListener('keydown', function(e) {
            // 3. EDIT INTERFACE GUARD (Dynamic)
        // 3. EDITING GUARD:
            // Check for active editor classes or elements
        // If the user is typing in a form field, a contentEditable div (VisualEditor),
            if (document.documentElement.classList.contains('ve-active') ||  
        // or if the html has the 've-active' class (VisualEditor active state), ignore them.
                document.querySelector('.ve-ui-surface') ||  
        const targetTag = e.target.tagName.toLowerCase();
                document.querySelector('.wikiEditor-ui')) {
        const isInput = ['input', 'textarea', 'select'].includes(targetTag);
                return;
        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;
        }


            // 4. INPUT GUARD
        if (e.key === 'Enter') {
            // Don't intercept typing in search bars or inputs
             const active = document.querySelector(`.${CONFIG.classActive} ${CONFIG.selectorLink}`);
             var target = e.target;
             if (active) {
            var tagName = target.tagName;
                 e.preventDefault();
             if (tagName === 'INPUT' ||
                 active.click();
                 tagName === 'TEXTAREA' ||
                tagName === 'SELECT' ||
                 target.isContentEditable) {
                return;
             }
             }
            return;
        }


             // 5. MODIFIER GUARD
        if (e.key === 'Backspace') {
             if (e.ctrlKey || e.altKey || e.metaKey) return;
             searchBuffer = searchBuffer.slice(0, -1);
             if (searchBuffer.length === 0) resetSearch();
            else updateSelection();
            return;
        }


            // --- Logic ---
        // Capture typing (Single character keys only)
              
        if (e.key.length === 1) {
             if (e.key === 'Escape') {
             // Optional: If you want to prevent Citizen's shortcuts (like '/')
                resetSearch();
             // if (e.key === '/') e.stopPropagation();  
                return;
            }


             if (e.key === 'Enter') {
             searchBuffer += e.key;
                var active = document.querySelector('.' + CONFIG.classActive + ' ' + CONFIG.selectorLink);
            updateSelection();
                if (active) {
                    e.preventDefault();
                    e.stopPropagation();
                    active.click();
                }
                return;
            }


             if (e.key === 'Backspace') {
             // Reset the "clear buffer" timer
                searchBuffer = searchBuffer.slice(0, -1);
            clearTimeout(clearTimer);
                if (searchBuffer.length === 0) resetSearch();
            clearTimer = setTimeout(resetSearch, CONFIG.timeout);
                else updateSelection();
        }
                return;
    });
            }


            // Capture single char keys (letters/numbers)
    function updateSelection() {
            if (e.key.length === 1) {
        // Clear previous highlighting
                searchBuffer += e.key;
        document.querySelectorAll(`.${CONFIG.classActive}`).forEach(el => el.classList.remove(CONFIG.classActive));
                updateSelection();
               
                clearTimeout(clearTimer);
                clearTimer = setTimeout(resetSearch, CONFIG.timeout);
            }
        });


         function updateSelection() {
         if (!searchBuffer) return;
            // Clear previous highlights
            var actives = document.querySelectorAll('.' + CONFIG.classActive);
            for (var i = 0; i < actives.length; i++) {
                actives[i].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


             // Escape special regex chars
        for (const card of cards) {
             var safeBuffer = searchBuffer.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
             // Get text content
             var regex = new RegExp('^' + safeBuffer, 'i');  
            const labelEl = card.querySelector(CONFIG.selectorLabel);
             const descEl = card.querySelector(CONFIG.selectorDesc);
           
            const labelText = labelEl ? labelEl.innerText.trim() : '';
             const descText = descEl ? descEl.innerText.trim() : '';


             for (var j = 0; j < cards.length; j++) {
             // Priority 1: Check the Label (Title)
                var card = cards[j];
            if (labelText && regex.test(labelText)) {
                var labelEl = card.querySelector(CONFIG.selectorLabel);
                 activateCard(card);
                var descEl = card.querySelector(CONFIG.selectorDesc);
                 return;
               
            }
                // Old school null checks (No optional chaining ?.)
                 var label = labelEl ? labelEl.innerText.trim() : '';
                 var desc = descEl ? descEl.innerText.trim() : '';


                // Priority 1: Label match
            // Priority 2: Check the Description
                if (label && regex.test(label)) {
            if (descText && regex.test(descText)) {
                    activateCard(card);
                activateCard(card);
                    return;
                return;
                }
                // Priority 2: Description match
                if (desc && regex.test(desc)) {
                    activateCard(card);
                    return;
                }
             }
             }
         }
         }
    }


        function activateCard(card) {
    function activateCard(card) {
            card.classList.add(CONFIG.classActive);
        card.classList.add(CONFIG.classActive);
            card.scrollIntoView({ behavior: 'smooth', block: 'center' });
        card.scrollIntoView({ behavior: 'smooth', block: 'center' });
        }
    }


        function resetSearch() {
    function resetSearch() {
            searchBuffer = '';
        searchBuffer = '';
            var actives = document.querySelectorAll('.' + CONFIG.classActive);
        document.querySelectorAll(`.${CONFIG.classActive}`).forEach(el => el.classList.remove(CONFIG.classActive));
            for (var i = 0; i < actives.length; i++) {
                actives[i].classList.remove(CONFIG.classActive);
            }
        }
   
    } catch (err) {
        console.error('Hallyu Dashboard Script Error:', err);
     }
     }
});
 
})();

Revision as of 00:34, 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));
    }

})();