function isTouchDevice() { return ( "ontouchstart" in window || navigator.maxTouchPoints > 0 || navigator.msMaxTouchPoints > 0 ); } class WindowManager { managerObject; children; extensions; minZIndex = 50; maxZIndex = 100; screen_overscan = { l: "0px", r: "0px", t: "0px", b: "0px", }; constructor(manager) { this.managerObject = manager; this.managerObject.classList.add("window-master"); this.managerObject.style.zIndex = this.minZIndex; this.children = []; this.extensions = []; window.addEventListener("beforeunload", () => this.destroy()); this.managerObject.addEventListener("windowcreate", (e) => this.#event_manageMinimized(e) ); this.managerObject.addEventListener("windowminimize", (e) => this.#event_manageMinimized(e) ); this.managerObject.addEventListener("windowdestroy", (e) => this.#event_manageMinimized(e) ); } set overscan_left(os) { this.screen_overscan.l = os; this.#event_manageMinimized(); } set overscan_right(os) { this.screen_overscan.r = os; this.#event_manageMinimized(); } set overscan_bottom(os) { this.screen_overscan.b = os; this.#event_manageMinimized(); } set overscan_top(os) { this.screen_overscan.t = os; this.#event_manageMinimized(); } get overscan() { return this.screen_overscan; } createWindow(content, srcdoc, w, h) { return new WindowObject(this, content, srcdoc, w, h); } #event_manageMinimized(e) { this.children .filter((x) => x.minimized) .forEach((curWinObj, i) => { curWinObj.windowObject.style.bottom = `calc(${this.screen_overscan.b} + (2em * ${i}))`; }); } raiseIndex(windowObj, add, focus = true) { const sortedFocusArray = this.children.toSorted( (x, y) => x.focusOrder - y.focusOrder ); this.raiseWindow( sortedFocusArray.at( (sortedFocusArray.indexOf(windowObj) + add) % this.children.length ), focus ); } addWindow(windowObj) { this.children.push(windowObj); this.managerObject.appendChild(windowObj.windowObject); } addExtension(ext, obj) { this.extensions.push(ext); this.managerObject.appendChild(ext[obj]); } removeWindow(windowId) { this.children.splice(windowId, 1); if (this.children.length > 0) this.raiseWindow( this.children.toSorted((x, y) => x.focusOrder - y.focusOrder)[0] ); } removeExtension(extId) { this.extensions.splice(extId, 1); } destroy() { this.children.forEach((x) => { console.log(x); x.destroy(); }); this.extensions.forEach((x) => { console.log(x); x.destroy(); }); this.managerObject.remove(); this.children = []; this.extensions = []; } dispatchEvent(eventName, data) { const event = new CustomEvent(eventName, data); this.managerObject.dispatchEvent(event); } raiseWindow(windowObj, focus = true) { this.children .toSorted((x, y) => x.focusOrder - y.focusOrder) .forEach((curWinObj, i) => { curWinObj.focusOrder = i + 1; curWinObj.windowObject.style.zIndex = this.maxZIndex - (i + 1); curWinObj.windowObject.classList.add("unfocused"); }); windowObj.focusOrder = 0; windowObj.windowObject.style.zIndex = windowObj.parentManager.maxZIndex; windowObj.windowObject.classList.remove("unfocused"); if (focus) windowObj.windowObject.focus(); windowObj.dispatchEvent("windowfocus", { detail: windowObj }); } } class WindowObject { parentManager; windowObject; windowManager; windowManagerLabel; windowContent; focusOrder; #orig_mousePosX; #orig_mousePosY; #orig_selfPosX; #orig_selfPosY; #isDragging; #isFrozen; #isRemoved = false; constructor(manager, content, srcdoc, w, h) { this.parentManager = manager; this.windowObject = WindowObject.createWindow(this, content, srcdoc, w, h); this.parentManager.addWindow(this); this.dispatchEvent("windowcreate", { detail: this }); this.parentManager.raiseWindow(this); if (isTouchDevice()) this.maximizeWindow(); this.windowManager = this.windowObject.querySelector(".window-manager"); this.windowManagerLabel = this.windowObject.querySelector( ".window-manager-label" ); this.windowContent = this.windowObject.querySelector(".window-content"); this.windowManager.addEventListener("mousedown", (e) => { this.event__dragMouseDown(e); this.parentManager.raiseWindow(this); }); this.windowObject.addEventListener("mousemove", (e) => this.event__dragMouseMove(e) ); this.windowObject.addEventListener("mouseout", (e) => this.event__dragMouseMove(e) ); this.windowObject.addEventListener("mouseup", (e) => this.event__dragMouseUp(e) ); this.windowManager.addEventListener("touchstart", (e) => { this.event__dragMouseDown(e, true); this.parentManager.raiseWindow(this); }); this.parentManager.managerObject.addEventListener("touchmove", (e) => this.event__dragMouseMove(e, true) ); this.parentManager.managerObject.addEventListener("touchend", (e) => this.event__dragMouseUp(e, true) ); this.windowObject.addEventListener("keydown", (e) => this.event__responseKeyDown(e) ); this.windowObject.addEventListener("focus", (e) => this.parentManager.raiseWindow(this) ); this.windowContent.contentWindow.addEventListener("focus", (e) => this.parentManager.raiseWindow(this, false) ); this.windowContent.addEventListener("load", () => { try { this.title = this.windowContent.contentWindow.document.title; } catch (err) {} }); } dispatchEvent(eventName, data) { const event = new CustomEvent(eventName, data); this.parentManager.managerObject.dispatchEvent(event); } get windowId() { return this.parentManager.children.indexOf(this); } get title() { return this.windowManagerLabel.textContent; } get content() { return this.windowContent.srcdoc; } get contentUrl() { return this.windowContent.src; } get x() { return this.windowObject.offsetLeft; } get y() { return this.windowObject.offsetTop; } get width() { return this.windowObject.offsetWidth; } get height() { return this.windowObject.offsetHeight; } set title(content) { this.windowManagerLabel.textContent = content; this.dispatchEvent("windowmanager_title", { detail: this }); } set content(src) { this.windowContent.srcdoc = src; this.dispatchEvent("windowmanager_content", { detail: this }); } set contentUrl(url) { this.windowContent.src = url; this.dispatchEvent("windowmanager_content", { detail: this }); } set x(x) { this.windowObject.style.left = x + "px"; this.dispatchEvent("windowmove", { detail: this, mouse: e }); } set y(y) { this.windowObject.style.top = y = "px"; this.dispatchEvent("windowmove", { detail: this, mouse: e }); } set width(w) { this.windowObject.style.width = w + "px"; } set height(h) { this.windowObject.style.height = h = "px"; } // raiseWindow() { // this.parentManager.raiseWindow(this); // } freezeWindow() { this.#isFrozen = !this.#isFrozen; this.dispatchEvent("windowfreeze", { detail: this }); } set frozen(state) { if (state != this.frozen) this.dispatchEvent("windowfreeze", { detail: this }); this.#isFrozen = state; } get frozen() { return this.#isFrozen; } petrifyWindow() { if (this.windowObject.classList.contains("petrified")) { this.windowObject.classList.remove("petrified"); this.windowObject.style.minWidth = null; this.windowObject.style.maxWidth = null; this.windowObject.style.minHeight = null; this.windowObject.style.maxHeight = null; } else { this.windowObject.classList.add("petrified"); this.windowObject.style.minWidth = this.w + "px"; this.windowObject.style.maxWidth = this.w + "px"; this.windowObject.style.minHeight = this.h + "px"; this.windowObject.style.maxHeight = this.h + "px"; } this.dispatchEvent("windowpetrify", { detail: this }); } set petrified(state) { if (state != this.petrified) this.dispatchEvent("windowpetrify", { detail: this }); if (state) { this.windowObject.classList.add("petrified"); this.windowObject.style.minWidth = this.w + "px"; this.windowObject.style.maxWidth = this.w + "px"; this.windowObject.style.minHeight = this.h + "px"; this.windowObject.style.maxHeight = this.h + "px"; } else { this.windowObject.classList.remove("petrified"); this.windowObject.style.minWidth = null; this.windowObject.style.maxWidth = null; this.windowObject.style.minHeight = null; this.windowObject.style.maxHeight = null; } } get petrified() { return this.windowObject.classList.contains("petrified"); } minimizeWindow() { if (this.windowObject.classList.contains("minimized")) { this.windowObject.classList.remove("minimized"); this.frozen = false; this.petrified = false; this.windowObject.style.setProperty("--z-index", null); this.parentManager.raiseWindow(this); } else { this.windowObject.classList.add("minimized"); this.maximized = false; this.frozen = true; this.petrified = true; this.windowObject.style.setProperty( "--z-index", this.parentManager.minZIndex ); } this.dispatchEvent("windowminimize", { detail: this }); } set minimized(state) { if (state != this.minimized) this.dispatchEvent("windowminimize", { detail: this }); if (state) { this.windowObject.classList.add("minimized"); this.maximized = false; this.frozen = true; this.petrified = true; this.windowObject.style.setProperty( "--z-index", this.parentManager.minZIndex ); } else { this.windowObject.classList.remove("minimized"); this.frozen = false; this.petrified = false; this.windowObject.style.setProperty("--z-index", null); this.parentManager.raiseWindow(this); } } get minimized() { return this.windowObject.classList.contains("minimized"); } maximizeWindow() { if (this.windowObject.classList.contains("maximized")) { this.windowObject.classList.remove("maximized"); } else { this.windowObject.classList.add("maximized"); this.minimized = false; } this.parentManager.raiseWindow(this); this.dispatchEvent("windowmaximize", { detail: this }); } set maximized(state) { if (state != this.maximized) this.dispatchEvent("windowmaximize", { detail: this }); if (state) { this.windowObject.classList.add("maximized"); this.minimized = false; } else { this.windowObject.classList.remove("maximized"); } this.parentManager.raiseWindow(this); } get maximized() { return this.windowObject.classList.contains("maximized"); } destroy() { try { if (this.windowContent.contentWindow != null) { const unloadEvent = new Event("beforeunload"); this.windowContent.contentWindow.dispatchEvent(unloadEvent); } } catch (err) {} this.windowObject.remove(); this.dispatchEvent("windowdestroy", { detail: this }); this.parentManager.removeWindow(this.windowId); } event__dragMouseDown(e, touch) { if (this.#isFrozen) return; this.#orig_mousePosX = touch ? e.touches[0].clientX : e.clientX; this.#orig_mousePosY = touch ? e.touches[0].clientY : e.clientY; this.#orig_selfPosX = this.windowObject.offsetLeft; this.#orig_selfPosY = this.windowObject.offsetTop; this.windowContent.style.userSelect = "none"; this.windowContent.style.pointerEvents = "none"; this.windowManager.style.cursor = "move"; this.#isDragging = true; if (this.windowObject.style.zIndex != this.parentManager.maxZIndex) this.parentManager.raiseWindow(this); this.dispatchEvent("windowdragdown", { detail: this, mouse: e }); } event__dragMouseUp(e, touch) { if (this.#isFrozen) return; if (this.#isDragging) this.dispatchEvent("windowdragup", { detail: this, mouse: e }); this.#orig_mousePosX = 0; this.#orig_mousePosY = 0; this.windowContent.style.userSelect = "auto"; this.windowContent.style.pointerEvents = "auto"; this.windowManager.style.cursor = "default"; this.#isDragging = false; } event__dragMouseMove(e, touch) { if (this.#isFrozen) return; if (this.#isDragging) { var cX = touch ? e.touches[0].clientX : e.clientX; var cY = touch ? e.touches[0].clientY : e.clientY; this.windowObject.style.left = this.#orig_selfPosX - (this.#orig_mousePosX - cX) + "px"; this.windowObject.style.top = this.#orig_selfPosY - (this.#orig_mousePosY - cY) + "px"; this.dispatchEvent("windowmove", { detail: this, mouse: e }); } } event__responseKeyDown(e) { switch (e.code) { case "Backquote": e.preventDefault(); this.parentManager.raiseIndex(this, 1); break; case "Backspace": e.preventDefault(); this.destroy(); break; case "Space": e.preventDefault(); if (e.shiftKey) this.minimizeWindow(); else this.maximizeWindow(); break; case "ArrowUp": e.preventDefault(); if (this.#isFrozen) return; if (e.shiftKey) this.windowObject.style.height = this.windowObject.offsetHeight - 10 + "px"; else this.windowObject.style.top = this.windowObject.offsetTop - 10 + "px"; break; case "ArrowDown": e.preventDefault(); if (this.#isFrozen) return; if (e.shiftKey) this.windowObject.style.height = this.windowObject.offsetHeight + 10 + "px"; else this.windowObject.style.top = this.windowObject.offsetTop + 10 + "px"; break; case "ArrowLeft": e.preventDefault(); if (this.#isFrozen) return; if (e.shiftKey) this.windowObject.style.width = this.windowObject.offsetWidth - 10 + "px"; else this.windowObject.style.left = this.windowObject.offsetLeft - 10 + "px"; break; case "ArrowRight": e.preventDefault(); if (this.#isFrozen) return; if (e.shiftKey) this.windowObject.style.width = this.windowObject.offsetWidth + 10 + "px"; else this.windowObject.style.left = this.windowObject.offsetLeft + 10 + "px"; break; case "KeyR": e.preventDefault(); if (this.#isFrozen) return; this.windowObject.style.width = `600px`; this.windowObject.style.height = `500px`; this.windowObject.style.left = `calc(50vw - ${this.width / 2}px)`; this.windowObject.style.top = `calc(50vh - ${this.height / 2}px)`; break; } } static createWindow(windowRef, content, srcdoc, w = 600, h = 500) { const windowObject = document.createElement("div"); windowObject.classList.add("window-object"); windowObject.style.width = `calc(${w}px + 2.75em)`; windowObject.style.height = `calc(${h}px + 4.5em)`; windowObject.style.left = `calc(50vw - (${w / 2}px + 1.375em))`; windowObject.style.top = `calc(50vh - (${h / 2}px + 2.25em))`; windowObject.tabIndex = "0"; { const windowManager = document.createElement("div"); windowManager.classList.add("window-manager"); { const windowManagerStart = document.createElement("div"); windowManagerStart.classList.add("window-manager-start"); { const windowManagerLabel = document.createElement("span"); windowManagerLabel.textContent = "Window"; windowManagerLabel.classList.add("window-manager-label"); windowManagerStart.appendChild(windowManagerLabel); } windowManager.appendChild(windowManagerStart); } { const windowManagerEnd = document.createElement("div"); windowManagerEnd.classList.add("window-manager-end"); if (!srcdoc) { const windowNewButton = document.createElement("button"); windowNewButton.innerHTML = "open_in_new"; windowNewButton.classList.add("window-new-button"); windowNewButton.addEventListener("click", () => window.open(content)); windowNewButton.addEventListener("touchstart", (e) => e.stopPropagation() ); windowNewButton.addEventListener("touchend", (e) => e.stopPropagation() ); windowManagerEnd.appendChild(windowNewButton); } { const windowMinimizeButton = document.createElement("button"); windowMinimizeButton.innerHTML = "minimize"; windowMinimizeButton.classList.add("window-minimize-button"); windowMinimizeButton.addEventListener("click", () => windowRef.minimizeWindow() ); windowMinimizeButton.addEventListener("touchstart", (e) => e.stopPropagation() ); windowMinimizeButton.addEventListener("touchend", (e) => e.stopPropagation() ); windowManagerEnd.appendChild(windowMinimizeButton); } { const windowMaximizeButton = document.createElement("button"); windowMaximizeButton.innerHTML = "maximize"; windowMaximizeButton.classList.add("window-maximize-button"); windowMaximizeButton.ariaHidden = true; // esoteric operation to screen-reader users windowMaximizeButton.addEventListener("click", () => windowRef.maximizeWindow() ); windowMaximizeButton.addEventListener("touchstart", (e) => e.stopPropagation() ); windowMaximizeButton.addEventListener("touchend", (e) => e.stopPropagation() ); windowManagerEnd.appendChild(windowMaximizeButton); } { const windowDestroyButton = document.createElement("button"); windowDestroyButton.innerHTML = "close"; windowDestroyButton.classList.add("window-destroy-button"); windowDestroyButton.ariaHidden = true; windowDestroyButton.addEventListener("click", () => windowRef.destroy() ); windowDestroyButton.addEventListener("touchstart", (e) => e.stopPropagation() ); windowDestroyButton.addEventListener("touchend", (e) => e.stopPropagation() ); windowManagerEnd.appendChild(windowDestroyButton); } windowManager.appendChild(windowManagerEnd); } windowObject.appendChild(windowManager); } { const windowContent = document.createElement("iframe"); windowContent.classList.add("window-content"); { if (srcdoc) windowContent.srcdoc = content; else windowContent.src = content; } windowObject.appendChild(windowContent); } return windowObject; } }