A music player that connects to your cloud/distributed storage.
at v4 244 lines 6.0 kB view raw
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}