// Bundled to f/js/app.js by esbuild (see the Justfile). Importing htmx here
// registers it globally and processes hx-* attributes on load, which is what
// powers hx-boost navigation and the live card preview.
import "htmx.org";
// Small toast helper available as window.toast(msg, {kind}) for ad-hoc use.
window.toast = (msg, opts = {}) => {
let host = document.getElementById("toast-host");
if (!host) {
host = document.createElement("div");
host.id = "toast-host";
host.className = "toast toast-end z-50";
document.body.appendChild(host);
}
const alert = document.createElement("div");
alert.className = `alert alert-${opts.kind || "info"} shadow-lg`;
const span = document.createElement("span");
span.textContent = msg;
alert.appendChild(span);
host.appendChild(alert);
const ttl = opts.ttl ?? 4000;
if (ttl > 0) setTimeout(() => alert.remove(), ttl);
return alert;
};
// Code textareas take Tab as a character instead of moving focus (Esc then
// Tab still leaves). execCommand keeps native undo and fires `input`, which
// is what re-renders the editor preview.
let escaped = false;
document.addEventListener("keydown", (e) => {
const el = e.target;
if (!(el instanceof HTMLTextAreaElement) || !el.hasAttribute("data-code-input")) return;
if (e.key === "Escape") { escaped = true; return; }
if (e.key !== "Tab" || e.shiftKey || e.ctrlKey || e.altKey || e.metaKey) { escaped = false; return; }
if (escaped) { escaped = false; return; }
e.preventDefault();
if (!document.execCommand("insertText", false, "\t")) {
el.setRangeText("\t", el.selectionStart, el.selectionEnd, "end");
el.dispatchEvent(new Event("input", { bubbles: true }));
}
});
// In-window editing. The preview SVG says where its code grid sits
// (data-code-x/-y, line height, font size, all in SVG units); a transparent,
// caret-only textarea is laid exactly over that grid at the SVG's current
// scale. Typing there mirrors into the real (named) code box, whose `input`
// re-renders the preview underneath; highlighted text shows through.
function placeInWindowEditor(preview) {
const svg = preview.querySelector("#preview-inner svg");
const ta = preview.querySelector("textarea[data-in-window]");
if (!svg || !ta || !svg.dataset.codeX) return;
const vb = svg.viewBox.baseVal;
const r = svg.getBoundingClientRect();
const p = preview.getBoundingClientRect();
if (!vb || !vb.width || !r.width) return;
const k = r.width / vb.width;
const d = svg.dataset;
const x = +d.codeX;
const y = +d.codeY;
// Offsets inside #preview's padding box, which positions the textarea.
const ox = r.left - p.left - preview.clientLeft + preview.scrollLeft;
const oy = r.top - p.top - preview.clientTop + preview.scrollTop;
Object.assign(ta.style, {
left: `${ox + x * k}px`,
top: `${oy + y * k}px`,
// Reach to the SVG's far edges, not just the code area, so text typed
// past the current width or height stays in view until the window grows.
width: `${(vb.width - x) * k}px`,
height: `${(vb.height - y) * k}px`,
fontSize: `${+d.fontSize * k}px`,
lineHeight: `${+d.lineHeight * k}px`,
caretColor: d.codeFg,
});
ta.scrollLeft = 0;
ta.scrollTop = 0;
}
function initInWindowEditing(root) {
const preview = root.querySelector ? root.querySelector("#preview") || (root.id === "preview" ? root : null) : null;
if (!preview || preview.dataset.inWindowReady) return;
const ta = preview.querySelector("textarea[data-in-window]");
const code = document.querySelector("#card-form textarea[name=code]");
if (!ta || !code) return;
preview.dataset.inWindowReady = "1";
ta.value = code.value;
ta.addEventListener("input", () => { code.value = ta.value; });
code.addEventListener("input", () => { if (ta.value !== code.value) ta.value = code.value; });
// Keep the grid pinned: never let the textarea scroll its text away from
// the rendered text under it.
ta.addEventListener("scroll", () => { ta.scrollLeft = 0; ta.scrollTop = 0; });
const place = () => placeInWindowEditor(preview);
preview.addEventListener("htmx:afterSwap", place);
preview.addEventListener("scroll", place);
window.addEventListener("resize", place);
if (window.ResizeObserver) new ResizeObserver(place).observe(preview);
document.fonts && document.fonts.ready.then(place);
place();
}
document.addEventListener("htmx:load", (e) => initInWindowEditing(e.detail.elt));
neiam
/bismuth
MOTHER-FUCKING-BISMUTH
public · Issues · Pulls · Labels · Forks · Compare · Actions success · Badges · Packages
⭐
Log in to mark this repository.
4.4 KiB · text
History
d76cd01