MediaWiki:Common.js
Note: After publishing, you may have to bypass your browser's cache to see the changes.
- Firefox / Safari: Hold Shift while clicking Reload, or press either Ctrl-F5 or Ctrl-R (⌘-R on a Mac)
- Google Chrome: Press Ctrl-Shift-R (⌘-Shift-R on a Mac)
- Edge: Hold Ctrl while clicking Refresh, or press Ctrl-F5.
/* Any JavaScript here will be loaded for all users on every page load. */
/**
* Night Mode Toggle for Minerva Neue
* Theme is applied early via LocalSettings.php hook to prevent FOUC
* This script just adds the toggle button
*/
(function () {
'use strict';
var STORAGE_KEY = 'minerva-night-mode';
// SVG icons (20x20)
var MOON_SVG = '<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"></path></svg>';
var SUN_SVG = '<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="5"></circle><line x1="12" y1="1" x2="12" y2="3"></line><line x1="12" y1="21" x2="12" y2="23"></line><line x1="4.22" y1="4.22" x2="5.64" y2="5.64"></line><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"></line><line x1="1" y1="12" x2="3" y2="12"></line><line x1="21" y1="12" x2="23" y2="12"></line><line x1="4.22" y1="19.78" x2="5.64" y2="18.36"></line><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"></line></svg>';
/**
* Detect OS color scheme preference
*/
function getOSPreference() {
if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {
return 'night';
}
return 'day';
}
/**
* Get saved theme, or detect from OS on first visit
*/
function getSavedTheme() {
try {
var saved = localStorage.getItem(STORAGE_KEY);
if (saved) {
return saved;
}
// First visit: detect OS preference and save it
var osPreference = getOSPreference();
localStorage.setItem(STORAGE_KEY, osPreference);
return osPreference;
} catch (e) {
return getOSPreference();
}
}
function saveTheme(theme) {
try {
localStorage.setItem(STORAGE_KEY, theme);
} catch (e) {}
}
function applyTheme(theme) {
var html = document.documentElement;
html.classList.remove('skin-theme-clientpref-day', 'skin-theme-clientpref-night');
html.classList.add('skin-theme-clientpref-' + theme);
}
function updateButtonState(button, theme) {
var icon = button.querySelector('.nightmode-icon');
if (theme === 'night') {
icon.innerHTML = SUN_SVG;
button.setAttribute('title', 'Switch to light mode');
} else {
icon.innerHTML = MOON_SVG;
button.setAttribute('title', 'Switch to dark mode');
}
}
function createToggleButton() {
var currentTheme = getSavedTheme();
var li = document.createElement('li');
var button = document.createElement('a');
button.setAttribute('role', 'button');
button.setAttribute('href', '#');
button.id = 'ca-nightmode';
button.className = 'cdx-button cdx-button--size-large cdx-button--fake-button cdx-button--fake-button--enabled cdx-button--icon-only cdx-button--weight-quiet';
button.style.cursor = 'pointer';
var icon = document.createElement('span');
icon.className = 'nightmode-icon';
icon.style.cssText = 'display:flex;align-items:center;justify-content:center;width:20px;height:20px;';
var label = document.createElement('span');
label.className = 'sr-only';
label.style.cssText = 'position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);border:0;';
label.textContent = currentTheme === 'night' ? 'Light mode' : 'Dark mode';
button.appendChild(icon);
button.appendChild(label);
li.appendChild(button);
updateButtonState(button, currentTheme);
button.addEventListener('click', function (e) {
e.preventDefault();
var newTheme = getSavedTheme() === 'night' ? 'day' : 'night';
saveTheme(newTheme);
applyTheme(newTheme);
updateButtonState(button, newTheme);
});
return li;
}
function insertButton() {
var notificationsList = document.querySelector('.minerva-notifications ul');
if (notificationsList) {
notificationsList.appendChild(createToggleButton());
} else {
var userNav = document.querySelector('.minerva-user-navigation');
if (userNav) {
var container = document.createElement('div');
container.className = 'minerva-notifications';
var ul = document.createElement('ul');
ul.appendChild(createToggleButton());
container.appendChild(ul);
userNav.insertBefore(container, userNav.firstChild);
}
}
}
// Insert button when DOM is ready
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', insertButton);
} else {
insertButton();
}
})();
/**
* Source Editor Warning Banner
* Shows a styled warning above the editor when users open the standard
* source editor on CRT model or AV device pages, directing them to the
* Page Forms editor instead.
*
*/
(function () {
'use strict';
// Only run on action=edit in the main namespace (ns 0)
if (mw.config.get('wgAction') !== 'edit' || mw.config.get('wgNamespaceNumber') !== 0) {
return;
}
mw.hook('wikipage.editform').add(function () {
// Don't add the banner twice
if (document.getElementById('ecrt-source-edit-warning')) return;
var textarea = document.getElementById('wpTextbox1');
if (!textarea) return;
var content = textarea.value;
var formName = null;
if (/\{\{CRT model/i.test(content)) {
formName = 'CRT model';
} else if (/\{\{AV device/i.test(content)) {
formName = 'AV device';
}
if (!formName) return;
var pageName = mw.config.get('wgPageName');
var formEditUrl = mw.util.getUrl('Special:FormEdit/' + formName + '/' + pageName);
var typeName = formName === 'CRT model' ? 'CRT model' : 'AV device';
var banner = document.createElement('div');
banner.id = 'ecrt-source-edit-warning';
banner.style.cssText = [
'background-color: #fef2f2',
'border: 1px solid #fca5a5',
'border-left: 4px solid #ef4444',
'border-radius: 8px',
'padding: 16px 20px',
'margin-bottom: 16px',
'font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
'line-height: 1.5'
].join(';');
banner.innerHTML =
'<div style="display:flex;align-items:flex-start;gap:12px;">' +
'<span style="color:#dc2626;font-size:20px;line-height:1;flex-shrink:0;">⚠️</span>' +
'<div>' +
'<div style="color:#991b1b;font-weight:600;font-size:15px;margin-bottom:4px;">' +
'You are using the source editor' +
'</div>' +
'<div style="color:#7f1d1d;font-size:13px;line-height:1.6;">' +
'This page is a <strong>' + typeName + '</strong>. ' +
'To avoid formatting issues and keep data consistent, please use the ' +
'<strong>structured form editor</strong> instead.' +
'<br>' +
'<a href="' + mw.html.escape(formEditUrl) + '" ' +
'style="display:inline-block;margin-top:10px;padding:8px 16px;' +
'background-color:#dc2626;color:#fff;font-weight:600;font-size:13px;' +
'border-radius:6px;text-decoration:none;">' +
'✎ Edit with form editor' +
'</a>' +
'<span style="display:inline-block;margin-top:10px;margin-left:12px;' +
'color:#991b1b;font-size:12px;font-style:italic;">' +
'Only use the source editor if you know what you\'re doing.' +
'</span>' +
'<div style="margin-top:10px;color:#991b1b;font-size:12px;">' +
'<strong>Tip:</strong> To go directly to the form editor next time, ' +
'click the <strong>"Edit this data"</strong> button on the page instead of "Edit".' +
'</div>' +
'</div>' +
'</div>' +
'</div>';
// Insert above the edit form
var editForm = document.getElementById('editform');
if (editForm) {
editForm.parentNode.insertBefore(banner, editForm);
}
});
})();
/**
* Quick Search Widgets — Reusable autocomplete for CRT & AV device lookup
* 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 () {
'use strict';
if (mw.config.get('wgPageName') !== 'Main_Page') return;
// ── Shared config ───────────────────────────────────────────────
var API_URL = mw.util.wikiScript('api');
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;
}
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');
els[idx].scrollIntoView({ block: 'nearest' });
}
}
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.addEventListener('mousedown', function (e) {
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();
}
// ── API fetch ───────────────────────────────────────────────
function fetchSuggestions(substr) {
renderLoading();
var url = API_URL + '?action=pfautocomplete&format=json'
+ '&substr=' + encodeURIComponent(substr)
+ '&category=' + encodeURIComponent(cfg.category);
fetch(url)
.then(function (r) { return r.json(); })
.then(function (data) {
if (input.value.trim().toLowerCase() === substr.toLowerCase()) {
renderItems((data && data.pfautocomplete) || []);
}
})
.catch(function () {
dropdown.innerHTML = '';
closeDropdown();
});
}
// ── Event handlers ──────────────────────────────────────────
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) {
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);
});
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();
// CRT Quick Search
initWidget({
containerId: 'crt-quick-search',
controlsId: 'ecrt-qs-controls-crt',
category: 'CRT models',
placeholder: 'Type a CRT model name (e.g. Sony KV-27S42)'
});
// AV Device Quick Search
initWidget({
containerId: 'av-quick-search',
controlsId: 'ecrt-qs-controls-av',
category: 'AV devices',
placeholder: 'Type an AV device name (e.g. JVC SR-V10U)'
});
}
// ── Run ─────────────────────────────────────────────────────────
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
})();
/**
* 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();
}
})();
// Load CRT Search module on the CRT search page
if (mw.config.get('wgPageName') === 'CRT_Search') {
mw.loader.load('/wiki/MediaWiki:CRT_Search.js?action=raw&ctype=text/javascript');
}
// Load AV Device Search module on the AV Device Search page
if (mw.config.get('wgPageName') === 'AV_Device_Search') {
mw.loader.load('/wiki/MediaWiki:AV_Device_Search.js?action=raw&ctype=text/javascript');
}