| @@ -8,13 +8,30 @@ const htmlPrefix = "h:"; |
| 8 |
8 |
|
| 9 |
9 |
|
| 10 |
10 |
|
| 11 |
|
-const allowedTags = new Set(["B", "STRONG", "I", "EM", "U", "UL", "OL", "LI", "DIV", "P", "BR"]); |
|
11 |
+const allowedTags = new Set([ |
|
12 |
+ "B", "STRONG", "I", "EM", "U", "UL", "OL", "LI", "DIV", "P", "BR", |
|
13 |
+ "TABLE", "THEAD", "TBODY", "TFOOT", "TR", "TH", "TD", |
|
14 |
+]); |
| 12 |
15 |
const droppedTags = new Set(["SCRIPT", "STYLE", "TITLE", "TEXTAREA"]); |
| 13 |
16 |
|
| 14 |
17 |
const input = getById("input"); |
| 15 |
18 |
const toolbar = getById("toolbar"); |
|
19 |
+const tableToggle = getById("table-toggle"); |
|
20 |
+const tableTools = getById("table-tools"); |
| 16 |
21 |
const buttons = [...toolbar.querySelectorAll("button[data-command]")]; |
| 17 |
22 |
|
|
23 |
+ |
|
24 |
+ |
|
25 |
+ |
|
26 |
+const tableActions = { |
|
27 |
+ rowAbove: cell => focusCell(insertRow(cell.parentElement, "before"), cell.cellIndex), |
|
28 |
+ rowBelow: cell => focusCell(insertRow(cell.parentElement, "after"), cell.cellIndex), |
|
29 |
+ columnLeft: cell => placeCaret(insertColumn(cell, "before")), |
|
30 |
+ columnRight: cell => placeCaret(insertColumn(cell, "after")), |
|
31 |
+ deleteRow, |
|
32 |
+ deleteColumn, |
|
33 |
+}; |
|
34 |
+ |
| 18 |
35 |
|
| 19 |
36 |
|
| 20 |
37 |
let written = null; |
| @@ -27,22 +44,44 @@ toolbar.addEventListener("mousedown", event => { |
| 27 |
44 |
if (event.target.closest("button")) event.preventDefault(); |
| 28 |
45 |
}); |
| 29 |
46 |
toolbar.addEventListener("click", event => { |
| 30 |
|
- const button = event.target.closest("button[data-command]"); |
|
47 |
+ const button = event.target.closest("button"); |
| 31 |
48 |
if (!button) return; |
| 32 |
49 |
input.focus(); |
| 33 |
|
- document.execCommand(button.dataset.command); |
|
50 |
+ const { command, table } = button.dataset; |
|
51 |
+ if (command) { |
|
52 |
+ document.execCommand(command); |
|
53 |
+ } else if (table === "toggle") { |
|
54 |
+ |
|
55 |
+ |
|
56 |
+ const cell = selectedCell(); |
|
57 |
+ if (cell) { |
|
58 |
+ removeTable(cell.closest("table")); |
|
59 |
+ } else { |
|
60 |
+ insertTable(); |
|
61 |
+ } |
|
62 |
+ changed(); |
|
63 |
+ } else if (table) { |
|
64 |
+ const cell = selectedCell(); |
|
65 |
+ if (!cell) return; |
|
66 |
+ tableActions[table](cell); |
|
67 |
+ changed(); |
|
68 |
+ } |
| 34 |
69 |
updateToolbar(); |
| 35 |
70 |
}); |
| 36 |
71 |
|
| 37 |
|
- |
| 38 |
|
- |
|
72 |
+ |
|
73 |
+ |
|
74 |
+ |
| 39 |
75 |
input.addEventListener("keydown", event => { |
| 40 |
76 |
if (event.key !== "Tab" || event.ctrlKey || event.altKey || event.metaKey) return; |
| 41 |
|
- const node = document.getSelection()?.anchorNode; |
| 42 |
|
- const element = node instanceof Element ? node : node?.parentElement; |
| 43 |
|
- if (!element?.closest("li")) return; |
| 44 |
|
- event.preventDefault(); |
| 45 |
|
- document.execCommand(event.shiftKey ? "outdent" : "indent"); |
|
77 |
+ const item = selectedElement()?.closest("li"); |
|
78 |
+ const cell = selectedCell(); |
|
79 |
+ if (item && (!cell || cell.contains(item))) { |
|
80 |
+ event.preventDefault(); |
|
81 |
+ document.execCommand(event.shiftKey ? "outdent" : "indent"); |
|
82 |
+ } else if (cell && moveFromCell(cell, event.shiftKey ? -1 : 1)) { |
|
83 |
+ event.preventDefault(); |
|
84 |
+ } |
| 46 |
85 |
}); |
| 47 |
86 |
|
| 48 |
87 |
|
| @@ -95,6 +134,206 @@ function updateToolbar() { |
| 95 |
134 |
for (const button of buttons) { |
| 96 |
135 |
button.setAttribute("aria-pressed", String(document.queryCommandState(button.dataset.command))); |
| 97 |
136 |
} |
|
137 |
+ const inTable = selectedCell() !== null; |
|
138 |
+ tableToggle.setAttribute("aria-pressed", String(inTable)); |
|
139 |
+ tableToggle.title = inTable ? "Delete this table" : "Insert a table"; |
|
140 |
+ tableTools.hidden = !inTable; |
|
141 |
+} |
|
142 |
+ |
|
143 |
+ |
|
144 |
+function changed() { |
|
145 |
+ input.dispatchEvent(new Event("input")); |
|
146 |
+} |
|
147 |
+ |
|
148 |
+ |
|
149 |
+function selectedElement() { |
|
150 |
+ const node = document.getSelection()?.anchorNode; |
|
151 |
+ return node instanceof Element ? node : node?.parentElement ?? null; |
|
152 |
+} |
|
153 |
+ |
|
154 |
+ |
|
155 |
+function selectedCell() { |
|
156 |
+ const cell = selectedElement()?.closest("td, th"); |
|
157 |
+ return cell && input.contains(cell) ? cell : null; |
|
158 |
+} |
|
159 |
+ |
|
160 |
+ |
|
161 |
+function placeCaret(node) { |
|
162 |
+ const range = document.createRange(); |
|
163 |
+ range.selectNodeContents(node); |
|
164 |
+ range.collapse(true); |
|
165 |
+ const selection = document.getSelection(); |
|
166 |
+ selection.removeAllRanges(); |
|
167 |
+ selection.addRange(range); |
|
168 |
+} |
|
169 |
+ |
|
170 |
+ |
|
171 |
+ |
|
172 |
+ |
|
173 |
+ |
|
174 |
+function insertTable() { |
|
175 |
+ const table = h("table", [h("tbody", [newRow(2), newRow(2)])]); |
|
176 |
+ const line = currentLine(); |
|
177 |
+ if (line === null) { |
|
178 |
+ input.prepend(table); |
|
179 |
+ } else if (line instanceof Element && ["DIV", "P"].includes(line.tagName) |
|
180 |
+ && line.textContent === '' && !line.querySelector("table")) { |
|
181 |
+ line.replaceWith(table); |
|
182 |
+ } else { |
|
183 |
+ endOfLine(line).after(table); |
|
184 |
+ } |
|
185 |
+ if (!table.nextSibling) { |
|
186 |
+ table.after(h("div", [h("br")])); |
|
187 |
+ } |
|
188 |
+ placeCaret(table.rows[0].cells[0]); |
|
189 |
+} |
|
190 |
+ |
|
191 |
+ |
|
192 |
+ |
|
193 |
+ |
|
194 |
+ |
|
195 |
+function currentLine() { |
|
196 |
+ const selection = document.getSelection(); |
|
197 |
+ let node = selection?.anchorNode; |
|
198 |
+ if (!node || !input.contains(node)) return null; |
|
199 |
+ if (node === input) return input.childNodes[selection.anchorOffset - 1] ?? null; |
|
200 |
+ while (node.parentNode !== input) node = node.parentNode; |
|
201 |
+ return node; |
|
202 |
+} |
|
203 |
+ |
|
204 |
+ |
|
205 |
+ |
|
206 |
+ |
|
207 |
+ |
|
208 |
+ |
|
209 |
+function endOfLine(node) { |
|
210 |
+ const isBlock = n => n instanceof Element && ["DIV", "P", "UL", "OL", "TABLE"].includes(n.tagName); |
|
211 |
+ while (!isBlock(node) && node.nodeName !== "BR" && node.nextSibling && !isBlock(node.nextSibling)) { |
|
212 |
+ node = node.nextSibling; |
|
213 |
+ } |
|
214 |
+ return node; |
|
215 |
+} |
|
216 |
+ |
|
217 |
+ |
|
218 |
+function newCell(tag = "td") { |
|
219 |
+ return h(tag, [h("br")]); |
|
220 |
+} |
|
221 |
+ |
|
222 |
+ |
|
223 |
+function newRow(count) { |
|
224 |
+ return h("tr", Array.from({ length: count }, () => newCell())); |
|
225 |
+} |
|
226 |
+ |
|
227 |
+ |
|
228 |
+ |
|
229 |
+ |
|
230 |
+ |
|
231 |
+function columnCount(table) { |
|
232 |
+ return Math.max(...[...table.rows].map(row => row.cells.length)); |
|
233 |
+} |
|
234 |
+ |
|
235 |
+ |
|
236 |
+ |
|
237 |
+ |
|
238 |
+ |
|
239 |
+function insertRow(row, where) { |
|
240 |
+ const created = newRow(columnCount(row.closest("table"))); |
|
241 |
+ row[where](created); |
|
242 |
+ return created; |
|
243 |
+} |
|
244 |
+ |
|
245 |
+ |
|
246 |
+ |
|
247 |
+ |
|
248 |
+ |
|
249 |
+ |
|
250 |
+function insertColumn(cell, where) { |
|
251 |
+ const index = cell.cellIndex; |
|
252 |
+ for (const row of cell.closest("table").rows) { |
|
253 |
+ const reference = row.cells[index]; |
|
254 |
+ if (reference) { |
|
255 |
+ |
|
256 |
+ reference[where](newCell(reference.localName)); |
|
257 |
+ } else { |
|
258 |
+ row.append(newCell()); |
|
259 |
+ } |
|
260 |
+ } |
|
261 |
+ return where === "before" ? cell.previousElementSibling : cell.nextElementSibling; |
|
262 |
+} |
|
263 |
+ |
|
264 |
+ |
|
265 |
+function deleteRow(cell) { |
|
266 |
+ const row = cell.parentElement; |
|
267 |
+ const table = row.closest("table"); |
|
268 |
+ const rows = [...table.rows]; |
|
269 |
+ const neighbour = rows[rows.indexOf(row) + 1] ?? rows[rows.indexOf(row) - 1]; |
|
270 |
+ row.remove(); |
|
271 |
+ if (neighbour) { |
|
272 |
+ focusCell(neighbour, cell.cellIndex); |
|
273 |
+ } else { |
|
274 |
+ removeTable(table); |
|
275 |
+ } |
|
276 |
+} |
|
277 |
+ |
|
278 |
+ |
|
279 |
+function deleteColumn(cell) { |
|
280 |
+ const index = cell.cellIndex; |
|
281 |
+ const row = cell.parentElement; |
|
282 |
+ const table = row.closest("table"); |
|
283 |
+ for (const r of [...table.rows]) { |
|
284 |
+ r.cells[index]?.remove(); |
|
285 |
+ if (r.cells.length === 0) r.remove(); |
|
286 |
+ } |
|
287 |
+ if (table.rows.length === 0) { |
|
288 |
+ removeTable(table); |
|
289 |
+ } else { |
|
290 |
+ focusCell(row.isConnected ? row : table.rows[0], index); |
|
291 |
+ } |
|
292 |
+} |
|
293 |
+ |
|
294 |
+ |
|
295 |
+ |
|
296 |
+ |
|
297 |
+ |
|
298 |
+ |
|
299 |
+function removeTable(table) { |
|
300 |
+ const next = table.nextElementSibling; |
|
301 |
+ if (next?.tagName === "DIV" && next.textContent === '' && !next.querySelector("table")) { |
|
302 |
+ table.remove(); |
|
303 |
+ placeCaret(next); |
|
304 |
+ } else { |
|
305 |
+ const line = h("div", [h("br")]); |
|
306 |
+ table.replaceWith(line); |
|
307 |
+ placeCaret(line); |
|
308 |
+ } |
|
309 |
+} |
|
310 |
+ |
|
311 |
+ |
|
312 |
+ |
|
313 |
+ |
|
314 |
+ |
|
315 |
+function focusCell(row, index) { |
|
316 |
+ placeCaret(row.cells[Math.min(index, row.cells.length - 1)]); |
|
317 |
+} |
|
318 |
+ |
|
319 |
+ |
|
320 |
+ |
|
321 |
+ |
|
322 |
+ |
|
323 |
+ |
|
324 |
+function moveFromCell(cell, step) { |
|
325 |
+ const table = cell.closest("table"); |
|
326 |
+ const cells = [...table.rows].flatMap(row => [...row.cells]); |
|
327 |
+ const next = cells[cells.indexOf(cell) + step]; |
|
328 |
+ if (next) { |
|
329 |
+ placeCaret(next); |
|
330 |
+ } else if (step > 0) { |
|
331 |
+ focusCell(insertRow(cell.parentElement, "after"), 0); |
|
332 |
+ changed(); |
|
333 |
+ } else { |
|
334 |
+ return false; |
|
335 |
+ } |
|
336 |
+ return true; |
| 98 |
337 |
} |
| 99 |
338 |
|
| 100 |
339 |
/** |