A music player that connects to your cloud/distributed storage.
1import { basicSetup, EditorView } from "codemirror";
2import { css as langCss } from "@codemirror/lang-css";
3import { html as langHtml } from "@codemirror/lang-html";
4import { javascript as langJs } from "@codemirror/lang-javascript";
5import { autocompletion } from "@codemirror/autocomplete";
6
7import * as TID from "@atcute/tid";
8
9import * as CID from "~/common/cid.js";
10import * as Output from "~/common/output.js";
11import { facetFromURI } from "~/common/facets/utils.js";
12import { loadURI } from "~/common/loader.js";
13import { signal } from "~/common/signal.js";
14
15import { saveFacet } from "./crud.js";
16import { output } from "./output.js";
17
18/**
19 * @import {Facet} from "~/definitions/types.d.ts"
20 */
21
22const $editor = signal(/** @type {EditorView | null} */ (null));
23const $editingFacet = signal(/** @type {Facet | null} */ (null));
24
25////////////////////////////////////////////
26// EDITOR
27////////////////////////////////////////////
28
29export function renderEditor() {
30 // Code editor
31 const editorContainer = document.body.querySelector("#html-input-container");
32 if (!editorContainer) throw new Error("Editor container not found");
33
34 const editor = new EditorView({
35 parent: editorContainer,
36 doc: `
37<style>
38 @import "./styles/base.css";
39</style>
40
41<script type="module">
42 import foundation from "~/common/foundation.js";
43</script>
44 `.trim(),
45 extensions: [
46 basicSetup,
47 langHtml(),
48 langCss(),
49 langJs(),
50 autocompletion(),
51 ],
52 });
53
54 $editor.value = editor;
55 return editor;
56}
57
58////////////////////////////////////////////
59// FORM
60////////////////////////////////////////////
61
62/**
63 * @param {EditorView} editor
64 */
65const onBuildSubmit = (editor) =>
66/**
67 * @param {Event} event
68 */
69async (event) => {
70 event.preventDefault();
71
72 const nameEl = /** @type {HTMLInputElement | null} */ (document.querySelector(
73 "#name-input",
74 ));
75
76 const descriptionEl = /** @type {HTMLTextAreaElement | null} */ (
77 document.querySelector("#description-input")
78 );
79
80 const kindEl = /** @type {HTMLSelectElement | null} */ (
81 document.querySelector("#kind-input")
82 );
83
84 const html = editor.state.doc.toString();
85 const cid = await CID.create(0x55, new TextEncoder().encode(html));
86 const name = nameEl?.value ?? "nameless";
87 const description = descriptionEl?.value ?? "";
88 const kind =
89 /** @type {"interactive" | "prelude"} */ (kindEl?.value ?? "interactive");
90
91 /** @type {Facet} */
92 const facet = $editingFacet.value
93 ? {
94 ...$editingFacet.value,
95 cid,
96 description,
97 html,
98 kind,
99 name,
100 }
101 : {
102 $type: "sh.diffuse.output.facet",
103 id: TID.now(),
104 cid,
105 description,
106 html,
107 kind,
108 name,
109 };
110
111 switch (/** @type {any} */ (event).submitter.name) {
112 case "save":
113 await saveFacet(facet);
114 break;
115 case "save+open":
116 await saveFacet(facet);
117 globalThis.open(`./l/?id=${facet.id}`, "blank");
118 break;
119 }
120};
121
122/**
123 * @param {Facet} ogFacet
124 */
125async function editFacet(ogFacet) {
126 const facet = { ...ogFacet };
127 const nameEl = /** @type {HTMLInputElement | null} */ (document.querySelector(
128 "#name-input",
129 ));
130
131 const descriptionEl = /** @type {HTMLTextAreaElement | null} */ (
132 document.querySelector("#description-input")
133 );
134
135 const kindEl = /** @type {HTMLSelectElement | null} */ (
136 document.querySelector("#kind-input")
137 );
138
139 if (!nameEl) return;
140
141 // Reset url — remove `id` param if not matching the facet
142 const url = new URL(location.href);
143 const id = url.searchParams.get("id");
144
145 if (id && facet.id !== id) {
146 url.searchParams.delete("id");
147 history.replaceState(null, "", url);
148 }
149
150 // Scroll to builder
151 document.querySelector("#build")?.scrollIntoView();
152
153 // Make sure HTML is loaded
154 if (!facet.html && facet.uri) {
155 const html = await loadURI(facet.uri);
156 const cid = await CID.create(0x55, new TextEncoder().encode(html));
157
158 facet.html = html;
159 facet.cid = cid;
160 }
161
162 $editingFacet.value = facet;
163 nameEl.value = facet.name;
164
165 if (kindEl) {
166 kindEl.value = facet.kind ?? "interactive";
167 }
168
169 if (descriptionEl) {
170 descriptionEl.value = facet.description ?? "";
171 }
172
173 const editor = $editor.value;
174 editor?.dispatch({
175 changes: { from: 0, to: editor.state.doc.length, insert: facet.html },
176 });
177}
178
179export function handleBuildFormSubmit() {
180 const editor = $editor.value;
181 if (!editor) return;
182
183 document.querySelector("#build-form")?.addEventListener(
184 "submit",
185 onBuildSubmit(editor),
186 );
187}
188
189////////////////////////////////////////////
190// EDIT EXAMPLES
191////////////////////////////////////////////
192
193let isListening = false;
194
195export function listenForExamplesEdit() {
196 if (isListening) return;
197 isListening = true;
198
199 document.body.addEventListener(
200 "click",
201 /**
202 * @param {MouseEvent} event
203 */
204 async (event) => {
205 const target = /** @type {HTMLElement} */ (event.target);
206 const rel = target.getAttribute("rel");
207 if (!rel) return;
208
209 const uri = target.closest("li")?.getAttribute("data-uri");
210 if (!uri) return;
211
212 const name = target.closest("li")?.getAttribute("data-name");
213 if (!name) return;
214
215 const kind = target.closest("li")?.getAttribute("data-kind") ?? undefined;
216
217 switch (rel) {
218 case "edit": {
219 const facet = await facetFromURI({ kind, name, uri }, {
220 fetchHTML: true,
221 });
222 editFacet(facet);
223 document.querySelector("#build")?.scrollIntoView();
224 break;
225 }
226 }
227 },
228 );
229}
230
231////////////////////////////////////////////
232// EDIT FACET FROM URL
233////////////////////////////////////////////
234
235export async function editFacetFromURL() {
236 const idParam = new URLSearchParams(location.search).get("id");
237
238 if (idParam) {
239 const out = await output();
240 const col = await Output.data(out.facets);
241 const facet = col.find((f) => f.id === idParam);
242 if (facet) await editFacet(facet);
243 }
244}