469 lines
17 KiB
TypeScript
469 lines
17 KiB
TypeScript
// 1 - border
|
|
// 10 - padding
|
|
// 35 - header
|
|
const _windowPaddingX: number = 1*2 + 10*2;
|
|
const _windowPaddingY: number = 1*2 + 10*2 + 35;
|
|
|
|
let WINDOWS: { [key: string]: _winConf } = {};
|
|
let MOUSE_MOVE_PROCESSING: {
|
|
[key: string]: {
|
|
callback: (x: number, y: number) => void,
|
|
mouseUp: boolean
|
|
}
|
|
} = {};
|
|
|
|
let globalIncrement: number = 1;
|
|
|
|
function escapeHTML(string: string): string {
|
|
return string.replaceAll("&", "&").replaceAll("<", "<").replaceAll("\"", """);
|
|
}
|
|
|
|
function incrementZIndex(windowID: string, focus: boolean=false): void {
|
|
WINDOWS[windowID].element.style.zIndex = String(globalIncrement);
|
|
WINDOWS[windowID].zIndex = globalIncrement;
|
|
globalIncrement++;
|
|
|
|
if (focus) {
|
|
WINDOWS[windowID].element.focus();
|
|
}
|
|
}
|
|
|
|
function edgeMoveEvent(x: number, y: number, pos: "top" | "bottom" | "left" | "right" | "top-left" | "top-right" | "bottom-left" | "bottom-right" | string, windowID: string): void {
|
|
let w: _winConf = WINDOWS[windowID];
|
|
|
|
if (w.fullscreen) { return; }
|
|
|
|
if (pos == "top" || pos.startsWith("top-")) {
|
|
w.height = Math.max(-(y - w.vars.mouseOffsetY - w.vars.startingPosY) + w.vars.startingHeight, w.minHeight);
|
|
w.posY = Math.max(w.vars.startingHeight - w.height + w.vars.startingPosY, 0);
|
|
|
|
if (w.posY < 0) {
|
|
w.height -= w.posY;
|
|
w.posY = 0;
|
|
}
|
|
} else if (pos == "bottom" || pos.startsWith("bottom-")) {
|
|
w.height = Math.max(y - w.vars.mouseOffsetY - w.vars.startingPosY + w.vars.startingHeight, w.minHeight);
|
|
}
|
|
|
|
if (pos == "left" || pos.endsWith("-left")) {
|
|
w.width = Math.max(-(x - w.vars.mouseOffsetX - w.vars.startingPosX) + w.vars.startingWidth, w.minWidth);
|
|
w.posX = Math.max(w.vars.startingWidth - w.width + w.vars.startingPosX, 0);
|
|
|
|
if (w.posX < 0) {
|
|
w.width -= w.posX;
|
|
w.posX = 0;
|
|
}
|
|
} else if (pos == "right" || pos.endsWith("-right")) {
|
|
w.width = Math.max(x - w.vars.mouseOffsetX - w.vars.startingPosX + w.vars.startingWidth, w.minWidth);
|
|
}
|
|
|
|
if (w.posX + w.width + _windowPaddingX > innerWidth ) { w.width = Math.max(innerWidth - w.posX - _windowPaddingX, w.minWidth); }
|
|
if (w.posY + w.height + _windowPaddingY > innerHeight) { w.height = Math.max(innerHeight - w.posY - _windowPaddingY, w.minHeight); }
|
|
|
|
w.element.style.left = `${w.posX}px`;
|
|
w.element.style.top = `${w.posY}px`;
|
|
|
|
w.element.style.width = `${w.width + _windowPaddingX - 2}px`;
|
|
(w.element.querySelector(".window") as HTMLElement).style.width = `${w.width }px`;
|
|
(w.element.querySelector(".window") as HTMLElement).style.height = `${w.height}px`;
|
|
}
|
|
|
|
function mouseMoveEvent(windowID: string, x: number, y: number): void {
|
|
let w: _winConf = WINDOWS[windowID];
|
|
w.posX = Math.max(0, Math.min(innerWidth - w.width - _windowPaddingX, x - w.vars.mouseOffsetX));
|
|
w.posY = Math.max(0, Math.min(innerHeight - w.height - _windowPaddingY, y - w.vars.mouseOffsetY));
|
|
w.element.style.left = `${w.posX}px`;
|
|
w.element.style.top = `${w.posY}px`;
|
|
}
|
|
|
|
function syncInputs(windowID: string): void {
|
|
let windowInput: HTMLInputElement = WINDOWS[windowID].element.querySelector("input.window-input");
|
|
let windowVisualText: HTMLDivElement = WINDOWS[windowID].element.querySelector("[data-type-area]");
|
|
let w: HTMLDivElement = WINDOWS[windowID].element.querySelector(".window");
|
|
|
|
if (!windowVisualText) { return; }
|
|
|
|
setTimeout(function(): void {
|
|
let text: string = windowInput.value;
|
|
let cursor: number = windowInput.selectionStart;
|
|
|
|
if (cursor == text.length) {
|
|
windowVisualText.innerHTML = `${escapeHTML(text)}<i class="cursor"> </i>`;
|
|
} else {
|
|
windowVisualText.innerHTML = `${escapeHTML(text.slice(0, cursor))}<span class="cursor">${escapeHTML(text[cursor])}</span>${escapeHTML(text.slice(cursor + 1))}`;
|
|
}
|
|
}, 1);
|
|
|
|
w.scrollTop = w.scrollHeight;
|
|
}
|
|
|
|
function setCursor(windowID: string): void {
|
|
let windowInput: HTMLInputElement = WINDOWS[windowID].element.querySelector("input.window-input");
|
|
|
|
setTimeout((): void => {
|
|
windowInput.setSelectionRange(windowInput.value.length, windowInput.value.length);
|
|
syncInputs(windowID);
|
|
}, 1);
|
|
}
|
|
|
|
function toggleFullscreen(windowID: string): void {
|
|
let w: _winConf = WINDOWS[windowID];
|
|
if (w.fullscreen) {
|
|
w.posX = Math.max(0, Math.min(innerWidth - w.vars.oldWidth - _windowPaddingX, w.vars.oldPosX)); // window.vars.oldPosX;
|
|
w.posY = Math.max(0, Math.min(innerHeight - w.vars.oldHeight - _windowPaddingY, w.vars.oldPosY)); // window.vars.oldPosY;
|
|
w.width = Math.max(w.minWidth, Math.min(w.vars.oldWidth, innerWidth - _windowPaddingX)); // window.vars.oldWidth;
|
|
w.height = Math.max(w.minHeight, Math.min(w.vars.oldHeight, innerHeight - _windowPaddingY)); // window.vars.oldHeight;
|
|
w.fullscreen = false;
|
|
delete w.vars.oldPosX;
|
|
delete w.vars.oldPosY;
|
|
delete w.vars.oldWidth;
|
|
delete w.vars.oldHeight;
|
|
w.element.style.left = `${w.posX}px`;
|
|
w.element.style.top = `${w.posY}px`;
|
|
w.element.style.width = `${w.width + _windowPaddingX - 2}px`;
|
|
(w.element.querySelector(".window") as HTMLElement).style.width = `${w.width}px`;
|
|
(w.element.querySelector(".window") as HTMLElement).style.height = `${w.height}px`;
|
|
} else {
|
|
w.vars.oldPosX = w.posX;
|
|
w.vars.oldPosY = w.posY;
|
|
w.vars.oldWidth = w.width;
|
|
w.vars.oldHeight = w.height;
|
|
w.fullscreen = true;
|
|
w.posX = 0;
|
|
w.posY = 0;
|
|
w.width = innerWidth;
|
|
w.height = innerHeight;
|
|
w.element.style.left = "0px";
|
|
w.element.style.top = "0px";
|
|
w.element.style.width = `${w.width}px`;
|
|
(w.element.querySelector(".window") as HTMLElement).style.width = `${w.width - _windowPaddingX}px`;
|
|
(w.element.querySelector(".window") as HTMLElement).style.height = `${w.height - _windowPaddingY}px`;
|
|
}
|
|
}
|
|
|
|
function createWindow(config: _winInitConf): void {
|
|
if (document.getElementById(config.id)) {
|
|
incrementZIndex(config.id);
|
|
return;
|
|
}
|
|
|
|
config.width = config.width || 600;
|
|
config.height = config.height || 400;
|
|
config.minWidth = config.minWidth || 200;
|
|
config.minHeight = config.minHeight || 200;
|
|
|
|
let realWidth: number = Math.max(config.minWidth, Math.min(config.width, innerWidth - _windowPaddingX - 20));
|
|
let realHeight: number = Math.max(config.minHeight, Math.min(config.height, innerHeight - _windowPaddingY - 20));
|
|
|
|
let posX: number = config.posX || Math.round((innerWidth / 2) - ((realWidth + _windowPaddingX) / 2));
|
|
let posY: number = config.posY || Math.round((innerHeight / 2) - ((realHeight + _windowPaddingY) / 2));
|
|
|
|
let wO: HTMLDivElement = document.createElement("div");
|
|
wO.classList.add("window-outer");
|
|
|
|
let w: HTMLDivElement = document.createElement("div");
|
|
w.classList.add("window");
|
|
w.style.width = `${realWidth}px`;
|
|
w.style.height = `${realHeight}px`;
|
|
w.innerHTML = config.content;
|
|
|
|
let wH: HTMLDivElement = document.createElement("div");
|
|
wH.classList.add("window-header");
|
|
wH.innerHTML = `
|
|
<i class="window-header-button blank"></i>
|
|
<i class="window-header-button blank"></i>
|
|
<i class="window-header-button blank"></i>
|
|
<strong class="window-header-title">${config.title}</strong>
|
|
<i data-no-move class="window-header-button minimize"></i>
|
|
<i data-no-move class="window-header-button fullscreen"></i>
|
|
<i data-no-move class="window-header-button close"></i>
|
|
`;
|
|
|
|
let wC: HTMLDivElement | HTMLLabelElement;
|
|
let wI: HTMLInputElement = null;
|
|
|
|
if (config.typeable !== false) {
|
|
wI = document.createElement("input");
|
|
wI.classList.add("window-input");
|
|
wI.id = `${config.id}__input`;
|
|
|
|
wI.oninput = (event: KeyboardEvent): void => {
|
|
syncInputs(config.id);
|
|
w.scrollTop = w.scrollHeight;
|
|
};
|
|
|
|
wI.onkeydown = (event: KeyboardEvent): void => {
|
|
if (event.key == "Enter") {
|
|
commandManager(config.id, wI.value.trim());
|
|
w.scrollTop = w.scrollHeight;
|
|
wI.value = "";
|
|
} else if (event.key == "Tab") {
|
|
event.preventDefault();
|
|
|
|
let val: string = wI.value.trim();
|
|
let possibilities: string[] = [];
|
|
let parent: _file | null;
|
|
|
|
if (!val) { return; }
|
|
|
|
if (val.split(" ").length == 1 && wI.value[wI.value.length - 1] != " ") {
|
|
possibilities = Object.keys(_internal_commands).filter((cmd: string): boolean => (cmd.startsWith(val) && !cmd.startsWith("_")));
|
|
} else if (_internal_commands[val.split(" ")[0]] && _internal_commands[val.split(" ")[0]].autocomplete) {
|
|
let ac: "dir" | "file" | string[] = _internal_commands[val.split(" ")[0]].autocomplete;
|
|
let path: string = val.split(" ").slice(1).join(" ").trim();
|
|
let sw: string = path.split("/")[path.split("/").length - 1];
|
|
|
|
if (typeof ac == "object") {
|
|
possibilities = ac;
|
|
} else {
|
|
if (path) {
|
|
if (path[path.length - 1] == "/") {
|
|
parent = _internal_getFile(_internal_sanitizePath(_internal_joinPaths(windowInformation[config.id].PWD, path)));
|
|
} else {
|
|
parent = _internal_getFile(_internal_sanitizePath(_internal_joinPaths(windowInformation[config.id].PWD, path + "/..")));
|
|
}
|
|
} else {
|
|
parent = _internal_getFile(windowInformation[config.id].PWD);
|
|
}
|
|
|
|
if (parent && parent.type == "directory") {
|
|
let f: _files = parent.files;
|
|
possibilities = Object.keys(f);
|
|
|
|
if (ac == "dir") {
|
|
possibilities = possibilities.filter((file: string): boolean => (f[file] && f[file].type == "directory"));
|
|
}
|
|
} else {
|
|
parent = null;
|
|
}
|
|
}
|
|
|
|
possibilities = possibilities.filter((v: string): boolean => v.startsWith(sw));
|
|
}
|
|
|
|
if (possibilities.length == 1) {
|
|
if (val.split(" ").length == 1 && wI.value[wI.value.length - 1] != " ") {
|
|
wI.value = possibilities[0] + " ";
|
|
} else if (_internal_commands[val.split(" ")[0]] && _internal_commands[val.split(" ")[0]].autocomplete) {
|
|
let path: string = val;
|
|
|
|
if (val[val.length - 1] == "/") {
|
|
path += possibilities[0];
|
|
} else {
|
|
let p: string[] = path.split("/");
|
|
|
|
if (p.length == 1) {
|
|
p = p[0].split(" ", 2);
|
|
|
|
if (p.length == 1) {
|
|
p.push("");
|
|
}
|
|
|
|
p.pop();
|
|
path = p.join(" ") + " " + possibilities[0];
|
|
} else {
|
|
p.pop();
|
|
path = p.join("/") + "/" + possibilities[0];
|
|
}
|
|
}
|
|
|
|
wI.value = path + (parent && parent.type == "directory" && parent.files[possibilities[0]].type == "directory" ? "/" : " ");
|
|
}
|
|
|
|
syncInputs(config.id);
|
|
} else if (possibilities) {
|
|
addWindowCommand(config.id, possibilities.join(" "));
|
|
syncInputs(config.id);
|
|
}
|
|
} else {
|
|
syncInputs(config.id);
|
|
}
|
|
};
|
|
|
|
wI.onfocus = (): void => { setCursor(config.id); };
|
|
wI.onclick = (): void => { setCursor(config.id); };
|
|
|
|
wC = document.createElement("label");
|
|
wC.htmlFor = `${config.id}__input`;
|
|
} else {
|
|
wC = document.createElement("div");
|
|
}
|
|
|
|
wC.classList.add("window-container");
|
|
wC.tabIndex = 0;
|
|
wC.style.left = `${posX}px`;
|
|
wC.style.top = `${posY}px`;
|
|
wC.id = config.id;
|
|
wC.style.zIndex = String(globalIncrement);
|
|
wC.style.width = `${realWidth + _windowPaddingX - 2}px`;
|
|
|
|
let edges: DocumentFragment = document.createDocumentFragment();
|
|
for (const pos of ["top", "bottom", "left", "right", "top-left", "top-right", "bottom-left", "bottom-right"]) {
|
|
let el: HTMLDivElement = document.createElement("div");
|
|
el.classList.add("edge", pos);
|
|
el.addEventListener("mousedown", function(e: MouseEvent): void {
|
|
incrementZIndex(config.id);
|
|
e.preventDefault();
|
|
|
|
WINDOWS[config.id].vars.mouseOffsetX = e.clientX - WINDOWS[config.id].posX;
|
|
WINDOWS[config.id].vars.mouseOffsetY = e.clientY - WINDOWS[config.id].posY;
|
|
WINDOWS[config.id].vars.startingWidth = WINDOWS[config.id].width;
|
|
WINDOWS[config.id].vars.startingHeight = WINDOWS[config.id].height;
|
|
WINDOWS[config.id].vars.startingPosX = WINDOWS[config.id].posX;
|
|
WINDOWS[config.id].vars.startingPosY = WINDOWS[config.id].posY;
|
|
MOUSE_MOVE_PROCESSING[config.id] = {
|
|
callback: (x: number, y: number): void => { edgeMoveEvent(x, y, pos, config.id); },
|
|
mouseUp: true
|
|
};
|
|
});
|
|
|
|
edges.append(el);
|
|
}
|
|
|
|
wO.append(w);
|
|
wC.append(wH, wO, edges);
|
|
document.body.append(wC);
|
|
|
|
if (config.typeable !== false) {
|
|
wC.append(wI);
|
|
wI.focus();
|
|
} else {
|
|
wC.focus();
|
|
}
|
|
|
|
WINDOWS[config.id] = {
|
|
element: wC,
|
|
height: realHeight,
|
|
width: realWidth,
|
|
minHeight: config.minHeight,
|
|
minWidth: config.minWidth,
|
|
posX: posX,
|
|
posY: posY,
|
|
fullscreen: false,
|
|
zIndex: globalIncrement,
|
|
vars: {}
|
|
};
|
|
|
|
windowInformation[config.id] = {
|
|
PWD: HOME_DIR
|
|
};
|
|
|
|
// wC.addEventListener("mousedown", function(): void { incrementZIndex(config.id); });
|
|
wC.addEventListener("focus", function(): void { incrementZIndex(config.id); });
|
|
|
|
for (const link of wC.querySelectorAll("a")) {
|
|
link.addEventListener("focus", function(): void { incrementZIndex(config.id); });
|
|
}
|
|
|
|
wH.addEventListener("mousedown", function(e: MouseEvent): void {
|
|
if ((e.target as HTMLElement).dataset.noMove !== undefined) {
|
|
return;
|
|
}
|
|
|
|
WINDOWS[config.id].vars.mouseOffsetX = e.clientX - WINDOWS[config.id].posX;
|
|
WINDOWS[config.id].vars.mouseOffsetY = e.clientY - WINDOWS[config.id].posY;
|
|
MOUSE_MOVE_PROCESSING[config.id] = {
|
|
callback: (x: number, y: number): void => { mouseMoveEvent(config.id, x, y); },
|
|
mouseUp: true
|
|
};
|
|
});
|
|
|
|
wH.querySelector(".close").addEventListener("click", function(): void {
|
|
delete WINDOWS[config.id];
|
|
delete windowInformation[config.id];
|
|
delete MOUSE_MOVE_PROCESSING[config.id];
|
|
wC.remove();
|
|
|
|
if (typeof config.onDestroy === "function") {
|
|
config.onDestroy();
|
|
}
|
|
});
|
|
|
|
wH.querySelector(".fullscreen").addEventListener("click", (): void => (toggleFullscreen(config.id)));
|
|
wH.addEventListener("dblclick", (): void => (toggleFullscreen(config.id)));
|
|
|
|
globalIncrement++;
|
|
}
|
|
|
|
function windowPreset(template: string, dontDisableTyping: boolean=false): void {
|
|
let el: HTMLElement = document.querySelector(`#window-templates > [data-template-id="${template}"]`);
|
|
|
|
if (!el) { return; }
|
|
|
|
if (WINDOWS[template]) {
|
|
incrementZIndex(template, true);
|
|
return;
|
|
}
|
|
|
|
let config: _winInitConf = {
|
|
id: template,
|
|
title: "~ - tSh",
|
|
content: "<div><b class=\"green\">trinkey@website</b>:<b class=\"blue\">~</b>$ <span data-type-area><i class=\"cursor\"> </i></span></div>"
|
|
};
|
|
|
|
for (const field of el.querySelectorAll("[data-template-field]")) {
|
|
config[(field as HTMLElement).dataset.templateField] = (field as HTMLElement).dataset.isNumber === "" ? +(field as HTMLElement).innerText : (field as HTMLElement).innerHTML;
|
|
}
|
|
|
|
createWindow(config);
|
|
|
|
for (const command of el.querySelectorAll("li")) {
|
|
WINDOWS[template].element.querySelector("[data-type-area]").innerHTML = command.innerHTML;
|
|
commandManager(template, command.innerHTML);
|
|
}
|
|
|
|
if (!dontDisableTyping) {
|
|
WINDOWS[template].element.querySelector("input").remove();
|
|
}
|
|
}
|
|
|
|
function emptyWindow(): void {
|
|
createWindow({
|
|
id: `terminal-${Math.random()}`,
|
|
title: "~ - tSh",
|
|
content: "<div><b class=\"green\">trinkey@website</b>:<b class=\"blue\">~</b>$ <span data-type-area><i class=\"cursor\"> </i></span></div>"
|
|
});
|
|
}
|
|
|
|
function copyButton(): void {
|
|
navigator.clipboard.writeText("<a href=\"https://trinkey.com/\" target=\"_blank\"><img src=\"https://trinkey.com/img/88x31.png\" alt=\"trinkey's 88x31. image of her cat on the right with the word 'trinkey' taking up the rest of the button.\" title=\"trinkey's 88x31. image of her cat on the right with the word 'trinkey' taking up the rest of the button.\"></a>");
|
|
}
|
|
|
|
onmousemove = function(e: MouseEvent): void {
|
|
e.preventDefault();
|
|
|
|
for (const key of Object.keys(MOUSE_MOVE_PROCESSING)) {
|
|
MOUSE_MOVE_PROCESSING[key].callback(e.clientX, e.clientY);
|
|
}
|
|
}
|
|
|
|
onmouseup = function(): void {
|
|
for (const key of Object.keys(MOUSE_MOVE_PROCESSING)) {
|
|
if (MOUSE_MOVE_PROCESSING[key].mouseUp) {
|
|
delete MOUSE_MOVE_PROCESSING[key];
|
|
};
|
|
}
|
|
}
|
|
|
|
onresize = function(): void {
|
|
for (const window of Object.keys(WINDOWS)) {
|
|
let w: _winConf = WINDOWS[window];
|
|
|
|
if (w.fullscreen) {
|
|
w.width = innerWidth - _windowPaddingX + 2;
|
|
w.height = innerHeight - _windowPaddingY + 2;
|
|
} else {
|
|
w.posX = Math.max(0, Math.min(innerWidth - w.width - _windowPaddingX, w.posX));
|
|
w.posY = Math.max(0, Math.min(innerHeight - w.height - _windowPaddingY, w.posY));
|
|
|
|
w.width = Math.max(w.minWidth, Math.min(w.width, innerWidth - _windowPaddingX));
|
|
w.height = Math.max(w.minHeight, Math.min(w.height, innerHeight - _windowPaddingY));
|
|
}
|
|
|
|
w.element.style.left = `${w.posX}px`;
|
|
w.element.style.top = `${w.posY}px`;
|
|
w.element.style.width = `${w.width + _windowPaddingX - 2}px`;
|
|
(w.element.querySelector(".window") as HTMLElement).style.width = `${w.width }px`;
|
|
(w.element.querySelector(".window") as HTMLElement).style.height = `${w.height}px`;
|
|
}
|
|
}
|