www-lordnet-sh/src/assets/js/wm.js

235 lines
7.4 KiB
JavaScript
Raw Normal View History

/*
Phosphor Terminal Window Manager (wm.js)
*/
const WM = (() => {
"use strict";
let topZ = 100;
const state = new Map(); // wid → { el, hidden, maximized, savedStyle }
let drag = null;
let resize = null;
/* ── Init ──────────────────────────────── */
function init() {
document.querySelectorAll(".window").forEach(register);
setupIcons();
document.addEventListener("mousemove", onMove);
document.addEventListener("mouseup", onUp);
// Focus topmost visible window on load
const visible = [...state.values()].filter(s => !s.hidden);
if (visible.length) focusEl(visible[visible.length - 1].el);
}
/* ── Build and inject window chrome ────── */
function buildChrome(win) {
const title = win.dataset.title || win.dataset.wid || "";
const bar = document.createElement("div");
bar.className = "window-titlebar";
const maxBtn = document.createElement("button");
maxBtn.className = "win-btn maximize";
maxBtn.title = "Full screen";
maxBtn.textContent = "⤢";
const titleSpan = document.createElement("span");
titleSpan.className = "window-title";
titleSpan.textContent = title;
bar.appendChild(maxBtn);
bar.appendChild(titleSpan);
win.insertBefore(bar, win.firstChild);
const footer = document.createElement("div");
footer.className = "window-footer";
const closeBtn = document.createElement("button");
closeBtn.className = "win-btn close";
closeBtn.title = "Close";
closeBtn.textContent = "×";
const spacer = document.createElement("div");
spacer.className = "win-footer-spacer";
const resizeHandle = document.createElement("div");
resizeHandle.className = "win-resize";
resizeHandle.title = "Drag to resize";
resizeHandle.textContent = "◢";
footer.appendChild(closeBtn);
footer.appendChild(spacer);
footer.appendChild(resizeHandle);
win.appendChild(footer);
}
/* ── Register a window element ─────────── */
function register(win) {
const id = win.dataset.wid;
if (!id || state.has(id)) return;
buildChrome(win);
const hidden = win.classList.contains("hidden");
state.set(id, { el: win, hidden, maximized: false, savedStyle: null });
// Draggable title bar
const bar = win.querySelector(".window-titlebar");
bar.addEventListener("mousedown", e => {
if (e.button !== 0 || e.target.closest(".win-btn")) return;
startDrag(e, win);
});
// Focus on any click inside window
win.addEventListener("mousedown", () => {
if (!state.get(id)?.hidden) focusEl(win);
});
// Window buttons
const q = s => win.querySelector(s);
const closeAction = win.dataset.close;
q(".win-btn.close") ?.addEventListener("click", () => closeAction === "back" ? history.back() : hide(id));
q(".win-btn.maximize")?.addEventListener("click", () => toggleMax(id));
// Resize handle
q(".win-resize")?.addEventListener("mousedown", e => {
if (e.button !== 0) return;
e.preventDefault(); e.stopPropagation();
const r = win.getBoundingClientRect();
resize = { el: win, x0: e.clientX, y0: e.clientY, w0: r.width, h0: r.height };
});
}
/* ── Focus ─────────────────────────────── */
function focusEl(win) {
document.querySelectorAll(".window.focused")
.forEach(w => w.classList.remove("focused"));
win.classList.add("focused");
win.style.zIndex = ++topZ;
}
function focus(id) {
const s = state.get(id);
if (s && !s.hidden) focusEl(s.el);
}
/* ── Show / Hide ───────────────────────── */
function show(id) {
const s = state.get(id);
if (!s) return;
s.el.classList.remove("hidden");
s.hidden = false;
focusEl(s.el);
}
function hide(id) {
const s = state.get(id);
if (!s) return;
s.el.classList.add("hidden");
s.hidden = true;
}
function toggle(id) {
const s = state.get(id);
if (!s) return;
if (s.hidden) {
show(id);
} else if (s.el.classList.contains("focused")) {
hide(id);
} else {
focusEl(s.el);
}
}
/* ── Maximize / Restore ────────────────── */
function toggleMax(id) {
const s = state.get(id);
if (!s) return;
const el = s.el;
const area = document.querySelector(".desktop-area");
if (!s.maximized) {
s.savedStyle = {
top: el.style.top, left: el.style.left,
width: el.style.width, height: el.style.height,
transform: el.style.transform,
};
el.style.transform = "none";
el.style.top = "0";
el.style.left = "0";
el.style.width = area.clientWidth + "px";
el.style.height = area.clientHeight + "px";
s.maximized = true;
} else {
const sv = s.savedStyle;
el.style.top = sv.top;
el.style.left = sv.left;
el.style.width = sv.width;
el.style.height = sv.height;
el.style.transform = sv.transform;
s.maximized = false;
}
focusEl(el);
}
/* ── Drag ──────────────────────────────── */
function startDrag(e, win) {
e.preventDefault();
focusEl(win);
// Freeze any CSS transform into explicit top/left so dragging is consistent
const wr = win.getBoundingClientRect();
const pr = win.offsetParent?.getBoundingClientRect() ?? { left: 0, top: 0 };
const left0 = wr.left - pr.left;
const top0 = wr.top - pr.top;
win.style.transform = "none";
win.style.left = left0 + "px";
win.style.top = top0 + "px";
drag = { el: win, x0: e.clientX, y0: e.clientY, left0, top0 };
document.body.style.cursor = "move";
}
/* ── Mouse events ──────────────────────── */
function onMove(e) {
if (drag) {
const dx = e.clientX - drag.x0;
const dy = e.clientY - drag.y0;
drag.el.style.left = (drag.left0 + dx) + "px";
drag.el.style.top = (drag.top0 + dy) + "px";
}
if (resize) {
const dx = e.clientX - resize.x0;
const dy = e.clientY - resize.y0;
resize.el.style.width = Math.max(280, resize.w0 + dx) + "px";
resize.el.style.height = Math.max(140, resize.h0 + dy) + "px";
}
}
function onUp() {
drag = null;
resize = null;
document.body.style.cursor = "";
}
/* ── Desktop icons ─────────────────────── */
function setupIcons() {
document.querySelectorAll(".desktop-icon[data-opens]").forEach(icon => {
icon.addEventListener("click", () => {
document.querySelectorAll(".desktop-icon.active")
.forEach(i => i.classList.remove("active"));
icon.classList.add("active");
show(icon.dataset.opens);
});
});
}
/* ── Boot: hide icons for hidden windows ─ */
document.addEventListener("DOMContentLoaded", init);
return { show, hide, toggle, focus };
})();