253 lines
9.4 KiB
JavaScript
253 lines
9.4 KiB
JavaScript
|
|
/**
|
||
|
|
* pc-live | Window Manager
|
||
|
|
* Handles: create, focus, drag, resize, maximize, minimize, close, taskbar
|
||
|
|
*/
|
||
|
|
const WindowManager = (() => {
|
||
|
|
const layer = document.getElementById('window-layer');
|
||
|
|
const taskbarApps = document.getElementById('taskbar-apps');
|
||
|
|
let zCounter = 10;
|
||
|
|
const windows = new Map(); // id -> { el, tbBtn, minimized, maximized, preMax, title, icon }
|
||
|
|
|
||
|
|
/* ── Helpers ──────────────────────────────────────────── */
|
||
|
|
function nextZ() { return ++zCounter; }
|
||
|
|
|
||
|
|
function clampPos(el) {
|
||
|
|
const lw = layer.clientWidth;
|
||
|
|
const lh = layer.clientHeight;
|
||
|
|
let l = parseInt(el.style.left) || 0;
|
||
|
|
let t = parseInt(el.style.top) || 0;
|
||
|
|
const w = el.offsetWidth;
|
||
|
|
const h = el.offsetHeight;
|
||
|
|
el.style.left = Math.min(Math.max(0, l), lw - w) + 'px';
|
||
|
|
el.style.top = Math.min(Math.max(0, t), lh - Math.max(40, h - 300)) + 'px';
|
||
|
|
}
|
||
|
|
|
||
|
|
/* ── Drag ─────────────────────────────────────────────── */
|
||
|
|
function makeDraggable(el, handle) {
|
||
|
|
let startX, startY, startL, startT;
|
||
|
|
handle.addEventListener('mousedown', (e) => {
|
||
|
|
if (e.target.classList.contains('wc-btn')) return;
|
||
|
|
if (e.target.classList.contains('wc-resize')) return;
|
||
|
|
const win = windows.get(el.dataset.wid);
|
||
|
|
if (win && win.maximized) return;
|
||
|
|
startX = e.clientX; startY = e.clientY;
|
||
|
|
startL = parseInt(el.style.left) || 0;
|
||
|
|
startT = parseInt(el.style.top) || 0;
|
||
|
|
focus(el.dataset.wid);
|
||
|
|
const mm = (e2) => {
|
||
|
|
el.style.left = (startL + e2.clientX - startX) + 'px';
|
||
|
|
el.style.top = (startT + e2.clientY - startY) + 'px';
|
||
|
|
};
|
||
|
|
const mu = () => {
|
||
|
|
clampPos(el);
|
||
|
|
document.removeEventListener('mousemove', mm);
|
||
|
|
document.removeEventListener('mouseup', mu);
|
||
|
|
};
|
||
|
|
document.addEventListener('mousemove', mm);
|
||
|
|
document.addEventListener('mouseup', mu);
|
||
|
|
});
|
||
|
|
// Double-click titlebar → toggle maximize
|
||
|
|
handle.addEventListener('dblclick', (e) => {
|
||
|
|
if (e.target.classList.contains('wc-btn')) return;
|
||
|
|
toggleMaximize(el.dataset.wid);
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
/* ── Resize ───────────────────────────────────────────── */
|
||
|
|
function makeResizable(el, id) {
|
||
|
|
const dirs = ['n', 's', 'e', 'w', 'nw', 'ne', 'sw', 'se'];
|
||
|
|
dirs.forEach(dir => {
|
||
|
|
const h = document.createElement('div');
|
||
|
|
h.className = 'wc-resize wc-resize-' + dir;
|
||
|
|
el.appendChild(h);
|
||
|
|
|
||
|
|
let startX, startY, startL, startT, startW, startH;
|
||
|
|
const MIN_W = 320, MIN_H = 200;
|
||
|
|
|
||
|
|
h.addEventListener('mousedown', (e) => {
|
||
|
|
const win = windows.get(id);
|
||
|
|
if (!win || win.maximized) return;
|
||
|
|
e.stopPropagation();
|
||
|
|
focus(id);
|
||
|
|
startX = e.clientX; startY = e.clientY;
|
||
|
|
startL = parseInt(el.style.left) || 0;
|
||
|
|
startT = parseInt(el.style.top) || 0;
|
||
|
|
startW = el.offsetWidth;
|
||
|
|
startH = el.offsetHeight;
|
||
|
|
|
||
|
|
const mm = (e2) => {
|
||
|
|
const dx = e2.clientX - startX;
|
||
|
|
const dy = e2.clientY - startY;
|
||
|
|
let newL = startL, newT = startT, newW = startW, newH = startH;
|
||
|
|
|
||
|
|
if (dir.includes('e')) newW = Math.max(MIN_W, startW + dx);
|
||
|
|
if (dir.includes('w')) { newW = Math.max(MIN_W, startW - dx); newL = startL + startW - newW; }
|
||
|
|
if (dir.includes('s')) newH = Math.max(MIN_H, startH + dy);
|
||
|
|
if (dir.includes('n')) { newH = Math.max(MIN_H, startH - dy); newT = startT + startH - newH; }
|
||
|
|
|
||
|
|
el.style.left = newL + 'px';
|
||
|
|
el.style.top = newT + 'px';
|
||
|
|
el.style.width = newW + 'px';
|
||
|
|
el.style.height = newH + 'px';
|
||
|
|
};
|
||
|
|
const mu = () => {
|
||
|
|
document.removeEventListener('mousemove', mm);
|
||
|
|
document.removeEventListener('mouseup', mu);
|
||
|
|
};
|
||
|
|
document.addEventListener('mousemove', mm);
|
||
|
|
document.addEventListener('mouseup', mu);
|
||
|
|
});
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
/* ── Focus ────────────────────────────────────────────── */
|
||
|
|
function focus(id) {
|
||
|
|
const win = windows.get(id);
|
||
|
|
if (!win) return;
|
||
|
|
windows.forEach((w, wid) => {
|
||
|
|
w.el.classList.toggle('focused', wid === id);
|
||
|
|
w.tbBtn && w.tbBtn.classList.toggle('active', wid === id);
|
||
|
|
});
|
||
|
|
win.el.style.zIndex = nextZ();
|
||
|
|
}
|
||
|
|
|
||
|
|
/* ── Maximize / Restore ───────────────────────────────── */
|
||
|
|
function maximize(id) {
|
||
|
|
const win = windows.get(id);
|
||
|
|
if (!win || win.maximized) return;
|
||
|
|
win.preMax = {
|
||
|
|
left: win.el.style.left,
|
||
|
|
top: win.el.style.top,
|
||
|
|
width: win.el.style.width,
|
||
|
|
height: win.el.style.height,
|
||
|
|
};
|
||
|
|
win.maximized = true;
|
||
|
|
win.el.classList.add('maximized');
|
||
|
|
win.el.style.left = '0';
|
||
|
|
win.el.style.top = '0';
|
||
|
|
win.el.style.width = layer.clientWidth + 'px';
|
||
|
|
win.el.style.height = layer.clientHeight + 'px';
|
||
|
|
const btn = win.el.querySelector('.wc-maximize');
|
||
|
|
if (btn) btn.title = 'Restore';
|
||
|
|
focus(id);
|
||
|
|
}
|
||
|
|
|
||
|
|
function unmaximize(id) {
|
||
|
|
const win = windows.get(id);
|
||
|
|
if (!win || !win.maximized || !win.preMax) return;
|
||
|
|
win.maximized = false;
|
||
|
|
win.el.classList.remove('maximized');
|
||
|
|
win.el.style.left = win.preMax.left;
|
||
|
|
win.el.style.top = win.preMax.top;
|
||
|
|
win.el.style.width = win.preMax.width;
|
||
|
|
win.el.style.height = win.preMax.height;
|
||
|
|
win.preMax = null;
|
||
|
|
const btn = win.el.querySelector('.wc-maximize');
|
||
|
|
if (btn) btn.title = 'Maximize';
|
||
|
|
focus(id);
|
||
|
|
}
|
||
|
|
|
||
|
|
function toggleMaximize(id) {
|
||
|
|
const win = windows.get(id);
|
||
|
|
if (!win) return;
|
||
|
|
if (win.maximized) unmaximize(id); else maximize(id);
|
||
|
|
}
|
||
|
|
|
||
|
|
/* ── Create ───────────────────────────────────────────── */
|
||
|
|
function create({ id, title, icon = '🖥', width = 700, height = 480, content }) {
|
||
|
|
if (windows.has(id)) {
|
||
|
|
restore(id);
|
||
|
|
return windows.get(id).el;
|
||
|
|
}
|
||
|
|
|
||
|
|
const lw = layer.clientWidth;
|
||
|
|
const lh = layer.clientHeight;
|
||
|
|
const left = Math.max(20, Math.floor(lw / 2 - width / 2));
|
||
|
|
const top = Math.max(20, Math.floor(lh / 2 - height / 2));
|
||
|
|
|
||
|
|
const el = document.createElement('div');
|
||
|
|
el.className = 'window fade-in';
|
||
|
|
el.dataset.wid = id;
|
||
|
|
el.style.cssText = `left:${left}px;top:${top}px;width:${width}px;height:${height}px;z-index:${nextZ()}`;
|
||
|
|
|
||
|
|
el.innerHTML = `
|
||
|
|
<div class="window-titlebar">
|
||
|
|
<span class="window-title-icon">${icon}</span>
|
||
|
|
<span class="window-title-text">${title}</span>
|
||
|
|
<div class="window-controls">
|
||
|
|
<button class="wc-btn wc-minimize" title="Minimize">─</button>
|
||
|
|
<button class="wc-btn wc-maximize" title="Maximize">□</button>
|
||
|
|
<button class="wc-btn wc-close" title="Close">✕</button>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
<div class="window-body">${content || ''}</div>
|
||
|
|
`;
|
||
|
|
|
||
|
|
el.querySelector('.wc-minimize').addEventListener('click', () => minimize(id));
|
||
|
|
el.querySelector('.wc-maximize').addEventListener('click', () => toggleMaximize(id));
|
||
|
|
el.querySelector('.wc-close').addEventListener('click', () => close(id));
|
||
|
|
el.addEventListener('mousedown', () => focus(id));
|
||
|
|
|
||
|
|
makeDraggable(el, el.querySelector('.window-titlebar'));
|
||
|
|
makeResizable(el, id);
|
||
|
|
|
||
|
|
layer.appendChild(el);
|
||
|
|
|
||
|
|
const tbBtn = document.createElement('div');
|
||
|
|
tbBtn.className = 'taskbar-btn';
|
||
|
|
tbBtn.innerHTML = `<span>${icon}</span><span>${title}</span>`;
|
||
|
|
tbBtn.addEventListener('click', () => toggle(id));
|
||
|
|
taskbarApps.appendChild(tbBtn);
|
||
|
|
|
||
|
|
windows.set(id, { el, tbBtn, minimized: false, maximized: false, preMax: null, title, icon });
|
||
|
|
focus(id);
|
||
|
|
return el;
|
||
|
|
}
|
||
|
|
|
||
|
|
/* ── Minimize / restore / toggle ────────────────────────── */
|
||
|
|
function minimize(id) {
|
||
|
|
const win = windows.get(id);
|
||
|
|
if (!win) return;
|
||
|
|
win.minimized = true;
|
||
|
|
win.el.classList.add('minimized');
|
||
|
|
win.tbBtn && win.tbBtn.classList.remove('active');
|
||
|
|
win.tbBtn && win.tbBtn.classList.add('minimized');
|
||
|
|
}
|
||
|
|
|
||
|
|
function restore(id) {
|
||
|
|
const win = windows.get(id);
|
||
|
|
if (!win) return;
|
||
|
|
win.minimized = false;
|
||
|
|
win.el.classList.remove('minimized');
|
||
|
|
win.tbBtn && win.tbBtn.classList.remove('minimized');
|
||
|
|
focus(id);
|
||
|
|
}
|
||
|
|
|
||
|
|
function toggle(id) {
|
||
|
|
const win = windows.get(id);
|
||
|
|
if (!win) return;
|
||
|
|
if (win.minimized) restore(id); else minimize(id);
|
||
|
|
}
|
||
|
|
|
||
|
|
/* ── Close ───────────────────────────────────────────────── */
|
||
|
|
function close(id) {
|
||
|
|
const win = windows.get(id);
|
||
|
|
if (!win) return;
|
||
|
|
win.el.remove();
|
||
|
|
win.tbBtn && win.tbBtn.remove();
|
||
|
|
windows.delete(id);
|
||
|
|
}
|
||
|
|
|
||
|
|
function closeAll() {
|
||
|
|
windows.forEach((_, id) => close(id));
|
||
|
|
}
|
||
|
|
|
||
|
|
/* ── Get body element ────────────────────────────────────── */
|
||
|
|
function getBody(id) {
|
||
|
|
const win = windows.get(id);
|
||
|
|
return win ? win.el.querySelector('.window-body') : null;
|
||
|
|
}
|
||
|
|
|
||
|
|
return { create, close, closeAll, focus, minimize, restore, toggle, maximize, unmaximize, toggleMaximize, getBody };
|
||
|
|
})();
|