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 };