// 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)} `; } else { windowVisualText.innerHTML = `${escapeHTML(text.slice(0, cursor))}${escapeHTML(text[cursor])}${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 = ` ${config.title} `; 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: "