635 lines
18 KiB
JavaScript
Executable file
635 lines
18 KiB
JavaScript
Executable file
function isTouchDevice() {
|
|
return (
|
|
"ontouchstart" in window ||
|
|
navigator.maxTouchPoints > 0 ||
|
|
navigator.msMaxTouchPoints > 0
|
|
);
|
|
}
|
|
|
|
class WindowManager {
|
|
managerObject;
|
|
|
|
children;
|
|
|
|
minZIndex = 50;
|
|
maxZIndex = 100;
|
|
|
|
constructor(manager) {
|
|
this.managerObject = manager;
|
|
this.managerObject.classList.add("window-master");
|
|
this.managerObject.style.zIndex = this.minZIndex;
|
|
|
|
this.children = [];
|
|
|
|
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)
|
|
);
|
|
}
|
|
|
|
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(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);
|
|
}
|
|
|
|
removeWindow(windowId) {
|
|
this.children.splice(windowId, 1);
|
|
if (this.children.length > 0)
|
|
this.raiseWindow(
|
|
this.children.toSorted((x, y) => x.focusOrder - y.focusOrder)[0]
|
|
);
|
|
}
|
|
|
|
destroy() {
|
|
this.children.forEach((x) => {
|
|
console.log(x);
|
|
x.destroy();
|
|
});
|
|
this.managerObject.remove();
|
|
this.children = [];
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
set content(src) {
|
|
this.windowContent.srcdoc = src;
|
|
}
|
|
|
|
set contentUrl(url) {
|
|
this.windowContent.src = url;
|
|
}
|
|
|
|
set x(x) {
|
|
this.windowObject.style.left = x + "px";
|
|
}
|
|
|
|
set y(y) {
|
|
this.windowObject.style.top = y = "px";
|
|
}
|
|
|
|
set width(w) {
|
|
this.windowObject.style.width = w + "px";
|
|
}
|
|
|
|
set height(h) {
|
|
this.windowObject.style.height = h = "px";
|
|
}
|
|
|
|
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.parentManager.removeWindow(this.windowId);
|
|
this.dispatchEvent("windowdestroy", { detail: this });
|
|
}
|
|
|
|
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("windowdragmove", { 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)`;
|
|
windowObject.style.top = `calc(50vh - ${h / 2}px)`;
|
|
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;
|
|
}
|
|
}
|