@anweb/nuxt-aneditor 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/README.md +81 -0
  2. package/dist/module.d.mts +14 -0
  3. package/dist/module.d.ts +14 -0
  4. package/dist/module.json +9 -0
  5. package/dist/module.mjs +29 -0
  6. package/dist/runtime/assets/icons/blockquote.svg +1 -0
  7. package/dist/runtime/assets/icons/bold.svg +1 -0
  8. package/dist/runtime/assets/icons/code-block.svg +1 -0
  9. package/dist/runtime/assets/icons/code.svg +1 -0
  10. package/dist/runtime/assets/icons/edit.svg +1 -0
  11. package/dist/runtime/assets/icons/expand.svg +1 -0
  12. package/dist/runtime/assets/icons/heading.svg +1 -0
  13. package/dist/runtime/assets/icons/horizontal-rule.svg +1 -0
  14. package/dist/runtime/assets/icons/image.svg +1 -0
  15. package/dist/runtime/assets/icons/italic.svg +1 -0
  16. package/dist/runtime/assets/icons/link.svg +1 -0
  17. package/dist/runtime/assets/icons/ordered-list.svg +1 -0
  18. package/dist/runtime/assets/icons/redo.svg +1 -0
  19. package/dist/runtime/assets/icons/remove.svg +1 -0
  20. package/dist/runtime/assets/icons/strikethrough.svg +1 -0
  21. package/dist/runtime/assets/icons/table.svg +1 -0
  22. package/dist/runtime/assets/icons/task-list.svg +1 -0
  23. package/dist/runtime/assets/icons/undo.svg +1 -0
  24. package/dist/runtime/assets/icons/unordered-list.svg +1 -0
  25. package/dist/runtime/assets/icons/upload.svg +1 -0
  26. package/dist/runtime/assets/icons/youtube.svg +1 -0
  27. package/dist/runtime/components/AnEditor/Editor.vue +630 -0
  28. package/dist/runtime/components/AnEditor/Editor.vue.d.ts +2 -0
  29. package/dist/runtime/components/AnEditor/Prompt.vue +50 -0
  30. package/dist/runtime/components/AnEditor/Prompt.vue.d.ts +2 -0
  31. package/dist/runtime/components/AnEditor/Toolbar.vue +191 -0
  32. package/dist/runtime/components/AnEditor/Toolbar.vue.d.ts +2 -0
  33. package/dist/runtime/components/AnEditor/Viewer.vue +16 -0
  34. package/dist/runtime/components/AnEditor/Viewer.vue.d.ts +2 -0
  35. package/dist/runtime/composables/useBlocks.d.ts +15 -0
  36. package/dist/runtime/composables/useBlocks.js +258 -0
  37. package/dist/runtime/composables/useHistory.d.ts +12 -0
  38. package/dist/runtime/composables/useHistory.js +56 -0
  39. package/dist/runtime/composables/useImage.d.ts +27 -0
  40. package/dist/runtime/composables/useImage.js +81 -0
  41. package/dist/runtime/composables/useList.d.ts +10 -0
  42. package/dist/runtime/composables/useList.js +116 -0
  43. package/dist/runtime/composables/useSelection.d.ts +20 -0
  44. package/dist/runtime/composables/useSelection.js +92 -0
  45. package/dist/runtime/composables/useTable.d.ts +29 -0
  46. package/dist/runtime/composables/useTable.js +175 -0
  47. package/dist/runtime/types/global.d.ts +8 -0
  48. package/dist/runtime/types/index.d.ts +1 -0
  49. package/dist/runtime/types/index.js +1 -0
  50. package/dist/runtime/utils/index.d.ts +3 -0
  51. package/dist/runtime/utils/index.js +3 -0
  52. package/dist/runtime/utils/parseMarkdown.d.ts +1 -0
  53. package/dist/runtime/utils/parseMarkdown.js +184 -0
  54. package/dist/runtime/utils/toMarkdown.d.ts +1 -0
  55. package/dist/runtime/utils/toMarkdown.js +233 -0
  56. package/dist/runtime/utils/youtube.d.ts +1 -0
  57. package/dist/runtime/utils/youtube.js +6 -0
  58. package/dist/types.d.mts +3 -0
  59. package/package.json +50 -0
@@ -0,0 +1,630 @@
1
+ <script setup>
2
+ import { ref, computed, watch, onMounted } from "vue";
3
+ import { useEventListener, useFileDialog, useDebounceFn, useThrottleFn } from "@vueuse/core";
4
+ import { useTemplateRef, parseMarkdown, toMarkdown, extractYouTubeId, useAnDialogs, useSelection, useHistory, useBlocks, useTable, useImage, useList } from "#imports";
5
+ import { AnEditorToolbar, AnEditorPrompt } from "#components";
6
+ const emit = defineEmits(["update:modelValue"]);
7
+ const props = defineProps({
8
+ modelValue: { type: String, required: false, default: "" },
9
+ placeholder: { type: String, required: false, default: "" },
10
+ upload: { type: Function, required: false },
11
+ disabled: { type: Boolean, required: false, default: false }
12
+ });
13
+ const Dialogs = useAnDialogs();
14
+ const FileDialog = useFileDialog({ accept: "image/*", multiple: false });
15
+ const refContent = useTemplateRef("refContent");
16
+ const activeFormats = ref(/* @__PURE__ */ new Set());
17
+ const isComposing = ref(false);
18
+ let skipNextUpdate = false;
19
+ let internalMd = "";
20
+ let wasEmpty = false;
21
+ const selection = useSelection(refContent);
22
+ const flushModel = useDebounceFn(() => {
23
+ if (isComposing.value) return;
24
+ if (history.isUndoRedo) return;
25
+ history.pushHistory();
26
+ const html = refContent.value?.innerHTML ?? "";
27
+ const md = toMarkdown(html);
28
+ internalMd = md;
29
+ emit("update:modelValue", md);
30
+ updateActiveFormats();
31
+ }, 150);
32
+ const syncToModel = () => {
33
+ if (isComposing.value || skipNextUpdate) {
34
+ skipNextUpdate = false;
35
+ return;
36
+ }
37
+ if (history.isUndoRedo) return;
38
+ blocks.normalizeContent();
39
+ cleanOrphanedFormatting();
40
+ flushModel();
41
+ };
42
+ const history = useHistory(refContent, selection);
43
+ const blocks = useBlocks(refContent, selection, syncToModel);
44
+ const table = useTable(refContent, syncToModel);
45
+ const image = useImage(refContent, syncToModel);
46
+ const list = useList(refContent, selection, syncToModel, blocks.ensureBlockWrapped, blocks.isInsideCodeBlock);
47
+ const focus = () => {
48
+ refContent.value?.focus();
49
+ };
50
+ const clear = () => {
51
+ if (!refContent.value) return;
52
+ refContent.value.innerHTML = "";
53
+ internalMd = "";
54
+ emit("update:modelValue", "");
55
+ };
56
+ const resetTypingStyle = () => {
57
+ if (!refContent.value) return;
58
+ const text = refContent.value.textContent ?? "";
59
+ if (text.trim()) return;
60
+ wasEmpty = true;
61
+ const inlines = refContent.value.querySelectorAll("strong, em, s, b, i, u, span:not([class])");
62
+ for (const el of inlines) {
63
+ const parent = el.parentNode;
64
+ while (el.firstChild) {
65
+ parent.insertBefore(el.firstChild, el);
66
+ }
67
+ el.remove();
68
+ }
69
+ refContent.value.innerHTML = "<p><br></p>";
70
+ const p = refContent.value.firstElementChild;
71
+ const sel = window.getSelection();
72
+ if (sel) {
73
+ const range = document.createRange();
74
+ range.setStart(p, 0);
75
+ range.collapse(true);
76
+ sel.removeAllRanges();
77
+ sel.addRange(range);
78
+ }
79
+ };
80
+ const cleanOrphanedFormatting = () => {
81
+ if (!refContent.value) return;
82
+ if (!wasEmpty) return;
83
+ wasEmpty = false;
84
+ const caretOffset = selection.getCaretOffsetInContent();
85
+ const unwrapInlines = (parent) => {
86
+ const inlines = parent.querySelectorAll("strong, b, em, i, s, u, span:not([class])");
87
+ for (const el of inlines) {
88
+ const elParent = el.parentNode;
89
+ while (el.firstChild) {
90
+ elParent.insertBefore(el.firstChild, el);
91
+ }
92
+ el.remove();
93
+ }
94
+ parent.normalize();
95
+ };
96
+ unwrapInlines(refContent.value);
97
+ selection.restoreCaretOffset(refContent.value, caretOffset);
98
+ };
99
+ const openPrompt = (params) => {
100
+ Dialogs.open(AnEditorPrompt, { ...params });
101
+ };
102
+ const createYouTubeElement = (ytId) => {
103
+ const div = document.createElement("div");
104
+ div.className = "an-editor__youtube";
105
+ div.innerHTML = `<iframe src="https://www.youtube-nocookie.com/embed/${ytId}" style="border:none" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>`;
106
+ return div;
107
+ };
108
+ const uploadAndInsert = async (file) => {
109
+ if (!props.upload) return;
110
+ try {
111
+ const url = await props.upload(file);
112
+ const figure = image.createImageElement(url, file.name);
113
+ blocks.insertBlockAtCursor(figure);
114
+ } catch {
115
+ }
116
+ };
117
+ const emitMd = (md) => {
118
+ internalMd = md;
119
+ emit("update:modelValue", md);
120
+ updateActiveFormats();
121
+ };
122
+ const onToolbarAction = (type) => {
123
+ if (props.disabled) return;
124
+ switch (type) {
125
+ case "undo":
126
+ history.undo(emitMd, toMarkdown);
127
+ return;
128
+ case "redo":
129
+ history.redo(emitMd, toMarkdown);
130
+ return;
131
+ case "bold":
132
+ blocks.wrapSelection("strong");
133
+ break;
134
+ case "italic":
135
+ blocks.wrapSelection("em");
136
+ break;
137
+ case "strikethrough":
138
+ blocks.wrapSelection("s");
139
+ break;
140
+ case "code":
141
+ blocks.wrapSelection("code", "an-editor__code");
142
+ break;
143
+ case "h1":
144
+ case "h2":
145
+ case "h3":
146
+ case "h4":
147
+ case "h5":
148
+ case "h6":
149
+ blocks.replaceBlock(type);
150
+ break;
151
+ case "unorderedList":
152
+ list.convertToList("ul");
153
+ break;
154
+ case "orderedList":
155
+ list.convertToList("ol");
156
+ break;
157
+ case "blockquote":
158
+ blocks.wrapBlockInTag("blockquote");
159
+ break;
160
+ case "horizontalRule": {
161
+ const hr = document.createElement("hr");
162
+ blocks.insertBlockAtCursor(hr);
163
+ break;
164
+ }
165
+ case "codeBlock": {
166
+ const sel = selection.getSelection();
167
+ const selectedText = sel && !sel.range.collapsed ? sel.range.toString() : "";
168
+ const pre = document.createElement("pre");
169
+ pre.className = "an-editor__codeblock";
170
+ const code = document.createElement("code");
171
+ code.textContent = selectedText || "\n";
172
+ pre.appendChild(code);
173
+ if (sel && !sel.range.collapsed) {
174
+ sel.range.deleteContents();
175
+ }
176
+ const currentSel = selection.getSelection();
177
+ if (!currentSel) {
178
+ refContent.value?.appendChild(pre);
179
+ } else {
180
+ const block = blocks.getCurrentBlock();
181
+ if (block) {
182
+ block.parentNode.insertBefore(pre, block.nextSibling);
183
+ } else {
184
+ refContent.value.appendChild(pre);
185
+ }
186
+ }
187
+ const p = document.createElement("p");
188
+ p.innerHTML = "<br>";
189
+ pre.parentNode.insertBefore(p, pre.nextSibling);
190
+ const range = document.createRange();
191
+ range.setStart(code, 0);
192
+ range.collapse(true);
193
+ const winSel = window.getSelection();
194
+ winSel?.removeAllRanges();
195
+ winSel?.addRange(range);
196
+ syncToModel();
197
+ break;
198
+ }
199
+ case "link": {
200
+ const sel = selection.getSelection();
201
+ const selectedText = sel && !sel.range.collapsed ? sel.range.toString() : "";
202
+ openPrompt({
203
+ title: "Insert link",
204
+ fields: [
205
+ { key: "url", label: "URL", placeholder: "https://" },
206
+ { key: "text", label: "Text", value: selectedText, placeholder: "Link text" }
207
+ ],
208
+ onSubmit: (values) => {
209
+ if (!values.url) return;
210
+ const currentSel = selection.getSelection();
211
+ const a = document.createElement("a");
212
+ a.href = values.url;
213
+ a.textContent = values.text || values.url;
214
+ if (currentSel && !currentSel.range.collapsed) {
215
+ currentSel.range.deleteContents();
216
+ }
217
+ if (currentSel) {
218
+ currentSel.range.insertNode(a);
219
+ currentSel.range.collapse(false);
220
+ currentSel.selection.removeAllRanges();
221
+ currentSel.selection.addRange(currentSel.range);
222
+ } else {
223
+ refContent.value?.appendChild(a);
224
+ }
225
+ syncToModel();
226
+ }
227
+ });
228
+ break;
229
+ }
230
+ case "image": {
231
+ openPrompt({
232
+ title: "Insert image",
233
+ fields: [
234
+ { key: "url", label: "URL", placeholder: "https://example.com/image.png" }
235
+ ],
236
+ onSubmit: (values) => {
237
+ if (!values.url) return;
238
+ const figure = image.createImageElement(values.url);
239
+ blocks.insertBlockAtCursor(figure);
240
+ }
241
+ });
242
+ break;
243
+ }
244
+ case "imageUpload": {
245
+ FileDialog.open();
246
+ break;
247
+ }
248
+ case "youtube": {
249
+ openPrompt({
250
+ title: "YouTube video",
251
+ fields: [
252
+ { key: "url", label: "URL", placeholder: "https://www.youtube.com/watch?v=..." }
253
+ ],
254
+ onSubmit: (values) => {
255
+ const ytId = extractYouTubeId(values.url ?? "");
256
+ if (!ytId) return;
257
+ const div = createYouTubeElement(ytId);
258
+ blocks.insertBlockAtCursor(div);
259
+ }
260
+ });
261
+ break;
262
+ }
263
+ case "table": {
264
+ openPrompt({
265
+ title: "Insert table",
266
+ fields: [
267
+ { key: "rows", label: "Rows", value: "3", placeholder: "3" },
268
+ { key: "cols", label: "Columns", value: "3", placeholder: "3" }
269
+ ],
270
+ onSubmit: (values) => {
271
+ const rows = Math.max(1, Math.min(20, parseInt(values.rows || "3")));
272
+ const cols = Math.max(1, Math.min(10, parseInt(values.cols || "3")));
273
+ const tbl = table.createTableElement(rows, cols);
274
+ blocks.insertBlockAtCursor(tbl);
275
+ }
276
+ });
277
+ break;
278
+ }
279
+ }
280
+ };
281
+ const openImagePrompt = (img, figure) => {
282
+ openPrompt({
283
+ title: "Edit image",
284
+ submitLabel: "Save",
285
+ deleteLabel: "Delete",
286
+ fields: [
287
+ { key: "url", label: "URL", value: img.src }
288
+ ],
289
+ onSubmit: (values) => {
290
+ if (!values.url) return;
291
+ img.src = values.url;
292
+ syncToModel();
293
+ },
294
+ onDelete: () => {
295
+ const toRemove = figure ?? img;
296
+ const next = toRemove.nextElementSibling;
297
+ toRemove.remove();
298
+ if (next && next.tagName === "P" && (!next.textContent || next.textContent.trim() === "") && next.innerHTML === "<br>") {
299
+ next.remove();
300
+ }
301
+ image.activeImage.value = null;
302
+ syncToModel();
303
+ }
304
+ });
305
+ };
306
+ const onContentClick = (e) => {
307
+ if (props.disabled) return;
308
+ const target = e.target;
309
+ if (!target) return;
310
+ if (!target.closest(".an-editor__table")) {
311
+ table.activeTable.value = null;
312
+ }
313
+ if (!target.closest(".an-editor__image")) {
314
+ image.activeImage.value = null;
315
+ }
316
+ const tableCell = target.closest("td, th");
317
+ if (tableCell) {
318
+ const tbl = tableCell.closest(".an-editor__table");
319
+ if (tbl) {
320
+ table.activeTable.value = { table: tbl, cell: tableCell };
321
+ return;
322
+ }
323
+ }
324
+ if (target.tagName === "A") {
325
+ e.preventDefault();
326
+ const a = target;
327
+ openPrompt({
328
+ title: "Edit link",
329
+ submitLabel: "Save",
330
+ deleteLabel: "Remove link",
331
+ fields: [
332
+ { key: "url", label: "URL", value: a.href },
333
+ { key: "text", label: "Text", value: a.textContent ?? "" }
334
+ ],
335
+ onSubmit: (values) => {
336
+ if (!values.url) return;
337
+ a.href = values.url;
338
+ a.textContent = values.text || values.url;
339
+ syncToModel();
340
+ },
341
+ onDelete: () => {
342
+ const text = document.createTextNode(a.textContent ?? "");
343
+ a.replaceWith(text);
344
+ syncToModel();
345
+ }
346
+ });
347
+ return;
348
+ }
349
+ const img = target.tagName === "IMG" ? target : null;
350
+ if (img) {
351
+ const figure = img.closest(".an-editor__image");
352
+ if (figure) {
353
+ image.activeImage.value = { img, figure };
354
+ }
355
+ return;
356
+ }
357
+ const ytDiv = target.closest(".an-editor__youtube");
358
+ if (ytDiv) {
359
+ const iframe = ytDiv.querySelector("iframe");
360
+ const src = iframe?.getAttribute("src") ?? "";
361
+ const ytMatch = src.match(/youtube(?:-nocookie)?\.com\/embed\/([A-Za-z0-9_-]{11})/);
362
+ const currentId = ytMatch ? ytMatch[1] : "";
363
+ openPrompt({
364
+ title: "Edit YouTube video",
365
+ submitLabel: "Save",
366
+ deleteLabel: "Delete",
367
+ fields: [
368
+ { key: "url", label: "URL", value: currentId ? `https://www.youtube.com/watch?v=${currentId}` : "" }
369
+ ],
370
+ onSubmit: (values) => {
371
+ const newId = extractYouTubeId(values.url ?? "");
372
+ if (!newId) return;
373
+ const newDiv = createYouTubeElement(newId);
374
+ ytDiv.replaceWith(newDiv);
375
+ syncToModel();
376
+ },
377
+ onDelete: () => {
378
+ const next = ytDiv.nextElementSibling;
379
+ ytDiv.remove();
380
+ if (next && next.tagName === "P" && (!next.textContent || next.textContent.trim() === "") && next.innerHTML === "<br>") {
381
+ next.remove();
382
+ }
383
+ syncToModel();
384
+ }
385
+ });
386
+ return;
387
+ }
388
+ };
389
+ const onPaste = (e) => {
390
+ e.preventDefault();
391
+ const text = e.clipboardData?.getData("text/plain") ?? "";
392
+ const ytId = extractYouTubeId(text);
393
+ if (ytId && /^https?:\/\//.test(text.trim())) {
394
+ const div = createYouTubeElement(ytId);
395
+ blocks.insertBlockAtCursor(div);
396
+ return;
397
+ }
398
+ const sel = selection.getSelection();
399
+ if (!sel) return;
400
+ sel.range.deleteContents();
401
+ sel.range.insertNode(document.createTextNode(text));
402
+ sel.range.collapse(false);
403
+ sel.selection.removeAllRanges();
404
+ sel.selection.addRange(sel.range);
405
+ syncToModel();
406
+ };
407
+ const onKeydown = (e) => {
408
+ if (props.disabled) return;
409
+ if (e.key.length === 1 && !e.ctrlKey && !e.metaKey && !e.altKey) {
410
+ resetTypingStyle();
411
+ }
412
+ const mod = e.ctrlKey || e.metaKey;
413
+ if (mod && e.key === "z" && !e.shiftKey) {
414
+ e.preventDefault();
415
+ history.undo(emitMd, toMarkdown);
416
+ return;
417
+ }
418
+ if (mod && e.key === "z" && e.shiftKey) {
419
+ e.preventDefault();
420
+ history.redo(emitMd, toMarkdown);
421
+ return;
422
+ }
423
+ if (mod && e.key === "y") {
424
+ e.preventDefault();
425
+ history.redo(emitMd, toMarkdown);
426
+ return;
427
+ }
428
+ if (mod && e.key === "b") {
429
+ e.preventDefault();
430
+ onToolbarAction("bold");
431
+ }
432
+ if (mod && e.key === "i") {
433
+ e.preventDefault();
434
+ onToolbarAction("italic");
435
+ }
436
+ if (mod && e.key === "k") {
437
+ e.preventDefault();
438
+ onToolbarAction("link");
439
+ }
440
+ if (e.key === "Tab") {
441
+ const li = list.findParentLi();
442
+ if (li) {
443
+ e.preventDefault();
444
+ if (e.shiftKey) {
445
+ list.outdentListItem(li);
446
+ } else {
447
+ list.indentListItem(li);
448
+ }
449
+ return;
450
+ }
451
+ }
452
+ if (e.key === "Enter" && !e.shiftKey) {
453
+ const sel = selection.getSelection();
454
+ if (sel) {
455
+ let node = sel.range.commonAncestorContainer;
456
+ if (node.nodeType === Node.TEXT_NODE) node = node.parentNode;
457
+ const cell = node?.closest?.("td, th");
458
+ if (cell && refContent.value?.contains(cell)) {
459
+ e.preventDefault();
460
+ const br = document.createElement("br");
461
+ sel.range.deleteContents();
462
+ sel.range.insertNode(br);
463
+ sel.range.setStartAfter(br);
464
+ sel.range.collapse(true);
465
+ sel.selection.removeAllRanges();
466
+ sel.selection.addRange(sel.range);
467
+ syncToModel();
468
+ return;
469
+ }
470
+ const heading = node?.closest?.("h1, h2, h3, h4, h5, h6");
471
+ if (heading && refContent.value?.contains(heading)) {
472
+ e.preventDefault();
473
+ const p = document.createElement("p");
474
+ p.innerHTML = "<br>";
475
+ heading.parentNode.insertBefore(p, heading.nextSibling);
476
+ const range = document.createRange();
477
+ range.setStart(p, 0);
478
+ range.collapse(true);
479
+ sel.selection.removeAllRanges();
480
+ sel.selection.addRange(range);
481
+ syncToModel();
482
+ return;
483
+ }
484
+ }
485
+ }
486
+ };
487
+ const updateActiveFormats = () => {
488
+ const sel = window.getSelection();
489
+ if (!sel || sel.rangeCount === 0) return;
490
+ if (!refContent.value?.contains(sel.anchorNode)) {
491
+ activeFormats.value = /* @__PURE__ */ new Set();
492
+ return;
493
+ }
494
+ const formats = /* @__PURE__ */ new Set();
495
+ let node = sel.anchorNode;
496
+ while (node && node !== refContent.value) {
497
+ if (node instanceof HTMLElement) {
498
+ const tag = node.tagName.toLowerCase();
499
+ if (tag === "strong" || tag === "b") formats.add("bold");
500
+ if (tag === "em" || tag === "i") formats.add("italic");
501
+ if (tag === "s" || tag === "del" || tag === "strike") formats.add("strikethrough");
502
+ if (tag === "code" && node.parentElement?.tagName.toLowerCase() !== "pre") formats.add("code");
503
+ if (tag === "blockquote") formats.add("blockquote");
504
+ if (tag === "ul") formats.add("unorderedList");
505
+ if (tag === "ol") formats.add("orderedList");
506
+ if (/^h[1-6]$/.test(tag)) formats.add(tag);
507
+ }
508
+ node = node.parentNode;
509
+ }
510
+ activeFormats.value = formats;
511
+ };
512
+ const isEmpty = computed(() => {
513
+ const html = refContent.value?.innerHTML ?? "";
514
+ return !html || html === "<br>" || html === "<p><br></p>";
515
+ });
516
+ const throttledUpdateActiveFormats = useThrottleFn(updateActiveFormats, 100);
517
+ useEventListener(document, "selectionchange", throttledUpdateActiveFormats);
518
+ FileDialog.onChange((files) => {
519
+ if (!files?.length) return;
520
+ const file = files[0];
521
+ if (file) uploadAndInsert(file);
522
+ FileDialog.reset();
523
+ });
524
+ onMounted(() => {
525
+ if (refContent.value && props.modelValue) {
526
+ refContent.value.innerHTML = parseMarkdown(props.modelValue);
527
+ internalMd = props.modelValue;
528
+ }
529
+ history.pushHistory();
530
+ });
531
+ watch(() => props.modelValue, (newMd) => {
532
+ if (newMd === internalMd) return;
533
+ internalMd = newMd;
534
+ skipNextUpdate = true;
535
+ if (refContent.value) {
536
+ refContent.value.innerHTML = newMd ? parseMarkdown(newMd) : "";
537
+ }
538
+ });
539
+ defineExpose({ focus, clear });
540
+ </script>
541
+
542
+ <template>
543
+ <div
544
+ class="an-editor"
545
+ :class="{ '-disabled': disabled }"
546
+ >
547
+ <AnEditorToolbar
548
+ :active-formats="activeFormats"
549
+ :has-upload="!!upload"
550
+ @action="onToolbarAction"
551
+ />
552
+
553
+ <div class="an-editor__body">
554
+ <div
555
+ ref="refContent"
556
+ class="an-editor__content"
557
+ :class="{ '-empty': isEmpty }"
558
+ :contenteditable="!disabled"
559
+ :data-placeholder="placeholder"
560
+ role="textbox"
561
+ aria-multiline="true"
562
+ :aria-disabled="disabled"
563
+ @input="syncToModel"
564
+ @keydown="onKeydown"
565
+ @compositionstart="isComposing = true"
566
+ @compositionend="isComposing = false;
567
+ syncToModel()"
568
+ @paste="onPaste"
569
+ @click="onContentClick"
570
+ />
571
+
572
+ <!-- Table action bar -->
573
+ <div
574
+ v-if="table.activeTable.value"
575
+ class="an-editor__table-actions"
576
+ :style="table.tableActionsStyle.value"
577
+ @mousedown.prevent
578
+ >
579
+ <button v-if="table.canAddHeader.value" class="an-editor__table-action-btn" @click="table.tableAddHeader()">Add header</button>
580
+ <template v-if="!table.isHeaderCell.value">
581
+ <button class="an-editor__table-action-btn" @click="table.tableAddRow('above')">Add row above</button>
582
+ <button class="an-editor__table-action-btn" @click="table.tableAddRow('below')">Add row below</button>
583
+ <button class="an-editor__table-action-btn" @click="table.tableAddColumn('left')">Add column left</button>
584
+ <button class="an-editor__table-action-btn" @click="table.tableAddColumn('right')">Add column right</button>
585
+ <div class="an-editor__table-actions-divider" />
586
+ <button class="an-editor__table-action-btn -danger" @click="table.tableDeleteRow()">Delete row</button>
587
+ </template>
588
+ <button v-if="table.isHeaderCell.value" class="an-editor__table-action-btn -danger" @click="table.tableDeleteRow()">Delete header</button>
589
+ <button class="an-editor__table-action-btn -danger" @click="table.tableDeleteColumn()">Delete column</button>
590
+ <button class="an-editor__table-action-btn -danger" @click="table.tableDelete()">Delete table</button>
591
+ </div>
592
+
593
+ <!-- Image overlay -->
594
+ <div
595
+ v-if="image.activeImage.value"
596
+ class="an-editor__image-overlay"
597
+ :style="image.imageOverlayStyle.value"
598
+ @mousedown.prevent
599
+ >
600
+ <!-- Expand to 100% width button (top-right) -->
601
+ <button
602
+ class="an-editor__image-expand"
603
+ title="Expand to full width"
604
+ @click="image.imageExpandFull()"
605
+ >
606
+ <Icon name="aneditor:expand" />
607
+ </button>
608
+
609
+ <!-- Edit button (top-right, second) -->
610
+ <button
611
+ class="an-editor__image-edit"
612
+ title="Edit image"
613
+ @click="openImagePrompt(image.activeImage.value.img, image.activeImage.value.figure)"
614
+ >
615
+ <Icon name="aneditor:edit" />
616
+ </button>
617
+
618
+ <!-- Resize handle (bottom-right) -->
619
+ <div
620
+ class="an-editor__image-resize"
621
+ @mousedown="image.onImageResizeStart"
622
+ />
623
+ </div>
624
+ </div>
625
+ </div>
626
+ </template>
627
+
628
+ <style scoped>
629
+ .an-editor{background:var(--an-editor-background,#fff);border:1px solid var(--an-editor-border,#e0e0e0);border-radius:8px;overflow:hidden}.an-editor:focus-within{border-color:var(--an-editor-border-focus,#7c83ff)}.an-editor.-disabled{opacity:.6;pointer-events:none}.an-editor__body{position:relative}.an-editor__content{font-size:var(--an-editor-font-size,15px);line-height:1.6;min-height:var(--an-editor-min-height,200px);outline:none;padding:16px}.an-editor__content.-empty:before{color:#999;content:attr(data-placeholder);pointer-events:none}.an-editor__content :deep(h1){font-size:2em;font-weight:700;margin:.5em 0}.an-editor__content :deep(h2){font-size:1.5em;font-weight:700;margin:.5em 0}.an-editor__content :deep(h3){font-size:1.17em;font-weight:700;margin:.5em 0}.an-editor__content :deep(h4){font-size:1em;font-weight:700;margin:.5em 0}.an-editor__content :deep(h5){font-size:.83em;font-weight:700;margin:.5em 0}.an-editor__content :deep(h6){font-size:.67em;font-weight:700;margin:.5em 0}.an-editor__content :deep(strong){font-weight:700}.an-editor__content :deep(em){font-style:italic}.an-editor__content :deep(s){text-decoration:line-through}.an-editor__content :deep(a){color:#7c83ff;cursor:pointer;text-decoration:underline}.an-editor__content :deep(blockquote){border-left:3px solid #e0e0e0;color:#666;margin:12px 0;padding-left:16px}.an-editor__content :deep(ol),.an-editor__content :deep(ul){margin:8px 0;padding-left:24px}.an-editor__content :deep(code){background:#f4f4f5;border-radius:4px;font-family:SF Mono,Fira Code,monospace;font-size:.9em;padding:2px 6px}.an-editor__content :deep(pre){background:#1e1e2e;border-radius:8px;color:#cdd6f4;margin:12px 0;min-height:3em;overflow-x:auto;padding:16px}.an-editor__content :deep(pre) code{background:none;border-radius:0;color:inherit;display:block;min-height:1.4em;outline:none;padding:0}.an-editor__content :deep(hr){border:none;border-top:2px solid #e0e0e0;margin:16px 0}.an-editor__content :deep(.an-editor__youtube){cursor:pointer;margin:12px 0}.an-editor__content :deep(.an-editor__youtube) iframe{aspect-ratio:16/9;border:none;border-radius:8px;pointer-events:none;width:100%}.an-editor__content :deep(.an-editor__image){cursor:pointer;margin:12px 0;position:relative}.an-editor__content :deep(.an-editor__image) img{border-radius:8px;display:block;max-width:100%;transition:filter .15s}.an-editor__content :deep(.an-editor__image):hover img{filter:brightness(.92)}.an-editor__content :deep(.an-editor__table){border-collapse:collapse;font-size:.95em;margin:12px 0;width:100%}.an-editor__content :deep(.an-editor__table) td,.an-editor__content :deep(.an-editor__table) th{border:1px solid #d0d5dd;box-sizing:border-box;cursor:text;line-height:1.5;min-width:60px;outline:none;padding:8px 12px;text-align:left}.an-editor__content :deep(.an-editor__table) th{background:#f4f5f7;font-weight:600}.an-editor__content :deep(.an-editor__table) tr:hover td{background:#f9fafb}.an-editor__content :deep(p){margin:8px 0}.an-editor__content :deep(p):first-child{margin-top:0}.an-editor__content :deep(p):last-child{margin-bottom:0}.an-editor__table-actions{background:#fff;border:1px solid #e8eaed;border-radius:8px;box-shadow:0 4px 16px rgba(0,0,0,.1);min-width:160px;padding:4px;position:absolute;z-index:10}.an-editor__table-actions-divider{background:#e8eaed;height:1px;margin:4px 0}.an-editor__table-action-btn{background:none;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;display:block;font-size:13px;padding:8px 12px;text-align:left;transition:background .1s;width:100%}.an-editor__table-action-btn:hover{background:#f4f5f7}.an-editor__table-action-btn.-danger{color:#ff6b6b}.an-editor__table-action-btn.-danger:hover{background:#fff5f5}.an-editor__image-overlay{border:2px solid #7c83ff;border-radius:8px;pointer-events:none;position:absolute;z-index:10}.an-editor__image-edit,.an-editor__image-expand{align-items:center;background:rgba(0,0,0,.55);border:none;border-radius:6px;color:#fff;cursor:pointer;display:flex;font-size:18px;height:32px;justify-content:center;pointer-events:auto;position:absolute;top:8px;transition:background .15s;width:32px}.an-editor__image-edit:hover,.an-editor__image-expand:hover{background:rgba(0,0,0,.75)}.an-editor__image-expand{right:8px}.an-editor__image-edit{right:46px}.an-editor__image-resize{background:#7c83ff;border-radius:2px 0 8px 0;bottom:-2px;cursor:nwse-resize;height:16px;pointer-events:auto;position:absolute;right:-2px;width:16px}.an-editor__image-resize:before{border-bottom:2px solid #fff;border-right:2px solid #fff;bottom:3px;content:"";height:6px;position:absolute;right:3px;width:6px}
630
+ </style>
@@ -0,0 +1,2 @@
1
+ declare const _default: {};
2
+ export default _default;
@@ -0,0 +1,50 @@
1
+ <script setup>
2
+ import { reactive } from "vue";
3
+ const emit = defineEmits(["close"]);
4
+ const props = defineProps({
5
+ params: { type: Object, required: true }
6
+ });
7
+ const form = reactive(
8
+ Object.fromEntries(props.params.fields.map((f) => [f.key, f.value ?? ""]))
9
+ );
10
+ const onSubmit = () => {
11
+ emit("close");
12
+ props.params.onSubmit(form);
13
+ };
14
+ const onDelete = () => {
15
+ emit("close");
16
+ props.params.onDelete?.();
17
+ };
18
+ </script>
19
+
20
+ <template>
21
+ <div class="an-editor-prompt" @keydown.enter.prevent="onSubmit">
22
+ <h3 class="an-editor-prompt__title">{{ params.title }}</h3>
23
+
24
+ <div v-for="field in params.fields" :key="field.key" class="an-editor-prompt__field">
25
+ <label class="an-editor-prompt__label">{{ field.label }}</label>
26
+ <input
27
+ v-model="form[field.key]"
28
+ class="an-editor-prompt__input"
29
+ :placeholder="field.placeholder"
30
+ type="text"
31
+ />
32
+ </div>
33
+
34
+ <div class="an-editor-prompt__actions">
35
+ <button class="an-editor-prompt__btn -primary" @click="onSubmit">
36
+ {{ params.submitLabel ?? "Insert" }}
37
+ </button>
38
+ <button v-if="params.onDelete" class="an-editor-prompt__btn -danger" @click="onDelete">
39
+ {{ params.deleteLabel ?? "Delete" }}
40
+ </button>
41
+ <button class="an-editor-prompt__btn -secondary" @click="emit('close')">
42
+ Cancel
43
+ </button>
44
+ </div>
45
+ </div>
46
+ </template>
47
+
48
+ <style scoped>
49
+ .an-editor-prompt{background:#fff;border-radius:12px;max-width:440px;min-width:360px;padding:24px}.an-editor-prompt__title{color:#1a1a2e;font-size:16px;font-weight:600;margin-bottom:16px}.an-editor-prompt__field{margin-bottom:12px}.an-editor-prompt__label{color:#344054;display:block;font-size:13px;font-weight:500;margin-bottom:4px}.an-editor-prompt__input{border:1px solid #d0d5dd;border-radius:6px;box-sizing:border-box;font-size:14px;outline:none;padding:8px 12px;transition:border-color .15s;width:100%}.an-editor-prompt__input:focus{border-color:#7c83ff}.an-editor-prompt__actions{display:flex;gap:8px;margin-top:16px}.an-editor-prompt__btn{border:none;border-radius:6px;cursor:pointer;font-size:13px;font-weight:500;padding:8px 16px;transition:all .15s}.an-editor-prompt__btn.-primary{background:#7c83ff;color:#fff}.an-editor-prompt__btn.-primary:hover{background:#6269e0}.an-editor-prompt__btn.-secondary{background:#e8eaed;color:#1a1a2e}.an-editor-prompt__btn.-secondary:hover{background:#d0d5dd}.an-editor-prompt__btn.-danger{background:#ff6b6b;color:#fff;margin-left:auto}.an-editor-prompt__btn.-danger:hover{background:#e05555}
50
+ </style>
@@ -0,0 +1,2 @@
1
+ declare const _default: {};
2
+ export default _default;