MediaWiki:Common.js: Difference between revisions
No edit summary |
No edit summary |
||
| Line 228: | Line 228: | ||
/** | /** | ||
* Quick Search — | * Quick Search Widget — Custom autocomplete for CRT model lookup | ||
* | * Replaces Page Forms #forminput with a fully custom implementation. | ||
* Fetches suggestions from the PF autocomplete API, navigates directly | |||
* to the wiki page on selection. | |||
* | |||
* | |||
*/ | */ | ||
(function () { | (function () { | ||
| Line 245: | Line 238: | ||
if (mw.config.get('wgPageName') !== 'Main_Page') return; | if (mw.config.get('wgPageName') !== 'Main_Page') return; | ||
function | // ── Config ────────────────────────────────────────────────────── | ||
var API_URL = mw.util.wikiScript('api'); | |||
if ( | var CATEGORY = 'CRT models'; | ||
var DEBOUNCE_MS = 250; | |||
var MIN_CHARS = 2; | |||
// ── Helpers ───────────────────────────────────────────────────── | |||
function getPageUrl(title) { | |||
return mw.util.getUrl(title); | |||
} | |||
function navigate(title) { | |||
title = (title || '').trim(); | |||
if (title) window.location.href = getPageUrl(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 = [ | |||
'#crt-quick-search {', | |||
' 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;', | |||
'}', | |||
'.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 */ | |||
'.skin-theme-clientpref-night #crt-quick-search {', | |||
' 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-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; }' | |||
].join('\n'); | |||
document.head.appendChild(style); | |||
} | |||
// ── Build the widget ──────────────────────────────────────────── | |||
function build(container) { | |||
injectStyles(); | |||
// Label | |||
var label = document.createElement('div'); | |||
label.className = 'ecrt-qs-label'; | |||
label.textContent = '\uD83D\uDD0D Quick Search'; | |||
// Wrapper (for positioning dropdown) | |||
var wrap = document.createElement('div'); | |||
wrap.className = 'ecrt-qs-wrap'; | |||
// Row: input + button | |||
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 = 'Type a CRT model name (e.g. Sony KV-27S42)'; | |||
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); | |||
// Dropdown | |||
var dropdown = document.createElement('div'); | |||
dropdown.className = 'ecrt-qs-dropdown'; | |||
wrap.appendChild(row); | |||
wrap.appendChild(dropdown); | |||
container.appendChild(label); | |||
container.appendChild(wrap); | |||
// ── State ─────────────────────────────────────────────────── | |||
var items = []; // current suggestion titles | |||
var activeIdx = -1; // keyboard-highlighted index (-1 = none) | |||
var timer = null; | |||
function openDropdown() { dropdown.classList.add('is-open'); } | |||
function closeDropdown() { | |||
dropdown.classList.remove('is-open'); | |||
activeIdx = -1; | |||
} | |||
function renderItems(results) { | |||
dropdown.innerHTML = ''; | |||
items = []; | |||
activeIdx = -1; | |||
if (!results.length) { | |||
var msg = document.createElement('div'); | |||
msg.className = 'ecrt-qs-status'; | |||
msg.textContent = 'No results found'; | |||
dropdown.appendChild(msg); | |||
openDropdown(); | |||
return; | |||
} | |||
results.forEach(function (r, i) { | |||
var el = document.createElement('div'); | |||
el.className = 'ecrt-qs-item'; | |||
el.textContent = r.title; | |||
el.setAttribute('data-index', i); | |||
el.addEventListener('mousedown', function (e) { | |||
// mousedown instead of click so it fires before blur | |||
e.preventDefault(); | |||
navigate(r.title); | |||
}); | |||
el.addEventListener('mouseenter', function () { | |||
setActive(i); | |||
}); | |||
dropdown.appendChild(el); | |||
items.push(r.title); | |||
}); | |||
openDropdown(); | |||
} | |||
function renderLoading() { | |||
dropdown.innerHTML = ''; | |||
var msg = document.createElement('div'); | |||
msg.className = 'ecrt-qs-status'; | |||
msg.textContent = 'Searching\u2026'; | |||
dropdown.appendChild(msg); | |||
openDropdown(); | |||
} | |||
function setActive(idx) { | |||
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'); | |||
// Scroll into view if needed | |||
els[idx].scrollIntoView({ block: 'nearest' }); | |||
} | |||
} | } | ||
// ── API fetch ─────────────────────────────────────────────── | |||
function fetchSuggestions(substr) { | |||
renderLoading(); | |||
var url = API_URL + '?action=pfautocomplete&format=json' | |||
+ '&substr=' + encodeURIComponent(substr) | |||
+ '&category=' + encodeURIComponent(CATEGORY); | |||
fetch(url) | |||
.then(function (r) { return r.json(); }) | |||
.then(function (data) { | |||
// Only render if input still matches (avoid stale results) | |||
if (input.value.trim().toLowerCase() === substr.toLowerCase()) { | |||
renderItems((data && data.pfautocomplete) || []); | |||
} | |||
}) | |||
.catch(function () { | |||
dropdown.innerHTML = ''; | |||
closeDropdown(); | |||
}); | |||
} | |||
// | // ── Event handlers ────────────────────────────────────────── | ||
input.addEventListener('input', function () { | input.addEventListener('input', function () { | ||
clearTimeout(timer); | |||
var val = input.value.trim(); | |||
if (val.length < MIN_CHARS) { | |||
closeDropdown(); | |||
return; | |||
} | |||
timer = setTimeout(function () { | |||
fetchSuggestions(val); | |||
}, DEBOUNCE_MS); | |||
}); | }); | ||
input.addEventListener('keydown', function (e) { | |||
if (!dropdown.classList.contains('is-open') || !items.length) { | |||
// Enter with no dropdown → navigate to whatever is typed | |||
if (e.key === 'Enter') { | |||
e.preventDefault(); | |||
navigate(input.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); | |||
e. | }); | ||
// Close dropdown when clicking outside | |||
document.addEventListener('click', function (e) { | |||
if (!wrap.contains(e.target)) { | |||
closeDropdown(); | |||
} | |||
}); | }); | ||
// Re-open on focus if there's already a query worth showing | |||
input.addEventListener('focus', function () { | |||
if (items.length && input.value.trim().length >= MIN_CHARS) { | |||
openDropdown(); | |||
} | |||
}); | |||
} | } | ||
// ── Init ──────────────────────────────────────────────────────── | |||
function init() { | function init() { | ||
var container = document.getElementById('crt-quick-search'); | var container = document.getElementById('crt-quick-search'); | ||
if (!container) return; | if (!container) return; | ||
build(container); | |||
} | } | ||