diff --git a/.swcrc b/.swcrc index b9e4c87..a874d56 100644 --- a/.swcrc +++ b/.swcrc @@ -7,6 +7,6 @@ "decorators": false, "dynamicImport": false }, - "target": "esnext" + "target": "es2022" } } \ No newline at end of file diff --git a/static/scripts/components/editor/script.ts b/static/scripts/components/editor/script.ts index 867992b..169a93f 100644 --- a/static/scripts/components/editor/script.ts +++ b/static/scripts/components/editor/script.ts @@ -1,21 +1,77 @@ import "/static/vendor/ace/ace.js"; import { $el } from "/static/vendor/domutil/domutil.js"; +class EditorTab extends HTMLElement { + static observedAttributes = ["name", "selected"]; + + private tabEl: HTMLDivElement; + private buttonsEl: HTMLDivElement; + private root: ShadowRoot; + + constructor() { + super(); + this.root = this.attachShadow({ mode: "open" }); + + const style = $el("style"); + style.textContent = ` + :host { + border: 2px solid var(--table-border-color); + padding: 0.5ch 0.8ch; + background-color: var(--button-bg); + cursor: pointer; + } + :host([selected="true"]) { + background-color: var(--table-border-color); + } + :host([modified="true"]) .tab-name::after { + content: " ᴹ"; + } + :host([unsaved="true"]) { + font-style: italic; + } + `; + this.root.append(style); + + this.tabEl = $el("div", { className: "tab-name" }, this.getAttribute("name") || $el("i", "unknown")); + this.buttonsEl = $el("div", { className: "buttons" }); + this.root.append(this.tabEl, this.buttonsEl); + } + + connectedCallback() { + } + + attributeChangedCallback(name, _, value) { + switch (name) { + case "name": + this.tabEl.textContent = value; + break; + case "selected": + this.classList.toggle("active", Boolean(value)); + break; + } + } +} +customElements.define("file-editor-tab", EditorTab); + + class Editor extends HTMLElement { private editor: AceAjax.Editor; private tabEl: HTMLDivElement; private sessions: Record; private root: ShadowRoot; - private addTabButton: HTMLDivElement; + private addTabButton: EditorTab; + private original: Record; constructor() { super(); this.sessions = {}; + this.original = {}; this.root = this.attachShadow({ mode: "open" }); } connectedCallback() { - this.addTabButton = $el("div", { className: "tab" }, "+"); + this.addTabButton = new EditorTab(); + this.addTabButton.setAttribute("name", "+"); this.addTabButton.addEventListener("click", () => this.#createNewTab()); this.tabEl = $el("div", { className: "tab-container" }, this.addTabButton); this.root.append(this.tabEl); @@ -25,19 +81,10 @@ class Editor extends HTMLElement { .tab-container { display: flex; margin-bottom: -2px; - - & .tab { - border: 2px solid var(--table-border-color); - padding: 0.5ch 0.8ch; - background-color: var(--button-bg); - cursor: pointer; - &.active { - background-color: var(--table-border-color); - } - &:not(:first-child) { - margin-left: -2px; - } - } + } + + file-editor-tab:not(:first-child) { + margin-left: -2px; } `; const slot = $el("slot", { name: "body" }); @@ -65,23 +112,27 @@ class Editor extends HTMLElement { * @param name File name * @param content File contents */ - addFile(name: string, content: string) { + addFile(name: string, content: string, saved = true) { // Add to session list this.sessions[name] = ace.createEditSession(content); + this.original[name] = content; // TODO replace this with auto-detection this.sessions[name].setMode("ace/mode/nix"); // Create tab and set as active - const tab = this.#addTab(name); + const tab = this.#addTab(name, saved); tab.click(); + + this.sessions[name].on("change", () => { + tab.setAttribute("modified", this.sessions[name].getValue() != this.original[name] ? "true" : "false"); + }); } setCurrent(name: string) { this.editor.setSession(this.sessions[name]); - this.tabEl.querySelectorAll(".tab").forEach(el => { - if (el.dataset.name === name) el.classList.add("active"); - else el.classList.remove("active"); + this.tabEl.querySelectorAll("file-editor-tab").forEach(el => { + el.setAttribute("selected", el.getAttribute("name") == name ? "true" : "false"); }); } @@ -94,20 +145,18 @@ class Editor extends HTMLElement { * @param name Tab name * @returns Tab element */ - #addTab(name: string) { - const tab = $el("div", { - className: "tab", - dataset: { name }, - "@click": () => this.setCurrent(name) - }, name); + #addTab(name: string, saved = true) { + const tab = new EditorTab(); + tab.setAttribute("name", name); + tab.setAttribute("unsaved", saved ? "false" : "true"); + tab.addEventListener("click", () => this.setCurrent(name)); this.tabEl.insertBefore(tab, this.addTabButton); return tab; } #createNewTab() { - this.addFile(`untitled${Object.keys(this.sessions).filter(s => s.startsWith("untitled")).length + 1}`, ""); + this.addFile(`untitled${Object.keys(this.sessions).filter(s => s.startsWith("untitled")).length + 1}`, "", false); } } - customElements.define("file-editor", Editor); \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index 4481fed..7ad53dc 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,19 +1,19 @@ { "compilerOptions": { - "target": "esnext", + "target": "ES2022", "paths": { "/static/*": [ "./static/*" ], }, "lib": [ - "ESNext", + "ES2022", "DOM" ], "rootDir": "./", "baseUrl": "./", "strictNullChecks": true, - "declaration": true, + "declaration": true }, "include": [ "./static/**/*.ts",