Blog attempt 5
at trunk 206 lines 5.6 kB view raw
1const inputTitle = document.querySelector('input[name="title"]'); 2const inputSlug = document.querySelector('input[name="slug"]'); 3const inputDt = document.querySelector('input[name="creation_datetime"]'); 4const inputSubtitle = document.querySelector('input[name="subtitle"]'); 5const inputCategory = document.querySelector('input[name="category"]'); 6const inputBskyUri = document.querySelector('input[name="bsky_uri"]'); 7const editor = document.querySelector("#editor"); 8const editorPreview = document.querySelector("#editorPreview"); 9 10function preview(render) { 11 if (render !== false) { 12 editorPreview.classList.remove("error"); 13 editorPreview.innerHTML = ` 14 <h1 class="blog-head">${inputTitle.value}</h1> 15 <span class="blog-subhead"><em>${inputSubtitle.value}</em></span> 16 <p class='blog-publish'> 17 🕒 ${inputDt.value} 18 </p> 19 <hr class='frontmatter'> 20 `; 21 editorPreview.innerHTML += render; 22 } else { 23 editorPreview.classList.add("error"); 24 } 25} 26 27// Enhanced editor features 28 29const handleTabs = (el) => { 30 const TAB = " "; 31 32 el.addEventListener("keydown", (e) => { 33 const { key, shiftKey, target } = e; 34 const { value, selectionStart, selectionEnd } = target; 35 36 if (key === "Tab") { 37 e.preventDefault(); 38 const startLine = value.lastIndexOf("\n", selectionStart - 1) + 1; 39 const endLine = value.indexOf("\n", selectionEnd); 40 const endIndex = endLine === -1 ? value.length : endLine; 41 const lines = value.slice(startLine, endIndex).split("\n"); 42 43 if (shiftKey) { 44 let removed = 0; 45 const updatedLines = lines.map((line) => { 46 if (line.startsWith(TAB)) { 47 removed += TAB.length; 48 return line.slice(TAB.length); 49 } 50 return line; 51 }); 52 target.value = 53 value.slice(0, startLine) + 54 updatedLines.join("\n") + 55 value.slice(endIndex); 56 if (removed > 0) { 57 target.selectionStart = Math.max(selectionStart - TAB.length, 0); 58 target.selectionEnd = Math.max( 59 selectionEnd - removed, 60 target.selectionStart, 61 ); 62 } 63 } else { 64 const updatedLines = lines.map((line) => TAB + line); 65 target.value = 66 value.slice(0, startLine) + 67 updatedLines.join("\n") + 68 value.slice(endIndex); 69 const shiftAmount = lines.length * TAB.length; 70 target.selectionStart = selectionStart + TAB.length; 71 target.selectionEnd = selectionEnd + shiftAmount; 72 } 73 74 updatePreview(false); 75 } 76 }); 77}; 78 79const handleEnterIndent = (el) => { 80 el.addEventListener("keydown", (e) => { 81 if (e.key !== "Enter") return; 82 e.preventDefault(); 83 84 const { value, selectionStart, selectionEnd } = el; 85 const before = value.slice(0, selectionStart); 86 const after = value.slice(selectionEnd); 87 const lineStart = before.lastIndexOf("\n") + 1; 88 const currentLine = before.slice(lineStart); 89 const match = currentLine.match(/^\s*/); 90 const indent = match ? match[0] : ""; 91 92 el.value = before + "\n" + indent + after; 93 const caretPos = selectionStart + 1 + indent.length; 94 el.selectionStart = el.selectionEnd = caretPos; 95 96 updatePreview(false); 97 }); 98}; 99 100const handleBackspace = (el) => { 101 const TAB = " "; 102 el.addEventListener("keydown", (e) => { 103 if (e.key !== "Backspace") return; 104 const { value, selectionStart, selectionEnd } = el; 105 const prevFour = value.slice(selectionStart - 4, selectionStart); 106 if (prevFour === TAB) { 107 e.preventDefault(); 108 el.value = value.slice(0, selectionStart - 4) + value.slice(selectionEnd); 109 el.selectionStart = el.selectionEnd = selectionStart - 4; 110 111 updatePreview(false); 112 } 113 }); 114}; 115 116const VOID_ELEMENTS = new Set([ 117 "area", 118 "base", 119 "br", 120 "col", 121 "embed", 122 "hr", 123 "img", 124 "input", 125 "link", 126 "meta", 127 "param", 128 "source", 129 "track", 130 "wbr", 131]); 132 133const enableEditorFeatures = (el) => { 134 if (!el) return; 135 el.value = el.value.replace(/\t/g, " "); 136 137 handleTabs(el); 138 handleEnterIndent(el); 139 handleBackspace(el); 140}; 141 142async function syncDraft() { 143 const resp = await fetch(`/blog/new/sync`, { 144 method: "POST", 145 headers: { "Content-Type": "application/json" }, 146 body: JSON.stringify({ 147 title: inputTitle.value, 148 slug: inputSlug.value, 149 contents: editor.value, 150 creation_datetime: inputDt.value, 151 subtitle: inputSubtitle.value || null, 152 category: inputCategory.value || null, 153 bsky_uri: inputBskyUri.value || null, 154 }), 155 }); 156 157 if (resp.ok) { 158 return await resp.text(); 159 } else { 160 return false 161 } 162} 163 164 165async function renderDraft() { 166 const resp = await fetch(`/blog/edit/render`, { 167 method: "POST", 168 headers: { "Content-Type": "application/json" }, 169 body: JSON.stringify({ 170 contents: editor.value, 171 }), 172 }); 173 174 if (resp.ok) { 175 return await resp.text(); 176 } else { 177 return false 178 } 179} 180 181async function updatePreview(sync = false) { 182 if (sync) { 183 let render = await syncDraft(); 184 preview(render); 185 } else { 186 let render = await renderDraft(); 187 preview(render); 188 } 189 if (typeof createFootnotes === "function") createFootnotes(); 190} 191 192// --- Entry point --- 193function initPostEditor(mode = "edit") { 194 enableEditorFeatures(document.querySelector("#editor")); 195 196 const sync = mode === "new"; 197 198 [editor, inputTitle, inputSubtitle, inputSlug, inputDt].forEach((el) => 199 el?.addEventListener("input", () => updatePreview(sync)), 200 ); 201 202 updatePreview(false) 203} 204 205// Export the initializer 206export { initPostEditor };