MediaWiki:Common.js: различия между версиями
Mei Day (обсуждение | вклад) Нет описания правки Метка: отменено |
Mei Day (обсуждение | вклад) Нет описания правки |
||
| (не показаны 3 промежуточные версии этого же участника) | |||
| Строка 19: | Строка 19: | ||
// Disable parallax on diff/comparison and old revision pages | // Disable parallax on diff/comparison and old revision pages | ||
var params = new URLSearchParams(window.location.search); | var params = new URLSearchParams(window.location.search); | ||
if (params.has("diff") || params.has("oldid") || params.get("action") === "historysubmit") { | if ( | ||
var hideEls = [parallaxBg, nebulaEl, starsSmallEl, starsBigEl, | params.has("diff") || | ||
params.has("oldid") || | |||
params.get("action") === "historysubmit" | |||
) { | |||
var hideEls = [ | |||
parallaxBg, | |||
nebulaEl, | |||
starsSmallEl, | |||
starsBigEl, | |||
document.getElementById("orbitalis-vignette"), | |||
]; | |||
for (var h = 0; h < hideEls.length; h++) { | for (var h = 0; h < hideEls.length; h++) { | ||
if (hideEls[h]) hideEls[h].style.display = "none"; | if (hideEls[h]) hideEls[h].style.display = "none"; | ||
| Строка 33: | Строка 42: | ||
return; | return; | ||
} | } | ||
// ---- Apply styles to the parallax elements ---------------------------- | // ---- Apply styles to the parallax elements ---------------------------- | ||
| Строка 198: | Строка 204: | ||
elOnline.textContent = count + " онлайн"; | elOnline.textContent = count + " онлайн"; | ||
elOnline.style.color = | elOnline.style.color = | ||
count > 0 | count > 0 ? "rgba(100,220,180,0.75)" : "rgba(100,220,180,0.35)"; | ||
elOnline.style.textShadow = | elOnline.style.textShadow = | ||
count > 0 | count > 0 ? "0 0 15px rgba(100,220,180,0.2)" : "none"; | ||
if (elMap && data.map_name) { | if (elMap && data.map_name) { | ||
| Строка 264: | Строка 266: | ||
/* Make entire card area clickable */ | /* Make entire card area clickable */ | ||
var cardIds = [ | var cardIds = [ | ||
"orb-card-1", "orb-card-2", "orb-card-3", "orb-card-4", | "orb-card-1", | ||
"orb-card-5", "orb-card-6", "orb-card-7", "orb-card-8" | "orb-card-2", | ||
"orb-card-3", | |||
"orb-card-4", | |||
"orb-card-5", | |||
"orb-card-6", | |||
"orb-card-7", | |||
"orb-card-8", | |||
]; | ]; | ||
for (var j = 0; j < cardIds.length; j++) { | for (var j = 0; j < cardIds.length; j++) { | ||
| Строка 275: | Строка 283: | ||
if (link) { | if (link) { | ||
c.addEventListener("click", function (ev) { | c.addEventListener("click", function (ev) { | ||
if (ev.target.tagName !== "A" && | if ( | ||
ev.target.tagName !== "A" && | |||
(!ev.target.parentNode || ev.target.parentNode.tagName !== "A") | |||
) { | |||
link.click(); | link.click(); | ||
} | } | ||
| Строка 341: | Строка 351: | ||
}); | }); | ||
/* ---- Orbitalis custom TOC ---- */ | |||
/* ---- | /* Activates when the page contains <div id="orb-toc"></div> (or class="orb-toc"). | ||
/* | Creates #orb-toc-sidebar directly in document.body so position:fixed | ||
sidebar | is never broken by skin containers with transform/will-change. */ | ||
(function () { | (function () { | ||
"use strict"; | "use strict"; | ||
function | function initOrbToc() { | ||
// | // Already built? | ||
if (document.getElementById("orb-toc-sidebar")) return; | if (document.getElementById("orb-toc-sidebar")) return; | ||
// | // Don't show in editor or other non-view actions | ||
if (document.body.classList.contains("action-edit") || | |||
document.body.classList.contains("action-submit") || | |||
document.body.classList.contains("action-history")) return; | |||
// Look for the opt-in marker | |||
var marker = | |||
document.getElementById("orb-toc") || document.querySelector(".orb-toc"); | |||
if (!marker) return; | |||
// Find content root | |||
var root = | var root = | ||
document.querySelector(".mw-parser-output") || | document.querySelector(".mw-parser-output") || | ||
| Строка 370: | Строка 379: | ||
if (!root) return; | if (!root) return; | ||
var headings = root.querySelectorAll("h2, h3, h4"); | // Scan ALL heading levels (h1 through h4) | ||
var headings = root.querySelectorAll("h1, h2, h3, h4"); | |||
if (!headings.length) return; | if (!headings.length) return; | ||
| Строка 376: | Строка 386: | ||
for (var i = 0; i < headings.length; i++) { | for (var i = 0; i < headings.length; i++) { | ||
var h = headings[i]; | var h = headings[i]; | ||
// Citizen skin | |||
// Citizen skin: heading text in .mw-headline span | |||
var headline = h.querySelector(".mw-headline"); | var headline = h.querySelector(".mw-headline"); | ||
var text = headline | var text = headline | ||
? headline.textContent | ? headline.textContent | ||
: h.textContent.replace(/\[edit[^\]]*\]/gi, "").trim(); | : h.textContent | ||
.replace(/\[\s*править[^\]]*\]/gi, "") | |||
.replace(/\[edit[^\]]*\]/gi, "") | |||
.trim(); | |||
if (!text) continue; | if (!text) continue; | ||
var id = headline ? headline.id : h.id; | var id = headline ? headline.id : h.id; | ||
if (!id) { | if (!id) { | ||
id = "orb-h-" + i; | |||
id = "orb- | |||
if (headline) headline.id = id; | if (headline) headline.id = id; | ||
else h.id = id; | else h.id = id; | ||
} | } | ||
var | var tag = h.tagName.toLowerCase(); | ||
items.push({ id: id, text: text, | items.push({ id: id, text: text, tag: tag, el: h }); | ||
} | } | ||
if (items.length < 2) return; | if (items.length < 2) return; | ||
// | // --- Create sidebar in document.body --- | ||
var sidebar = document.createElement("nav"); | var sidebar = document.createElement("nav"); | ||
sidebar.id = "orb-toc-sidebar"; | sidebar.id = "orb-toc-sidebar"; | ||
sidebar.setAttribute("aria-label", "Table of Contents"); | sidebar.setAttribute("aria-label", "Table of Contents"); | ||
var | var head = document.createElement("div"); | ||
head.className = "orb-toc-head"; | |||
head.textContent = | |||
sidebar.appendChild( | "\u0421\u043e\u0434\u0435\u0440\u0436\u0430\u043d\u0438\u0435"; | ||
sidebar.appendChild(head); | |||
var ul = document.createElement("ul"); | var ul = document.createElement("ul"); | ||
| Строка 411: | Строка 425: | ||
var item = items[j]; | var item = items[j]; | ||
var li = document.createElement("li"); | var li = document.createElement("li"); | ||
li.className = "orb-toc | li.className = "orb-toc-" + item.tag; | ||
li.setAttribute("data-target", item.id); | li.setAttribute("data-target", item.id); | ||
| Строка 417: | Строка 431: | ||
a.href = "#" + item.id; | a.href = "#" + item.id; | ||
a.textContent = item.text; | a.textContent = item.text; | ||
(function (targetId) { | |||
a.addEventListener("click", function (e) { | |||
var target = document.getElementById(targetId); | |||
if (target) { | |||
e.preventDefault(); | |||
target.scrollIntoView({ behavior: "smooth", block: "start" }); | |||
history.replaceState(null, "", "#" + targetId); | |||
} | |||
}); | |||
})(item.id); | |||
li.appendChild(a); | li.appendChild(a); | ||
ul.appendChild(li); | ul.appendChild(li); | ||
} | } | ||
sidebar.appendChild(ul); | sidebar.appendChild(ul); | ||
// Append to body - outside any Citizen container | |||
document.body.appendChild(sidebar); | document.body.appendChild(sidebar); | ||
// --- Scroll | // --- Scroll spy --- | ||
var | var allLi = ul.querySelectorAll("li"); | ||
var headingEls = []; | var headingEls = []; | ||
for (var k = 0; k < items.length; k++) { | for (var k = 0; k < items.length; k++) { | ||
| Строка 437: | Строка 464: | ||
requestAnimationFrame(function () { | requestAnimationFrame(function () { | ||
ticking = false; | ticking = false; | ||
var current = -1; | var current = -1; | ||
for (var s = 0; s < headingEls.length; s++) { | for (var s = 0; s < headingEls.length; s++) { | ||
if (headingEls[s].getBoundingClientRect().top <= | if (headingEls[s].getBoundingClientRect().top <= 120) { | ||
current = s; | current = s; | ||
} | } | ||
} | } | ||
for (var t = 0; t < | for (var t = 0; t < allLi.length; t++) { | ||
if (t === current) { | if (t === current) { | ||
allLi[t].classList.add("orb-toc-active"); | |||
} else { | } else { | ||
allLi[t].classList.remove("orb-toc-active"); | |||
} | } | ||
} | } | ||
| Строка 455: | Строка 481: | ||
window.addEventListener("scroll", onScroll, { passive: true }); | window.addEventListener("scroll", onScroll, { passive: true }); | ||
onScroll(); | onScroll(); | ||
} | } | ||
// Run on multiple hooks for skin compat | // Run on multiple hooks for skin compat | ||
if (typeof mw !== "undefined" && mw.hook) { | if (typeof mw !== "undefined" && mw.hook) { | ||
mw.hook("wikipage.content").add( | mw.hook("wikipage.content").add(initOrbToc); | ||
} | } | ||
document.addEventListener("DOMContentLoaded", | document.addEventListener("DOMContentLoaded", initOrbToc); | ||
window.addEventListener("load", function () { | window.addEventListener("load", function () { | ||
setTimeout( | setTimeout(initOrbToc, 150); | ||
}); | }); | ||
})(); | })(); | ||
Текущая версия от 16:17, 10 апреля 2026
/* MediaWiki:Common.js — Orbitalis Wiki
* This code runs on every wiki page.
* Paste this into MediaWiki:Common.js on the wiki (requires admin rights).
*
* Parallax space background that follows the mouse cursor,
* matching the in-game lobby style.
*/
(function () {
"use strict";
// Only activate on the main page (or everywhere — your choice)
var parallaxBg = document.getElementById("orbitalis-parallax-bg");
if (!parallaxBg) return; // no parallax container on this page
var nebulaEl = document.getElementById("parallax-nebula");
var starsSmallEl = document.getElementById("parallax-stars-small");
var starsBigEl = document.getElementById("parallax-stars-big");
// Disable parallax on diff/comparison and old revision pages
var params = new URLSearchParams(window.location.search);
if (
params.has("diff") ||
params.has("oldid") ||
params.get("action") === "historysubmit"
) {
var hideEls = [
parallaxBg,
nebulaEl,
starsSmallEl,
starsBigEl,
document.getElementById("orbitalis-vignette"),
];
for (var h = 0; h < hideEls.length; h++) {
if (hideEls[h]) hideEls[h].style.display = "none";
}
// Restore opaque background so diff/old revision is readable
document.body.style.background = "#060608";
var contentEl = document.getElementById("content");
if (contentEl) {
contentEl.style.background = "#14141a";
}
return;
}
// ---- Apply styles to the parallax elements ----------------------------
// We do it from JS because MediaWiki strips complex inline styles
// Background container
parallaxBg.style.cssText =
"position:fixed;top:0;left:0;width:100%;height:100%;z-index:-10;pointer-events:none;overflow:hidden;background:#060608;";
// Shared layer base
var layerBase =
"position:fixed;top:-50px;left:-50px;width:calc(100% + 100px);height:calc(100% + 100px);pointer-events:none;";
// Nebula
if (nebulaEl) {
nebulaEl.style.cssText =
layerBase +
"background:" +
"radial-gradient(ellipse at 70% 40%, rgba(30,50,80,0.4) 0%, transparent 50%)," +
"radial-gradient(ellipse at 85% 60%, rgba(50,30,60,0.3) 0%, transparent 45%)," +
"radial-gradient(ellipse at 60% 70%, rgba(20,40,70,0.35) 0%, transparent 55%);" +
"opacity:0.7;z-index:-9;";
}
// Small stars
if (starsSmallEl) {
starsSmallEl.style.cssText =
layerBase +
"background-image:" +
"radial-gradient(1px 1px at 10% 20%, rgba(255,255,255,0.8), transparent)," +
"radial-gradient(1px 1px at 25% 35%, rgba(255,255,255,0.6), transparent)," +
"radial-gradient(1px 1px at 40% 10%, rgba(255,255,255,0.7), transparent)," +
"radial-gradient(1px 1px at 55% 45%, rgba(255,255,255,0.5), transparent)," +
"radial-gradient(1px 1px at 70% 25%, rgba(255,255,255,0.8), transparent)," +
"radial-gradient(1px 1px at 85% 55%, rgba(255,255,255,0.6), transparent)," +
"radial-gradient(1px 1px at 15% 60%, rgba(255,255,255,0.7), transparent)," +
"radial-gradient(1px 1px at 30% 75%, rgba(255,255,255,0.5), transparent)," +
"radial-gradient(1px 1px at 45% 85%, rgba(255,255,255,0.8), transparent)," +
"radial-gradient(1px 1px at 60% 70%, rgba(255,255,255,0.6), transparent)," +
"radial-gradient(1px 1px at 75% 90%, rgba(255,255,255,0.7), transparent)," +
"radial-gradient(1px 1px at 90% 15%, rgba(255,255,255,0.5), transparent)," +
"radial-gradient(1px 1px at 5% 40%, rgba(255,255,255,0.6), transparent)," +
"radial-gradient(1px 1px at 20% 95%, rgba(255,255,255,0.7), transparent)," +
"radial-gradient(1px 1px at 35% 50%, rgba(255,255,255,0.5), transparent)," +
"radial-gradient(1px 1px at 50% 30%, rgba(255,255,255,0.8), transparent)," +
"radial-gradient(1px 1px at 65% 5%, rgba(255,255,255,0.6), transparent)," +
"radial-gradient(1px 1px at 80% 65%, rgba(255,255,255,0.7), transparent)," +
"radial-gradient(1px 1px at 95% 80%, rgba(255,255,255,0.5), transparent)," +
"radial-gradient(1px 1px at 12% 88%, rgba(255,255,255,0.6), transparent);" +
"background-size:200px 200px;opacity:0.8;z-index:-8;";
}
// Big stars
if (starsBigEl) {
starsBigEl.style.cssText =
layerBase +
"background-image:" +
"radial-gradient(2px 2px at 8% 15%, rgba(200,220,255,0.9), transparent)," +
"radial-gradient(2px 2px at 22% 42%, rgba(255,240,220,0.8), transparent)," +
"radial-gradient(2px 2px at 38% 8%, rgba(220,255,255,0.7), transparent)," +
"radial-gradient(2px 2px at 52% 68%, rgba(255,255,220,0.8), transparent)," +
"radial-gradient(2px 2px at 68% 32%, rgba(200,200,255,0.9), transparent)," +
"radial-gradient(2px 2px at 82% 78%, rgba(255,220,200,0.7), transparent)," +
"radial-gradient(2px 2px at 18% 55%, rgba(220,255,220,0.8), transparent)," +
"radial-gradient(2px 2px at 48% 92%, rgba(255,200,255,0.7), transparent)," +
"radial-gradient(2px 2px at 72% 18%, rgba(200,255,255,0.8), transparent)," +
"radial-gradient(2px 2px at 92% 45%, rgba(255,255,200,0.7), transparent);" +
"background-size:300px 300px;opacity:0.6;z-index:-7;";
}
// Vignette overlay
var vignetteEl = document.getElementById("orbitalis-vignette");
if (vignetteEl) {
vignetteEl.style.cssText =
"position:fixed;top:0;left:0;width:100%;height:100%;z-index:-6;pointer-events:none;" +
"background:radial-gradient(ellipse at center, rgba(6,6,8,0) 30%, rgba(6,6,8,0.6) 70%, #060608 100%);";
}
// ---- Override MediaWiki default backgrounds ----------------------------
document.body.style.background = "transparent";
var ids = ["mw-page-base", "mw-head-base"];
for (var i = 0; i < ids.length; i++) {
var el = document.getElementById(ids[i]);
if (el) el.style.background = "none";
}
var content = document.getElementById("content");
if (content) {
content.style.background = "none";
content.style.border = "none";
}
var bodyContent = document.getElementById("bodyContent");
if (bodyContent) {
bodyContent.style.position = "relative";
}
// ---- Mouse-tracking parallax animation --------------------------------
var targetX = 0,
targetY = 0;
var currentX = 0,
currentY = 0;
document.addEventListener("mousemove", function (e) {
var w = window.innerWidth || document.documentElement.clientWidth;
var h = window.innerHeight || document.documentElement.clientHeight;
targetX = (e.clientX / w - 0.5) * 2;
targetY = (e.clientY / h - 0.5) * 2;
});
function animate() {
currentX += (targetX - currentX) * 0.06;
currentY += (targetY - currentY) * 0.06;
if (nebulaEl) {
nebulaEl.style.left = currentX * 12 - 50 + "px";
nebulaEl.style.top = currentY * 12 - 50 + "px";
}
if (starsSmallEl) {
starsSmallEl.style.left = currentX * 25 - 50 + "px";
starsSmallEl.style.top = currentY * 25 - 50 + "px";
}
if (starsBigEl) {
starsBigEl.style.left = currentX * 40 - 50 + "px";
starsBigEl.style.top = currentY * 40 - 50 + "px";
}
requestAnimationFrame(animate);
}
animate();
})();
/* ---- Server online status + info ---- */
/* Queries /api/status.php on the same wiki host — which sends a UDP BYOND
* topic to localhost:41060 and returns JSON { players, map_name, chaos_level, ... }.
* No proxy needed: wiki and game server share the same machine.
*/
(function () {
"use strict";
var elOnline = document.getElementById("orb-online");
var elMap = document.getElementById("orb-map-val");
var elST = document.getElementById("orb-st-val");
if (!elOnline) return;
var STATUS_URL = "/api/status.json";
var REFRESH_MS = 30000;
function update() {
fetch(STATUS_URL)
.then(function (r) {
console.log("[Orbitalis Status] HTTP", r.status, r.url);
if (!r.ok) throw new Error("HTTP " + r.status);
return r.json();
})
.then(function (data) {
console.log("[Orbitalis Status] Data:", data);
var count = parseInt(data.players, 10);
if (isNaN(count)) throw new Error("bad data: players=" + data.players);
elOnline.textContent = count + " онлайн";
elOnline.style.color =
count > 0 ? "rgba(100,220,180,0.75)" : "rgba(100,220,180,0.35)";
elOnline.style.textShadow =
count > 0 ? "0 0 15px rgba(100,220,180,0.2)" : "none";
if (elMap && data.map_name) {
elMap.textContent = data.map_name;
elMap.style.color = "rgba(255,255,255,0.55)";
}
if (elST && data.chaos_level) {
elST.textContent = data.chaos_level;
elST.style.color = "rgba(255,255,255,0.55)";
}
})
.catch(function (err) {
console.error("[Orbitalis Status] Fetch failed:", err);
elOnline.textContent = "оффлайн";
elOnline.style.color = "rgba(255,120,120,0.5)";
elOnline.style.textShadow = "none";
if (elMap) elMap.textContent = "—";
if (elST) elST.textContent = "—";
});
}
update();
setInterval(update, REFRESH_MS);
})();
/* ---- Main page: interactive engine ---- */
/* All visual styling is in Common.css (CSS-first approach).
JS handles: nuclear strip for <a> tags, click handlers.
Safe to re-run on Citizen DOM updates. */
(function () {
"use strict";
function run() {
var orb = document.getElementById("orb-content");
if (!orb) return;
/* Nuclear strip: clear Citizen-applied backgrounds on <a> tags only */
var links = orb.querySelectorAll("a");
for (var i = 0; i < links.length; i++) {
var a = links[i];
a.style.setProperty("background", "none", "important");
a.style.setProperty("background-color", "transparent", "important");
a.style.setProperty("background-image", "none", "important");
a.style.setProperty("box-shadow", "none", "important");
a.style.setProperty("border", "none", "important");
a.style.setProperty("padding", "0", "important");
a.style.setProperty("text-decoration", "none", "important");
}
/* Connect button — byond:// link */
var addr = document.getElementById("orb-addr");
if (addr && !addr._orbClick) {
addr._orbClick = true;
addr.addEventListener("click", function () {
window.location.href = "byond://45.141.208.222:41060";
});
}
/* Make entire card area clickable */
var cardIds = [
"orb-card-1",
"orb-card-2",
"orb-card-3",
"orb-card-4",
"orb-card-5",
"orb-card-6",
"orb-card-7",
"orb-card-8",
];
for (var j = 0; j < cardIds.length; j++) {
var card = document.getElementById(cardIds[j]);
if (card && !card._orbClick) {
card._orbClick = true;
(function (c) {
var link = c.querySelector("a");
if (link) {
c.addEventListener("click", function (ev) {
if (
ev.target.tagName !== "A" &&
(!ev.target.parentNode || ev.target.parentNode.tagName !== "A")
) {
link.click();
}
});
}
})(card);
}
}
}
/* Hook into multiple events — retry for Citizen's late DOM updates */
if (typeof mw !== "undefined" && mw.hook) {
mw.hook("wikipage.content").add(run);
}
document.addEventListener("DOMContentLoaded", run);
window.addEventListener("load", function () {
setTimeout(run, 50);
setTimeout(run, 500);
});
})();
// Переключение вкладок и классов "chaos-*".
mw.hook("wikipage.content").add(function ($root) {
$root.find(".hj-chaos-container").each(function () {
var $container = $(this);
var $buttons = $container.find(".hj-chaos-tab-button");
var $blocks = $container.find(".hj-chaos-block");
if (!$buttons.length || !$blocks.length) return;
var CHAOS_CLASSES = "chaos-overview chaos-calm chaos-medium chaos-high";
function activate(key, $btn) {
// активная кнопка
$buttons.removeClass("active");
$btn.addClass("active");
// активный блок
$blocks.removeClass("active").hide();
$blocks
.filter('[data-chaos="' + key + '"]')
.addClass("active")
.show();
// класс на контейнере для раскраски рамок/фона
$container.removeClass(CHAOS_CLASSES).addClass("chaos-" + key);
}
// Инициализация
var $activeBtn = $buttons.filter(".active").first();
if ($activeBtn.length) {
activate($activeBtn.data("chaos"), $activeBtn);
} else {
var $first = $buttons.first();
activate($first.data("chaos"), $first);
}
// Клики
$buttons.off("click.hjChaos").on("click.hjChaos", function () {
var $btn = $(this);
activate($btn.data("chaos"), $btn);
});
});
});
/* ---- Orbitalis custom TOC ---- */
/* Activates when the page contains <div id="orb-toc"></div> (or class="orb-toc").
Creates #orb-toc-sidebar directly in document.body so position:fixed
is never broken by skin containers with transform/will-change. */
(function () {
"use strict";
function initOrbToc() {
// Already built?
if (document.getElementById("orb-toc-sidebar")) return;
// Don't show in editor or other non-view actions
if (document.body.classList.contains("action-edit") ||
document.body.classList.contains("action-submit") ||
document.body.classList.contains("action-history")) return;
// Look for the opt-in marker
var marker =
document.getElementById("orb-toc") || document.querySelector(".orb-toc");
if (!marker) return;
// Find content root
var root =
document.querySelector(".mw-parser-output") ||
document.getElementById("mw-content-text") ||
document.getElementById("bodyContent");
if (!root) return;
// Scan ALL heading levels (h1 through h4)
var headings = root.querySelectorAll("h1, h2, h3, h4");
if (!headings.length) return;
var items = [];
for (var i = 0; i < headings.length; i++) {
var h = headings[i];
// Citizen skin: heading text in .mw-headline span
var headline = h.querySelector(".mw-headline");
var text = headline
? headline.textContent
: h.textContent
.replace(/\[\s*править[^\]]*\]/gi, "")
.replace(/\[edit[^\]]*\]/gi, "")
.trim();
if (!text) continue;
var id = headline ? headline.id : h.id;
if (!id) {
id = "orb-h-" + i;
if (headline) headline.id = id;
else h.id = id;
}
var tag = h.tagName.toLowerCase();
items.push({ id: id, text: text, tag: tag, el: h });
}
if (items.length < 2) return;
// --- Create sidebar in document.body ---
var sidebar = document.createElement("nav");
sidebar.id = "orb-toc-sidebar";
sidebar.setAttribute("aria-label", "Table of Contents");
var head = document.createElement("div");
head.className = "orb-toc-head";
head.textContent =
"\u0421\u043e\u0434\u0435\u0440\u0436\u0430\u043d\u0438\u0435";
sidebar.appendChild(head);
var ul = document.createElement("ul");
for (var j = 0; j < items.length; j++) {
var item = items[j];
var li = document.createElement("li");
li.className = "orb-toc-" + item.tag;
li.setAttribute("data-target", item.id);
var a = document.createElement("a");
a.href = "#" + item.id;
a.textContent = item.text;
(function (targetId) {
a.addEventListener("click", function (e) {
var target = document.getElementById(targetId);
if (target) {
e.preventDefault();
target.scrollIntoView({ behavior: "smooth", block: "start" });
history.replaceState(null, "", "#" + targetId);
}
});
})(item.id);
li.appendChild(a);
ul.appendChild(li);
}
sidebar.appendChild(ul);
// Append to body - outside any Citizen container
document.body.appendChild(sidebar);
// --- Scroll spy ---
var allLi = ul.querySelectorAll("li");
var headingEls = [];
for (var k = 0; k < items.length; k++) {
headingEls.push(items[k].el);
}
var ticking = false;
function onScroll() {
if (ticking) return;
ticking = true;
requestAnimationFrame(function () {
ticking = false;
var current = -1;
for (var s = 0; s < headingEls.length; s++) {
if (headingEls[s].getBoundingClientRect().top <= 120) {
current = s;
}
}
for (var t = 0; t < allLi.length; t++) {
if (t === current) {
allLi[t].classList.add("orb-toc-active");
} else {
allLi[t].classList.remove("orb-toc-active");
}
}
});
}
window.addEventListener("scroll", onScroll, { passive: true });
onScroll();
}
// Run on multiple hooks for skin compat
if (typeof mw !== "undefined" && mw.hook) {
mw.hook("wikipage.content").add(initOrbToc);
}
document.addEventListener("DOMContentLoaded", initOrbToc);
window.addEventListener("load", function () {
setTimeout(initOrbToc, 150);
});
})();