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: EditorTab; private original: Record; constructor() { super(); this.sessions = {}; this.original = {}; this.root = this.attachShadow({ mode: "open" }); } connectedCallback() { 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); const style = $el("style"); style.textContent = ` .tab-container { display: flex; margin-bottom: -2px; } file-editor-tab:not(:first-child) { margin-left: -2px; } `; const slot = $el("slot", { name: "body" }); this.root.append(style, slot); const editorEl = $el("div", { slot: "body" }); this.append(editorEl); // Create editor ace.config.set('basePath', '/static/vendor/ace') this.editor = ace.edit(editorEl); // todo make this stuff customizable?? this.editor.setTheme("ace/theme/dracula"); this.editor.setOptions({ fontFamily: "Iosevka Web", fontSize: "12pt", }); this.editor.getSession().setUseWrapMode(true); this.editor.setKeyboardHandler("ace/keyboard/vim"); } /** * Add a file editing session * @param name File name * @param content File contents */ 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, 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("file-editor-tab").forEach(el => { el.setAttribute("selected", el.getAttribute("name") == name ? "true" : "false"); }); } files(): [String, String][] { return Object.entries(this.sessions).map(([name, session]) => [name, session.getValue()]); } /** * Create a new tab * @param name Tab name * @returns Tab element */ #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}`, "", false); } } customElements.define("file-editor", Editor);