import { basicSetup, EditorView } from "codemirror";
import { css as langCss } from "@codemirror/lang-css";
import { html as langHtml } from "@codemirror/lang-html";
import { javascript as langJs } from "@codemirror/lang-javascript";
import { autocompletion } from "@codemirror/autocomplete";
import * as TID from "@atcute/tid";
import * as CID from "~/common/cid.js";
import * as Output from "~/common/output.js";
import { facetFromURI } from "~/common/facets/utils.js";
import { loadURI } from "~/common/loader.js";
import { signal } from "~/common/signal.js";
import { saveFacet } from "./crud.js";
import { output } from "./output.js";
/**
* @import {Facet} from "~/definitions/types.d.ts"
*/
const $editor = signal(/** @type {EditorView | null} */ (null));
const $editingFacet = signal(/** @type {Facet | null} */ (null));
////////////////////////////////////////////
// EDITOR
////////////////////////////////////////////
export function renderEditor() {
// Code editor
const editorContainer = document.body.querySelector("#html-input-container");
if (!editorContainer) throw new Error("Editor container not found");
const editor = new EditorView({
parent: editorContainer,
doc: `
`.trim(),
extensions: [
basicSetup,
langHtml(),
langCss(),
langJs(),
autocompletion(),
],
});
$editor.value = editor;
return editor;
}
////////////////////////////////////////////
// FORM
////////////////////////////////////////////
/**
* @param {EditorView} editor
*/
const onBuildSubmit = (editor) =>
/**
* @param {Event} event
*/
async (event) => {
event.preventDefault();
const nameEl = /** @type {HTMLInputElement | null} */ (document.querySelector(
"#name-input",
));
const descriptionEl = /** @type {HTMLTextAreaElement | null} */ (
document.querySelector("#description-input")
);
const kindEl = /** @type {HTMLSelectElement | null} */ (
document.querySelector("#kind-input")
);
const html = editor.state.doc.toString();
const cid = await CID.create(0x55, new TextEncoder().encode(html));
const name = nameEl?.value ?? "nameless";
const description = descriptionEl?.value ?? "";
const kind =
/** @type {"interactive" | "prelude"} */ (kindEl?.value ?? "interactive");
/** @type {Facet} */
const facet = $editingFacet.value
? {
...$editingFacet.value,
cid,
description,
html,
kind,
name,
}
: {
$type: "sh.diffuse.output.facet",
id: TID.now(),
cid,
description,
html,
kind,
name,
};
switch (/** @type {any} */ (event).submitter.name) {
case "save":
await saveFacet(facet);
break;
case "save+open":
await saveFacet(facet);
globalThis.open(`./l/?id=${facet.id}`, "blank");
break;
}
};
/**
* @param {Facet} ogFacet
*/
async function editFacet(ogFacet) {
const facet = { ...ogFacet };
const nameEl = /** @type {HTMLInputElement | null} */ (document.querySelector(
"#name-input",
));
const descriptionEl = /** @type {HTMLTextAreaElement | null} */ (
document.querySelector("#description-input")
);
const kindEl = /** @type {HTMLSelectElement | null} */ (
document.querySelector("#kind-input")
);
if (!nameEl) return;
// Reset url — remove `id` param if not matching the facet
const url = new URL(location.href);
const id = url.searchParams.get("id");
if (id && facet.id !== id) {
url.searchParams.delete("id");
history.replaceState(null, "", url);
}
// Scroll to builder
document.querySelector("#build")?.scrollIntoView();
// Make sure HTML is loaded
if (!facet.html && facet.uri) {
const html = await loadURI(facet.uri);
const cid = await CID.create(0x55, new TextEncoder().encode(html));
facet.html = html;
facet.cid = cid;
}
$editingFacet.value = facet;
nameEl.value = facet.name;
if (kindEl) {
kindEl.value = facet.kind ?? "interactive";
}
if (descriptionEl) {
descriptionEl.value = facet.description ?? "";
}
const editor = $editor.value;
editor?.dispatch({
changes: { from: 0, to: editor.state.doc.length, insert: facet.html },
});
}
export function handleBuildFormSubmit() {
const editor = $editor.value;
if (!editor) return;
document.querySelector("#build-form")?.addEventListener(
"submit",
onBuildSubmit(editor),
);
}
////////////////////////////////////////////
// EDIT EXAMPLES
////////////////////////////////////////////
let isListening = false;
export function listenForExamplesEdit() {
if (isListening) return;
isListening = true;
document.body.addEventListener(
"click",
/**
* @param {MouseEvent} event
*/
async (event) => {
const target = /** @type {HTMLElement} */ (event.target);
const rel = target.getAttribute("rel");
if (!rel) return;
const uri = target.closest("li")?.getAttribute("data-uri");
if (!uri) return;
const name = target.closest("li")?.getAttribute("data-name");
if (!name) return;
const kind = target.closest("li")?.getAttribute("data-kind") ?? undefined;
switch (rel) {
case "edit": {
const facet = await facetFromURI({ kind, name, uri }, {
fetchHTML: true,
});
editFacet(facet);
document.querySelector("#build")?.scrollIntoView();
break;
}
}
},
);
}
////////////////////////////////////////////
// EDIT FACET FROM URL
////////////////////////////////////////////
export async function editFacetFromURL() {
const idParam = new URLSearchParams(location.search).get("id");
if (idParam) {
const out = await output();
const col = await Output.data(out.facets);
const facet = col.find((f) => f.id === idParam);
if (facet) await editFacet(facet);
}
}