This repository has no description
0

Configure Feed

Select the types of activity you want to include in your feed.

core / appview / pages / static / markdown-editor.js
5.5 kB 174 lines
1export default class TangledMarkdownEditor extends HTMLElement { 2 static tag = "markdown-editor"; 3 4 static define(tag = this.tag) { 5 this.tag = tag; 6 7 const name = customElements.getName(this); 8 if (name && name !== tag) return console.warn(`${this.name} already defined as <${name}>!`); 9 10 const ce = customElements.get(tag); 11 if (ce && ce !== this) return console.warn(`${tag} already defined as ${ce.name}!`); 12 13 customElements.define(tag, this); 14 } 15 16 static { 17 const tag = new URL(import.meta.url).searchParams.get("tag") || this.tag; 18 if (tag != "none") this.define(tag); 19 } 20 21 #dragHoverClass = "drag-hover"; 22 23 constructor() { 24 super(); 25 this.textarea = this.querySelector("textarea"); 26 if (!this.textarea) { 27 console.error("textarea is missing in markdown-editor"); 28 return; 29 } 30 31 this.querySelectorAll('[data-md-mode]').forEach(btn => { 32 btn.addEventListener("click", () => { 33 const mode = btn.dataset.mdMode; 34 this.querySelectorAll('[data-md-panel]').forEach(p => { 35 p.classList.toggle('hidden', p.dataset.mdPanel !== mode); 36 }); 37 this.querySelectorAll('[data-md-mode]').forEach(b => { 38 b.classList.toggle('active', b === btn); 39 }); 40 }); 41 }); 42 43 // TODO: blob upload support 44 // this.textarea.addEventListener("paste", (ev) => this.#onPaste(ev)); 45 // this.textarea.addEventListener("dragover", (ev) => this.#onDragOver(ev)); 46 // this.textarea.addEventListener("dragleave", (ev) => this.#onDragLeave(ev)); 47 // this.textarea.addEventListener("drop", (ev) => this.#onDrop(ev)); 48 } 49 50 async insertFile() { 51 const input = document.createElement("input"); 52 input.type = "file"; 53 input.accept = "image/*"; 54 input.multiple = true; 55 input.style.display = "none"; 56 input.addEventListener("change", () => { 57 if (!input.files) return; 58 for (const file of input.files) { 59 this.#handleFile(file); 60 } 61 }); 62 this.appendChild(input); 63 input.click(); 64 this.removeChild(input); 65 } 66 67 /** @param {ClipboardEvent} ev */ 68 async #onPaste(ev) { 69 const dt = ev.clipboardData; 70 if (!dt || !dt.files || dt.files.length === 0) return; 71 72 ev.preventDefault(); 73 74 for (const file of dt.files) { 75 if (!file.type.startsWith("image/")) continue; 76 77 await this.#handleFile(file); 78 } 79 } 80 81 /** @param {DragEvent} ev */ 82 async #onDragOver(ev) { 83 ev.preventDefault(); 84 this.classList.add(this.#dragHoverClass); 85 } 86 87 /** @param {DragEvent} ev */ 88 async #onDragLeave(ev) { 89 ev.preventDefault(); 90 this.classList.remove(this.#dragHoverClass); 91 } 92 93 /** @param {DragEvent} ev */ 94 async #onDrop(ev) { 95 this.classList.remove(this.#dragHoverClass); 96 97 const dt = ev.dataTransfer; 98 if (!dt || !dt.files || dt.files.length === 0) return; 99 100 ev.preventDefault(); 101 102 for (const file of dt.files) { 103 if (!file.type.startsWith("image/")) continue; 104 105 await this.#handleFile(file); 106 } 107 } 108 109 /** @param {File} file */ 110 async #handleFile(file) { 111 const textarea = this.textarea; 112 if (!textarea) return; 113 114 const placeholder = `<!-- Uploading "${file.name}"... -->`; 115 116 this.#insertTextAtCursor(placeholder); 117 118 let blob; 119 try { 120 blob = await this.#upload(file); 121 } catch (e) { 122 console.error("failed to upload blob", e) 123 textarea.value = textarea.value.replace(placeholder, `<!-- Failed to upload "${file.name}". -->`); 124 return 125 } 126 127 // TODO: insert blob itself to form 128 129 const cid = blob.ref["$link"] 130 textarea.value = textarea.value.replace(placeholder, `![Image](blob://${cid})`); 131 } 132 133 /** @param {string} text */ 134 #insertTextAtCursor(text) { 135 const textarea = this.textarea; 136 if (!textarea) return; 137 const start = textarea.selectionStart; 138 const end = textarea.selectionEnd; 139 140 const before = textarea.value.slice(0, start); 141 const after = textarea.value.slice(end); 142 143 // add surrounding newlines if it's mid-line 144 if (before && !before.endsWith("\n")) text = "\n\n" + text; 145 if (after && !after.startsWith("\n")) text = text + "\n\n"; 146 147 textarea.value = before + text + after; 148 149 const newPos = start + text.length; 150 textarea.selectionStart = textarea.selectionEnd = newPos; 151 152 textarea.dispatchEvent( 153 new InputEvent("input", { bubbles: true, inputType: "insertText", data: text }) 154 ); 155 // textarea.dispatchEvent(new Event("input", { bubbles: true })); 156 textarea.dispatchEvent(new Event("change", { bubbles: true })); 157 } 158 159 /** @param {File} file */ 160 async #upload(file) { 161 await new Promise(r => setTimeout(r, 500)); 162 163 const host = this.getAttribute("host") ?? ""; 164 const res = await fetch(host + "/xrpc/com.atproto.repo.uploadBlob", { 165 method: "POST", 166 body: file, 167 headers: { 168 "Content-Type": file.type, 169 }, 170 }); 171 const output = await res.json(); 172 return output.blob; 173 } 174}