Something went wrong. Try again.
Monorepo for Tangled tangled.org
Something went wrong. Try again.
JavaScript
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175export default class TangledMarkdownEditor extends HTMLElement { static tag = "markdown-editor";
static define(tag = this.tag) { this.tag = tag;
const name = customElements.getName(this); if (name && name !== tag) return console.warn(`${this.name} already defined as <${name}>!`);
const ce = customElements.get(tag); if (ce && ce !== this) return console.warn(`${tag} already defined as ${ce.name}!`);
customElements.define(tag, this); }
static { const tag = new URL(import.meta.url).searchParams.get("tag") || this.tag; if (tag != "none") this.define(tag); }
#dragHoverClass = "drag-hover";
constructor() { super(); this.textarea = this.querySelector("textarea"); if (!this.textarea) { console.error("textarea is missing in markdown-editor"); return; }
this.querySelectorAll('[data-md-mode]').forEach(btn => { btn.addEventListener("click", () => { const mode = btn.dataset.mdMode; this.querySelectorAll('[data-md-panel]').forEach(p => { p.classList.toggle('hidden', p.dataset.mdPanel !== mode); }); this.querySelectorAll('[data-md-mode]').forEach(b => { b.classList.toggle('active', b === btn); }); }); });
// TODO: blob upload support // this.textarea.addEventListener("paste", (ev) => this.#onPaste(ev)); // this.textarea.addEventListener("dragover", (ev) => this.#onDragOver(ev)); // this.textarea.addEventListener("dragleave", (ev) => this.#onDragLeave(ev)); // this.textarea.addEventListener("drop", (ev) => this.#onDrop(ev)); }
async insertFile() { const input = document.createElement("input"); input.type = "file"; input.accept = "image/*"; input.multiple = true; input.style.display = "none"; input.addEventListener("change", () => { if (!input.files) return; for (const file of input.files) { this.#handleFile(file); } }); this.appendChild(input); input.click(); this.removeChild(input); }
/** @param {ClipboardEvent} ev */ async #onPaste(ev) { const dt = ev.clipboardData; if (!dt || !dt.files || dt.files.length === 0) return;
ev.preventDefault();
for (const file of dt.files) { if (!file.type.startsWith("image/")) continue;
await this.#handleFile(file); } }
/** @param {DragEvent} ev */ async #onDragOver(ev) { ev.preventDefault(); this.classList.add(this.#dragHoverClass); }
/** @param {DragEvent} ev */ async #onDragLeave(ev) { ev.preventDefault(); this.classList.remove(this.#dragHoverClass); }
/** @param {DragEvent} ev */ async #onDrop(ev) { this.classList.remove(this.#dragHoverClass);
const dt = ev.dataTransfer; if (!dt || !dt.files || dt.files.length === 0) return;
ev.preventDefault();
for (const file of dt.files) { if (!file.type.startsWith("image/")) continue;
await this.#handleFile(file); } }
/** @param {File} file */ async #handleFile(file) { const textarea = this.textarea; if (!textarea) return;
const placeholder = `<!-- Uploading "${file.name}"... -->`;
this.#insertTextAtCursor(placeholder);
let blob; try { blob = await this.#upload(file); } catch (e) { console.error("failed to upload blob", e) textarea.value = textarea.value.replace(placeholder, `<!-- Failed to upload "${file.name}". -->`); return }
// TODO: insert blob itself to form
const cid = blob.ref["$link"] textarea.value = textarea.value.replace(placeholder, ``); }
/** @param {string} text */ #insertTextAtCursor(text) { const textarea = this.textarea; if (!textarea) return; const start = textarea.selectionStart; const end = textarea.selectionEnd;
const before = textarea.value.slice(0, start); const after = textarea.value.slice(end);
// add surrounding newlines if it's mid-line if (before && !before.endsWith("\n")) text = "\n\n" + text; if (after && !after.startsWith("\n")) text = text + "\n\n";
textarea.value = before + text + after;
const newPos = start + text.length; textarea.selectionStart = textarea.selectionEnd = newPos;
textarea.dispatchEvent( new InputEvent("input", { bubbles: true, inputType: "insertText", data: text }) ); // textarea.dispatchEvent(new Event("input", { bubbles: true })); textarea.dispatchEvent(new Event("change", { bubbles: true })); }
/** @param {File} file */ async #upload(file) { await new Promise(r => setTimeout(r, 500));
const host = this.getAttribute("host") ?? ""; const res = await fetch(host + "/xrpc/com.atproto.repo.uploadBlob", { method: "POST", body: file, headers: { "Content-Type": file.type, }, }); const output = await res.json(); return output.blob; }}