MediaWiki:Common.js: Difference between revisions

No edit summary
No edit summary
 
(5 intermediate revisions by the same user not shown)
Line 228: Line 228:


/**
/**
  * Quick Search — DEBUG v2
  * Quick Search Widgets Reusable autocomplete for CRT & AV device lookup
  * Focused on finding the right event for OOUI autocomplete selection
* Each widget is configured via a simple object (container ID, controls ID,
* category, placeholder text). The Main Page wikitext contains the styled
  * containers with "Loading search…" placeholders; this script replaces them
* with the actual input, button, and dropdown, then wires up event handlers.
  */
  */
(function () {
(function () {
Line 236: Line 239:
     if (mw.config.get('wgPageName') !== 'Main_Page') return;
     if (mw.config.get('wgPageName') !== 'Main_Page') return;


     function init() {
    // ── Shared config ───────────────────────────────────────────────
         var container = document.getElementById('crt-quick-search');
    var API_URL      = mw.util.wikiScript('api');
         if (!container) return;
    var DEBOUNCE_MS  = 250;
    var MIN_CHARS    = 2;
 
    // ── Helpers ─────────────────────────────────────────────────────
     function navigate(title) {
        title = (title || '').trim();
        if (title) window.location.href = mw.util.getUrl(title);
    }
 
    // ── Styles (injected once) ──────────────────────────────────────
    function injectStyles() {
        if (document.getElementById('ecrt-qs-styles')) return;
         var style = document.createElement('style');
        style.id = 'ecrt-qs-styles';
        style.textContent = [
            /* ── Base (blue) container & label ── */
            '.ecrt-qs-container {',
            '  background: linear-gradient(135deg, #f0f9ff 0%, #e0f2fe 100%);',
            '  border: 2px solid #7dd3fc;',
            '  border-radius: 0.75rem;',
            '  padding: 1rem 1.25rem;',
            '  margin-bottom: 1.25rem;',
            '  box-shadow: 0 2px 4px rgba(0,0,0,0.05);',
            '}',
            '.ecrt-qs-label {',
            '  font-weight: 600;',
            '  color: #0369a1;',
            '  margin-bottom: 0.5rem;',
            '  font-size: 0.9rem;',
            '}',
 
            /* ── Green variant (AV devices) ── */
            '.ecrt-qs-container--av {',
            '  background: linear-gradient(135deg, #f0fdf4 0%, #dcfce7 100%);',
            '  border-color: #86efac;',
            '}',
            '.ecrt-qs-container--av .ecrt-qs-label {',
            '  color: #15803d;',
            '}',
            '.ecrt-qs-container--av .ecrt-qs-input:focus {',
            '  border-color: #16a34a;',
            '  box-shadow: 0 0 0 3px rgba(22,163,74,0.15);',
            '}',
            '.ecrt-qs-container--av .ecrt-qs-btn {',
            '  background: #16a34a;',
            '}',
            '.ecrt-qs-container--av .ecrt-qs-btn:hover {',
            '  background: #15803d;',
            '}',
            '.ecrt-qs-container--av .ecrt-qs-btn:active {',
            '  background: #166534;',
            '}',
            '.ecrt-qs-container--av .ecrt-qs-item:hover,',
            '.ecrt-qs-container--av .ecrt-qs-item.is-active {',
            '  background: #dcfce7;',
            '  color: #14532d;',
            '}',
 
            /* Placeholder shown before JS loads */
            '.ecrt-qs-placeholder {',
            '  padding: 0.625rem 0.875rem;',
            '  font-size: 0.95rem;',
            '  border: 1.5px solid #cbd5e1;',
            '  border-radius: 0.5rem;',
            '  background: #f1f5f9;',
            '  color: #94a3b8;',
            '  font-style: italic;',
            '}',
 
            /* Interactive controls */
            '.ecrt-qs-wrap {',
            '  position: relative;',
            '}',
            '.ecrt-qs-row {',
            '  display: flex;',
            '  gap: 0.5rem;',
            '}',
            '.ecrt-qs-input {',
            '  flex: 1;',
            '  padding: 0.625rem 0.875rem;',
            '  font-size: 0.95rem;',
            '  border: 1.5px solid #94a3b8;',
            '  border-radius: 0.5rem;',
            '  outline: none;',
            '  background: #fff;',
            '  color: #1e293b;',
            '  transition: border-color 0.15s, box-shadow 0.15s;',
            '  min-width: 0;',
            '}',
            '.ecrt-qs-input:focus {',
            '  border-color: #0284c7;',
            '  box-shadow: 0 0 0 3px rgba(2,132,199,0.15);',
            '}',
            '.ecrt-qs-input::placeholder { color: #94a3b8; }',
            '.ecrt-qs-btn {',
            '  padding: 0.625rem 1.25rem;',
            '  font-size: 0.95rem;',
            '  font-weight: 600;',
            '  color: #fff;',
            '  background: #0284c7;',
            '  border: none;',
            '  border-radius: 0.5rem;',
            '  cursor: pointer;',
            '  transition: background 0.15s;',
            '  white-space: nowrap;',
            '}',
            '.ecrt-qs-btn:hover { background: #0369a1; }',
            '.ecrt-qs-btn:active { background: #075985; }',
 
            /* Dropdown */
            '.ecrt-qs-dropdown {',
            '  position: absolute;',
            '  top: 100%;',
            '  left: 0;',
            '  right: 0;',
            '  margin-top: 0.25rem;',
            '  background: #fff;',
            '  border: 1.5px solid #cbd5e1;',
            '  border-radius: 0.5rem;',
            '  box-shadow: 0 10px 25px -5px rgba(0,0,0,0.12), 0 4px 6px -2px rgba(0,0,0,0.06);',
            '  max-height: 20rem;',
            '  overflow-y: auto;',
            '  z-index: 1000;',
            '  display: none;',
            '}',
            '.ecrt-qs-dropdown.is-open { display: block; }',
            '.ecrt-qs-item {',
            '  padding: 0.5rem 0.875rem;',
            '  cursor: pointer;',
            '  font-size: 0.925rem;',
            '  color: #334155;',
            '  transition: background 0.1s;',
            '}',
            '.ecrt-qs-item:hover,',
            '.ecrt-qs-item.is-active {',
            '  background: #e0f2fe;',
            '  color: #0c4a6e;',
            '}',
            '.ecrt-qs-item + .ecrt-qs-item {',
            '  border-top: 1px solid #f1f5f9;',
            '}',
            '.ecrt-qs-status {',
            '  padding: 0.625rem 0.875rem;',
            '  font-size: 0.85rem;',
            '  color: #94a3b8;',
            '  font-style: italic;',
            '}',
 
            /* ── Night-mode overrides (base / blue) ── */
            '.skin-theme-clientpref-night .ecrt-qs-container {',
            '  background: linear-gradient(135deg, #0f172a 0%, #1e293b 100%);',
            '  border-color: #334155;',
            '}',
            '.skin-theme-clientpref-night .ecrt-qs-label { color: #7dd3fc; }',
            '.skin-theme-clientpref-night .ecrt-qs-placeholder {',
            '  background: #0f172a;',
            '  border-color: #334155;',
            '  color: #475569;',
            '}',
            '.skin-theme-clientpref-night .ecrt-qs-input {',
            '  background: #1e293b;',
            '  border-color: #475569;',
            '  color: #e2e8f0;',
            '}',
            '.skin-theme-clientpref-night .ecrt-qs-input:focus {',
            '  border-color: #38bdf8;',
            '  box-shadow: 0 0 0 3px rgba(56,189,248,0.2);',
            '}',
            '.skin-theme-clientpref-night .ecrt-qs-input::placeholder { color: #64748b; }',
            '.skin-theme-clientpref-night .ecrt-qs-btn { background: #0369a1; }',
            '.skin-theme-clientpref-night .ecrt-qs-btn:hover { background: #0284c7; }',
            '.skin-theme-clientpref-night .ecrt-qs-dropdown {',
            '  background: #1e293b;',
            '  border-color: #475569;',
            '}',
            '.skin-theme-clientpref-night .ecrt-qs-item { color: #cbd5e1; }',
            '.skin-theme-clientpref-night .ecrt-qs-item:hover,',
            '.skin-theme-clientpref-night .ecrt-qs-item.is-active {',
            '  background: #0c4a6e;',
            '  color: #e0f2fe;',
            '}',
            '.skin-theme-clientpref-night .ecrt-qs-item + .ecrt-qs-item {',
            '  border-top-color: #334155;',
            '}',
            '.skin-theme-clientpref-night .ecrt-qs-status { color: #64748b; }',
 
            /* ── Night-mode overrides (green / AV) ── */
            '.skin-theme-clientpref-night .ecrt-qs-container--av {',
            '  background: linear-gradient(135deg, #0a1f0f 0%, #14291a 100%);',
            '  border-color: #1e4d2b;',
            '}',
            '.skin-theme-clientpref-night .ecrt-qs-container--av .ecrt-qs-label {',
            '  color: #86efac;',
            '}',
            '.skin-theme-clientpref-night .ecrt-qs-container--av .ecrt-qs-input:focus {',
            '  border-color: #4ade80;',
            '  box-shadow: 0 0 0 3px rgba(74,222,128,0.2);',
            '}',
            '.skin-theme-clientpref-night .ecrt-qs-container--av .ecrt-qs-btn {',
            '  background: #15803d;',
            '}',
            '.skin-theme-clientpref-night .ecrt-qs-container--av .ecrt-qs-btn:hover {',
            '  background: #16a34a;',
            '}',
            '.skin-theme-clientpref-night .ecrt-qs-container--av .ecrt-qs-item:hover,',
            '.skin-theme-clientpref-night .ecrt-qs-container--av .ecrt-qs-item.is-active {',
            '  background: #14532d;',
            '  color: #bbf7d0;',
            '}'
        ].join('\n');
        document.head.appendChild(style);
    }
 
    // ── Generic widget initializer ──────────────────────────────────
    // cfg = { containerId, controlsId, category, placeholder }
    function initWidget(cfg) {
        var container = document.getElementById(cfg.containerId);
        var wrap      = document.getElementById(cfg.controlsId);
         if (!container || !wrap) return;
 
        // ── Build interactive controls ──────────────────────────────
        var row = document.createElement('div');
        row.className = 'ecrt-qs-row';
 
        var input = document.createElement('input');
        input.type = 'text';
        input.className = 'ecrt-qs-input';
        input.placeholder = cfg.placeholder;
        input.setAttribute('autocomplete', 'off');
 
        var btn = document.createElement('button');
        btn.type = 'button';
        btn.className = 'ecrt-qs-btn';
        btn.textContent = 'Go';
 
        row.appendChild(input);
        row.appendChild(btn);
 
        var dropdown = document.createElement('div');
        dropdown.className = 'ecrt-qs-dropdown';
 
        // Replace placeholder with real controls
        wrap.innerHTML = '';
        wrap.appendChild(row);
        wrap.appendChild(dropdown);
 
        // ── State ───────────────────────────────────────────────────
        var items    = [];
        var activeIdx = -1;
        var timer    = null;
 
        function openDropdown()  { dropdown.classList.add('is-open'); }
        function closeDropdown() {
            dropdown.classList.remove('is-open');
            activeIdx = -1;
        }


         var form = container.querySelector('form');
         function setActive(idx) {
        if (!form) return;
            var els = dropdown.querySelectorAll('.ecrt-qs-item');
            els.forEach(function (el) { el.classList.remove('is-active'); });
            activeIdx = idx;
            if (idx >= 0 && idx < els.length) {
                els[idx].classList.add('is-active');
                els[idx].scrollIntoView({ block: 'nearest' });
            }
        }


        // Wait for OOUI to create the input
         function renderItems(results) {
         function trySetup() {
             dropdown.innerHTML = '';
             // OOUI creates input with this class
             items = [];
             var input = container.querySelector('input.oo-ui-inputWidget-input, .pfFormInputWrapper input:not([type="hidden"])');
             activeIdx = -1;
             if (!input) return false;


             console.log('[QS] Found input:', input, 'classes:', input.className);
             if (!results.length) {
                var msg = document.createElement('div');
                msg.className = 'ecrt-qs-status';
                msg.textContent = 'No results found';
                dropdown.appendChild(msg);
                openDropdown();
                return;
            }


             // Log ALL events on the input to find the right one
             results.forEach(function (r, i) {
            ['change', 'input', 'blur', 'focus', 'select'].forEach(function (evt) {
                 var el = document.createElement('div');
                 input.addEventListener(evt, function (e) {
                el.className = 'ecrt-qs-item';
                     console.log('[QS] Native event:', evt, '| value:', JSON.stringify(input.value));
                el.textContent = r.title;
                el.addEventListener('mousedown', function (e) {
                     e.preventDefault();
                    navigate(r.title);
                });
                el.addEventListener('mouseenter', function () {
                    setActive(i);
                 });
                 });
                dropdown.appendChild(el);
                items.push(r.title);
             });
             });


             // jQuery events that PF/OOUI might use
             openDropdown();
            var $input = $(input);
        }
            $input.on('change input blur select autocompleteselect autocompleteclose pfautocomplete', function (e) {
                console.log('[QS] jQuery event:', e.type, '| value:', JSON.stringify(input.value));
            });


             // Look for OOUI widget instance
        function renderLoading() {
             var $wrapper = $(container).find('.pfFormInputWrapper');
             dropdown.innerHTML = '';
             console.log('[QS] pfFormInputWrapper:', $wrapper.length ? $wrapper[0] : 'not found');
             var msg = document.createElement('div');
             msg.className = 'ecrt-qs-status';
            msg.textContent = 'Searching\u2026';
            dropdown.appendChild(msg);
            openDropdown();
        }


            // Check if there's an OOUI widget attached
        // ── API fetch ───────────────────────────────────────────────
             var widget = OO && OO.ui ? OO.ui.infuse && $wrapper.find('.oo-ui-widget')[0] : null;
        function fetchSuggestions(substr) {
             console.log('[QS] OO.ui available:', typeof OO !== 'undefined' && !!OO.ui);
             renderLoading();
             var url = API_URL + '?action=pfautocomplete&format=json'
                + '&substr=' + encodeURIComponent(substr)
                + '&category=' + encodeURIComponent(cfg.category);


             // Use event delegation to catch clicks on autocomplete menu items
             fetch(url)
            // OOUI menus are often appended to <body>, not inside the container
                .then(function (r) { return r.json(); })
            $(document.body).on('click', '.oo-ui-menuOptionWidget, .oo-ui-optionWidget, .ui-menu-item, .pf-autocomplete-item, .autocomplete-suggestion', function (e) {
                .then(function (data) {
                var text = $(this).text().trim();
                    if (input.value.trim().toLowerCase() === substr.toLowerCase()) {
                console.log('[QS] Autocomplete item clicked! Text:', JSON.stringify(text));
                        renderItems((data && data.pfautocomplete) || []);
                 // After PF updates the input value:
                    }
                 setTimeout(function () {
                 })
                     console.log('[QS] Input value after click:', JSON.stringify(input.value));
                 .catch(function () {
                 }, 100);
                     dropdown.innerHTML = '';
            });
                    closeDropdown();
                 });
        }


            // Also watch for any new elements appearing (the dropdown)
        // ── Event handlers ──────────────────────────────────────────
             console.log('[QS] Searching for any autocomplete-related elements...');
        input.addEventListener('input', function () {
             setTimeout(function () {
             clearTimeout(timer);
                 // Log all elements with autocomplete-related classes
            var val = input.value.trim();
                var candidates = document.querySelectorAll('[class*="autocomplete"], [class*="menu"], [class*="lookup"], [class*="suggestion"]');
             if (val.length < MIN_CHARS) {
                 candidates.forEach(function (el) {
                 closeDropdown();
                    console.log('[QS] Candidate element:', el.tagName, el.className.substring(0, 100));
                 return;
                });
            }
            }, 3000);
            timer = setTimeout(function () {
                fetchSuggestions(val);
            }, DEBOUNCE_MS);
        });


            // Form submit still works
        input.addEventListener('keydown', function (e) {
            form.addEventListener('submit', function (e) {
            if (!dropdown.classList.contains('is-open') || !items.length) {
                e.preventDefault();
                 if (e.key === 'Enter') {
                e.stopPropagation();
                    e.preventDefault();
                 var value = input.value.trim();
                    navigate(input.value);
                console.log('[QS] Form submit, value:', JSON.stringify(value));
                if (value) {
                    window.location.href = mw.util.getUrl(value);
                 }
                 }
             });
                return;
             }
 
            switch (e.key) {
                case 'ArrowDown':
                    e.preventDefault();
                    setActive(activeIdx < items.length - 1 ? activeIdx + 1 : 0);
                    break;
                case 'ArrowUp':
                    e.preventDefault();
                    setActive(activeIdx > 0 ? activeIdx - 1 : items.length - 1);
                    break;
                case 'Enter':
                    e.preventDefault();
                    if (activeIdx >= 0 && activeIdx < items.length) {
                        navigate(items[activeIdx]);
                    } else {
                        navigate(input.value);
                    }
                    break;
                case 'Escape':
                    closeDropdown();
                    break;
            }
        });
 
        btn.addEventListener('click', function () {
            navigate(input.value);
        });
 
        document.addEventListener('click', function (e) {
            if (!container.contains(e.target)) {
                closeDropdown();
            }
        });
 
        input.addEventListener('focus', function () {
            if (items.length && input.value.trim().length >= MIN_CHARS) {
                openDropdown();
            }
        });
    }
 
    // ── Init all widgets ────────────────────────────────────────────
    function init() {
        injectStyles();


             console.log('[QS] Debug setup complete. Type something and click a suggestion.');
        // CRT Quick Search
            return true;
        initWidget({
         }
            containerId: 'crt-quick-search',
            controlsId:  'ecrt-qs-controls-crt',
            category:    'CRT models',
             placeholder: 'Type a CRT model name (e.g. Sony KV-27S42)'
         });


         if (!trySetup()) {
         // AV Device Quick Search
             var observer = new MutationObserver(function (mutations, obs) {
        initWidget({
                if (trySetup()) obs.disconnect();
             containerId: 'av-quick-search',
             });
             controlsId:  'ecrt-qs-controls-av',
             observer.observe(container, { childList: true, subtree: true });
             category:   'AV devices',
             setTimeout(function () { observer.disconnect(); }, 10000);
             placeholder: 'Type an AV device name (e.g. JVC SR-V10U)'
        }
        });
     }
     }


    // ── Run ─────────────────────────────────────────────────────────
     if (document.readyState === 'loading') {
     if (document.readyState === 'loading') {
         document.addEventListener('DOMContentLoaded', init);
         document.addEventListener('DOMContentLoaded', init);
Line 325: Line 661:




/**
* Footer Credit — "An Arclight Automata project"
* Appends inline credit text with link after the site name
* in the Minerva Neue footer (.minerva-footer-logo).
*/
(function () {
    'use strict';
    function addFooterCredit() {
        var logo = document.querySelector('.minerva-footer-logo');
        if (!logo || logo.querySelector('.ecrt-footer-credit')) return;
        var span = document.createElement('span');
        span.className = 'ecrt-footer-credit';
        span.innerHTML = ' \u00b7 An <a href="https://arclight.run" '
            + 'style="color:inherit;text-decoration:underline;text-decoration-color:rgba(128,128,128,0.4);'
            + 'text-underline-offset:2px;" '
            + 'target="_blank" rel="noopener">Arclight Automata</a> project';
        span.style.cssText = 'font-weight:normal;font-size:1em;opacity:0.7;';
        logo.appendChild(span);
    }
    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', addFooterCredit);
    } else {
        addFooterCredit();
    }
})();




MediaWiki Appliance - Powered by TurnKey Linux