From a8d2ed380e6ae2a9a33a669126bcf37a3a65514d Mon Sep 17 00:00:00 2001 From: Hamcha Date: Thu, 30 Nov 2023 01:57:45 +0100 Subject: [PATCH] multi file editor --- static/components/editor/script.mjs | 98 ++++++++++++++++++++++++++++ static/css/editor.css | 7 +- static/vendor/domutil/domutil.d.ts | 1 + static/vendor/domutil/domutil.js | 2 + static/vendor/domutil/domutil.js.map | 7 ++ static/vendor/domutil/makeDOM.d.ts | 9 +++ templates/container/get-one.html | 10 +-- templates/stack/get-one.html | 20 ++++-- templates/stack/history.html | 4 +- 9 files changed, 145 insertions(+), 13 deletions(-) create mode 100644 static/components/editor/script.mjs create mode 100644 static/vendor/domutil/domutil.d.ts create mode 100644 static/vendor/domutil/domutil.js create mode 100644 static/vendor/domutil/domutil.js.map create mode 100644 static/vendor/domutil/makeDOM.d.ts diff --git a/static/components/editor/script.mjs b/static/components/editor/script.mjs new file mode 100644 index 0000000..c0ade61 --- /dev/null +++ b/static/components/editor/script.mjs @@ -0,0 +1,98 @@ +import "/static/vendor/ace/ace.js"; +import { $el } from "../../vendor/domutil/domutil.js"; + +class Editor extends HTMLElement { + editor; + + /** @type {HTMLDivElement} Tab container */ + tabEl; + + /** @type {Map} File name to file session */ + files; + + constructor() { + super(); + this.files = {}; + this.attachShadow({ mode: "open" }); + } + + connectedCallback() { + this.tabEl = $el("div", { className: "tab-container" }); + this.shadowRoot.append(this.tabEl); + + const style = $el("style"); + style.textContent = ` + .tab-container { + display: flex; + + & .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; + } + } + } + `; + const slot = $el("slot", { name: "body" }); + this.shadowRoot.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 {string} name File name + * @param {string} content File contents + */ + addFile(name, content) { + // Add to session list + this.files[name] = ace.createEditSession(content); + + // TODO replace this with auto-detection + this.files[name].setMode("ace/mode/nix"); + + // Create tab and set as active + const tab = this.#addTab(name); + tab.click(); + } + + /** + * Create a new tab + * @param {string} name Tab name + * @returns Tab element + */ + #addTab(name) { + const tab = $el("div", { + className: "tab", + "@click": () => { + this.editor.setSession(this.files[name]); + this.tabEl.querySelectorAll(".tab").forEach(el => el.classList.remove("active")); + tab.classList.add("active"); + } + }, name); + this.tabEl.append(tab); + return tab; + } +} + +customElements.define("file-editor", Editor); \ No newline at end of file diff --git a/static/css/editor.css b/static/css/editor.css index 1f6d9f2..8f32cbe 100644 --- a/static/css/editor.css +++ b/static/css/editor.css @@ -26,9 +26,14 @@ padding: 4px 8px; display: none; } + + & noscript { + display: flex; + flex-direction: column; + } } -#editor { +textarea.nojs-editor { border-width: 3px; min-height: 50vh; } diff --git a/static/vendor/domutil/domutil.d.ts b/static/vendor/domutil/domutil.d.ts new file mode 100644 index 0000000..3e4e538 --- /dev/null +++ b/static/vendor/domutil/domutil.d.ts @@ -0,0 +1 @@ +export * from "./makeDOM.ts"; diff --git a/static/vendor/domutil/domutil.js b/static/vendor/domutil/domutil.js new file mode 100644 index 0000000..8993e51 --- /dev/null +++ b/static/vendor/domutil/domutil.js @@ -0,0 +1,2 @@ +var f=Object.defineProperty;var m=(t,n)=>f(t,"name",{value:n,configurable:!0});function d(t,...n){let l=t,o="",T="";t.includes("#")&&([l,T]=t.split("#")),t.includes(".")&&([l,o]=t.split("."));let a=document.createElement(l);o&&(a.className=o),T&&(a.id=T);let s=n[0];if(typeof s=="object"&&!(s instanceof Node)){for(let e in s){let i=s[e];if(e.startsWith("@"))a.addEventListener(e.substring(1),i);else if(e==="dataset")for(let r in i)a.dataset[r]=i[r];else a[e]=i}n.shift()}for(let e of n)a.appendChild(e instanceof Node?e:document.createTextNode(e));return a}m(d,"$el");export{d as $el}; +//# sourceMappingURL=domutil.js.map diff --git a/static/vendor/domutil/domutil.js.map b/static/vendor/domutil/domutil.js.map new file mode 100644 index 0000000..6bc167e --- /dev/null +++ b/static/vendor/domutil/domutil.js.map @@ -0,0 +1,7 @@ +{ + "version": 3, + "sources": ["../makeDOM.ts"], + "sourcesContent": ["// Inspired by `make` by Matthew Crumley (silentmatt.com) https://stackoverflow.com/a/2947012\r\n// Licensed under AGPL-3.0, check `LICENSE` for the full text.\r\n\r\ntype ElementProperties = {\r\n [key in keyof HTMLElementTagNameMap[T]]: HTMLElementTagNameMap[T][key];\r\n} & {\r\n [key in keyof HTMLElementEventMap as `@${key}`]: (\r\n this: HTMLElement,\r\n ev: HTMLElementEventMap[key]\r\n ) => void;\r\n};\r\n\r\ntype DOMElem = Node | string;\r\n\r\ntype WithClassOrId =\r\n | `${T}#${string}`\r\n | `${T}.${string}`;\r\n\r\nexport function $el(\r\n name: T | WithClassOrId,\r\n ...desc: [Partial>, ...DOMElem[]] | DOMElem[]\r\n): HTMLElementTagNameMap[T] {\r\n // Take off ID or class from the name\r\n let elementName = name as string;\r\n let className = \"\";\r\n let id = \"\";\r\n\r\n if (name.includes(\"#\")) {\r\n [elementName, id] = name.split(\"#\");\r\n }\r\n\r\n if (name.includes(\".\")) {\r\n [elementName, className] = name.split(\".\");\r\n }\r\n\r\n // Create the element and add the attributes (if found)\r\n const el = document.createElement(elementName as T);\r\n if (className) {\r\n el.className = className;\r\n }\r\n if (id) {\r\n el.id = id;\r\n }\r\n\r\n const attributes = desc[0];\r\n if (typeof attributes === \"object\" && !(attributes instanceof Node)) {\r\n for (const attr in attributes) {\r\n const value = (attributes as Record)[attr];\r\n if (attr.startsWith(\"@\")) {\r\n el.addEventListener(attr.substring(1), value);\r\n } else if (attr === \"dataset\") {\r\n for (const key in value) {\r\n el.dataset[key] = value[key];\r\n }\r\n } else {\r\n el[attr as keyof HTMLElementTagNameMap[T]] = value;\r\n }\r\n }\r\n desc.shift();\r\n }\r\n\r\n for (const item of desc as DOMElem[]) {\r\n el.appendChild(item instanceof Node ? item : document.createTextNode(item));\r\n }\r\n\r\n return el;\r\n}\r\n\r\nexport default $el;\r\n"], + "mappings": "+EAkBO,SAASA,EACdC,KACGC,EACuB,CAE1B,IAAIC,EAAcF,EACdG,EAAY,GACZC,EAAK,GAELJ,EAAK,SAAS,GAAG,IACnB,CAACE,EAAaE,CAAE,EAAIJ,EAAK,MAAM,GAAG,GAGhCA,EAAK,SAAS,GAAG,IACnB,CAACE,EAAaC,CAAS,EAAIH,EAAK,MAAM,GAAG,GAI3C,IAAMK,EAAK,SAAS,cAAcH,CAAgB,EAC9CC,IACFE,EAAG,UAAYF,GAEbC,IACFC,EAAG,GAAKD,GAGV,IAAME,EAAaL,EAAK,CAAC,EACzB,GAAI,OAAOK,GAAe,UAAY,EAAEA,aAAsB,MAAO,CACnE,QAAWC,KAAQD,EAAY,CAC7B,IAAME,EAASF,EAAmCC,CAAI,EACtD,GAAIA,EAAK,WAAW,GAAG,EACrBF,EAAG,iBAAiBE,EAAK,UAAU,CAAC,EAAGC,CAAK,UACnCD,IAAS,UAClB,QAAWE,KAAOD,EAChBH,EAAG,QAAQI,CAAG,EAAID,EAAMC,CAAG,OAG7BJ,EAAGE,CAAsC,EAAIC,CAEjD,CACAP,EAAK,MAAM,CACb,CAEA,QAAWS,KAAQT,EACjBI,EAAG,YAAYK,aAAgB,KAAOA,EAAO,SAAS,eAAeA,CAAI,CAAC,EAG5E,OAAOL,CACT,CAhDgBM,EAAAZ,EAAA", + "names": ["$el", "name", "desc", "elementName", "className", "id", "el", "attributes", "attr", "value", "key", "item", "__name"] +} diff --git a/static/vendor/domutil/makeDOM.d.ts b/static/vendor/domutil/makeDOM.d.ts new file mode 100644 index 0000000..6ec1322 --- /dev/null +++ b/static/vendor/domutil/makeDOM.d.ts @@ -0,0 +1,9 @@ +type ElementProperties = { + [key in keyof HTMLElementTagNameMap[T]]: HTMLElementTagNameMap[T][key]; +} & { + [key in keyof HTMLElementEventMap as `@${key}`]: (this: HTMLElement, ev: HTMLElementEventMap[key]) => void; +}; +type DOMElem = Node | string; +type WithClassOrId = `${T}#${string}` | `${T}.${string}`; +export declare function $el(name: T | WithClassOrId, ...desc: [Partial>, ...DOMElem[]] | DOMElem[]): HTMLElementTagNameMap[T]; +export default $el; diff --git a/templates/container/get-one.html b/templates/container/get-one.html index 96aca98..04a7cae 100644 --- a/templates/container/get-one.html +++ b/templates/container/get-one.html @@ -192,8 +192,8 @@ logSource.close(); const err = document.createElement("div"); err.className = "error"; - err.appendChild(document.createTextNode("No logs available after this (container not running)")); - logEl.appendChild(err); + err.append(document.createTextNode("No logs available after this (container not running)")); + logEl.append(err); err.scrollIntoView(); return; }); @@ -204,8 +204,8 @@ logSource.close(); const err = document.createElement("div"); err.className = "error"; - err.appendChild(document.createTextNode(data.error)); - logEl.appendChild(err); + err.append(document.createTextNode(data.error)); + logEl.append(err); err.scrollIntoView(); return; } @@ -220,7 +220,7 @@ const [timestamp, logline] = [line.substring(0, firstSpace), line.substring(firstSpace + 1)]; const lineEl = document.createElement("p"); lineEl.innerHTML = `${convert.toHtml(logline)}`.trim(); - logEl.appendChild(lineEl); + logEl.append(lineEl); lineEl.scrollIntoView(); }); } diff --git a/templates/stack/get-one.html b/templates/stack/get-one.html index 5f46e0e..0b6f603 100644 --- a/templates/stack/get-one.html +++ b/templates/stack/get-one.html @@ -3,6 +3,7 @@ {% block title %}Stack {{stack_name}}{% endblock %} {% block content %} +

Stack details for {{stack_name}}

@@ -62,7 +63,14 @@
- + {% for (filename, content) in files %} + + + {% endfor %} +
@@ -105,14 +113,16 @@ {% endblock %} \ No newline at end of file diff --git a/templates/stack/history.html b/templates/stack/history.html index 21634d6..a7f4b34 100644 --- a/templates/stack/history.html +++ b/templates/stack/history.html @@ -129,12 +129,12 @@ let toggle = document.createElement("button"); toggle.type = "button"; - toggle.appendChild(document.createTextNode(toggle_text[0])); + toggle.append(document.createTextNode(toggle_text[0])); toggle.addEventListener("click", () => { const index = toggle_text.indexOf(toggle.textContent); versions.forEach(version => { version.open = index == 1 }); toggle.textContent = toggle_text[(index + 1) % 2]; }); - actions.appendChild(toggle); + actions.append(toggle); {% endblock %} \ No newline at end of file