multi file editor

This commit is contained in:
Hamcha 2023-11-30 01:57:45 +01:00
parent 8d2788b911
commit a8d2ed380e
9 changed files with 145 additions and 13 deletions

View file

@ -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<string, AceSession>} 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);

View file

@ -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;
}

1
static/vendor/domutil/domutil.d.ts vendored Normal file
View file

@ -0,0 +1 @@
export * from "./makeDOM.ts";

2
static/vendor/domutil/domutil.js vendored Normal file
View file

@ -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

7
static/vendor/domutil/domutil.js.map vendored Normal file
View file

@ -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<T extends keyof HTMLElementTagNameMap> = {\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<T extends keyof HTMLElementTagNameMap> =\r\n | `${T}#${string}`\r\n | `${T}.${string}`;\r\n\r\nexport function $el<T extends keyof HTMLElementTagNameMap>(\r\n name: T | WithClassOrId<T>,\r\n ...desc: [Partial<ElementProperties<T>>, ...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<string, any>)[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"]
}

9
static/vendor/domutil/makeDOM.d.ts vendored Normal file
View file

@ -0,0 +1,9 @@
type ElementProperties<T extends keyof HTMLElementTagNameMap> = {
[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 extends keyof HTMLElementTagNameMap> = `${T}#${string}` | `${T}.${string}`;
export declare function $el<T extends keyof HTMLElementTagNameMap>(name: T | WithClassOrId<T>, ...desc: [Partial<ElementProperties<T>>, ...DOMElem[]] | DOMElem[]): HTMLElementTagNameMap[T];
export default $el;

View file

@ -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 = `<time datetime="${timestamp}">${timestamp}</time>${convert.toHtml(logline)}`.trim();
logEl.appendChild(lineEl);
logEl.append(lineEl);
lineEl.scrollIntoView();
});
}

View file

@ -3,6 +3,7 @@
{% block title %}Stack {{stack_name}}{% endblock %}
{% block content %}
<script type="module" src="/static/components/editor/script.mjs" defer></script>
<main>
<header>
<h1>Stack details for <span class="stack-name">{{stack_name}}</span></h1>
@ -62,7 +63,14 @@
</h2>
<form method="POST" action="./edit" id="editor-form">
<div class="error"></div>
<textarea name="source" id="editor">{{files["arion-compose.nix"]}}</textarea>
{% for (filename, content) in files %}
<script type="stackfile" data-name="{{filename}}">{{content|safe}}</script>
<noscript>
<h3>{{filename}}</h3>
<textarea name="file[{{filename}}]" class="nojs-editor">{{content|safe}}</textarea>
</noscript>
{% endfor %}
<file-editor></file-editor>
<div class="row">
<input style="flex:1" name="commit_message" type="text"
placeholder="What did you change?" />
@ -105,14 +113,16 @@
</style>
<script type="module">
import Editor from "/static/js/ace.mjs";
import { add_check } from "/static/js/enhancements/check.mjs";
const editor = new Editor("editor");
const editor = document.querySelector("file-editor");
document.querySelectorAll("script[type=stackfile]").forEach((script) => {
editor.addFile(script.dataset.name, script.innerText);
});
/* Enforce check pre-submit */
const form = document.getElementById("editor-form");
add_check(form, editor);
//const form = document.getElementById("editor-form");
//add_check(form, editor);
</script>
{% endblock %}

View file

@ -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);
</script>
{% endblock %}