@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,191 @@
1
+ <script setup>
2
+ import { AnDropdown, Icon } from "#components";
3
+ const emit = defineEmits(["action"]);
4
+ const props = defineProps({
5
+ activeFormats: { type: Set, required: false },
6
+ hasUpload: { type: Boolean, required: false }
7
+ });
8
+ const onAction = (type, event) => {
9
+ event?.preventDefault();
10
+ emit("action", type);
11
+ };
12
+ const isActive = (format) => {
13
+ return props.activeFormats?.has(format) ?? false;
14
+ };
15
+ </script>
16
+
17
+ <template>
18
+ <div class="an-editor-toolbar" role="toolbar">
19
+ <!-- Undo / Redo -->
20
+ <div class="an-editor-toolbar__group">
21
+ <button
22
+ class="an-editor-toolbar__btn"
23
+ title="Undo (Ctrl+Z)"
24
+ @mousedown.prevent="onAction('undo')"
25
+ >
26
+ <Icon name="aneditor:undo" />
27
+ </button>
28
+ <button
29
+ class="an-editor-toolbar__btn"
30
+ title="Redo (Ctrl+Shift+Z)"
31
+ @mousedown.prevent="onAction('redo')"
32
+ >
33
+ <Icon name="aneditor:redo" />
34
+ </button>
35
+ </div>
36
+
37
+ <!-- Text formatting -->
38
+ <div class="an-editor-toolbar__group">
39
+ <button
40
+ class="an-editor-toolbar__btn"
41
+ :class="{ '-active': isActive('bold') }"
42
+ title="Bold (Ctrl+B)"
43
+ @mousedown.prevent="onAction('bold')"
44
+ >
45
+ <Icon name="aneditor:bold" />
46
+ </button>
47
+ <button
48
+ class="an-editor-toolbar__btn"
49
+ :class="{ '-active': isActive('italic') }"
50
+ title="Italic (Ctrl+I)"
51
+ @mousedown.prevent="onAction('italic')"
52
+ >
53
+ <Icon name="aneditor:italic" />
54
+ </button>
55
+ <button
56
+ class="an-editor-toolbar__btn"
57
+ :class="{ '-active': isActive('strikethrough') }"
58
+ title="Strikethrough"
59
+ @mousedown.prevent="onAction('strikethrough')"
60
+ >
61
+ <Icon name="aneditor:strikethrough" />
62
+ </button>
63
+ <AnDropdown area="bottom span-right">
64
+ <template #button="{ toggle }">
65
+ <button
66
+ class="an-editor-toolbar__btn"
67
+ :class="{ '-active': isActive('h1') || isActive('h2') || isActive('h3') || isActive('h4') || isActive('h5') || isActive('h6') }"
68
+ title="Heading"
69
+ @mousedown.prevent="toggle()"
70
+ >
71
+ <Icon name="aneditor:heading" />
72
+ </button>
73
+ </template>
74
+ <template #menu="{ close }">
75
+ <div class="an-editor-toolbar__submenu">
76
+ <button v-for="n in 6" :key="n" class="an-editor-toolbar__submenu-item" :class="{ '-active': isActive(`h${n}`) }" @mousedown.prevent="onAction(`h${n}`);
77
+ close()">
78
+ H{{ n }}
79
+ </button>
80
+ </div>
81
+ </template>
82
+ </AnDropdown>
83
+ </div>
84
+
85
+ <!-- Lists & structure -->
86
+ <div class="an-editor-toolbar__group">
87
+ <button
88
+ class="an-editor-toolbar__btn"
89
+ :class="{ '-active': isActive('unorderedList') }"
90
+ title="Bullet list"
91
+ @mousedown.prevent="onAction('unorderedList')"
92
+ >
93
+ <Icon name="aneditor:unordered-list" />
94
+ </button>
95
+ <button
96
+ class="an-editor-toolbar__btn"
97
+ :class="{ '-active': isActive('orderedList') }"
98
+ title="Numbered list"
99
+ @mousedown.prevent="onAction('orderedList')"
100
+ >
101
+ <Icon name="aneditor:ordered-list" />
102
+ </button>
103
+ <button
104
+ class="an-editor-toolbar__btn"
105
+ :class="{ '-active': isActive('blockquote') }"
106
+ title="Blockquote"
107
+ @mousedown.prevent="onAction('blockquote')"
108
+ >
109
+ <Icon name="aneditor:blockquote" />
110
+ </button>
111
+ <button
112
+ class="an-editor-toolbar__btn"
113
+ title="Horizontal rule"
114
+ @mousedown.prevent="onAction('horizontalRule')"
115
+ >
116
+ <Icon name="aneditor:horizontal-rule" />
117
+ </button>
118
+ <button
119
+ class="an-editor-toolbar__btn"
120
+ title="Insert table"
121
+ @click.prevent="onAction('table')"
122
+ >
123
+ <Icon name="aneditor:table" />
124
+ </button>
125
+ </div>
126
+
127
+ <!-- Code -->
128
+ <div class="an-editor-toolbar__group">
129
+ <button
130
+ class="an-editor-toolbar__btn"
131
+ :class="{ '-active': isActive('code') }"
132
+ title="Inline code"
133
+ @mousedown.prevent="onAction('code')"
134
+ >
135
+ <Icon name="aneditor:code" />
136
+ </button>
137
+ <button
138
+ class="an-editor-toolbar__btn"
139
+ title="Code block"
140
+ @mousedown.prevent="onAction('codeBlock')"
141
+ >
142
+ <Icon name="aneditor:code-block" />
143
+ </button>
144
+ </div>
145
+
146
+ <!-- Media -->
147
+ <div class="an-editor-toolbar__group">
148
+ <button
149
+ class="an-editor-toolbar__btn"
150
+ title="Insert link (Ctrl+K)"
151
+ @click.prevent="onAction('link')"
152
+ >
153
+ <Icon name="aneditor:link" />
154
+ </button>
155
+ <AnDropdown area="bottom span-right">
156
+ <template #button="{ toggle }">
157
+ <button
158
+ class="an-editor-toolbar__btn"
159
+ title="Insert image"
160
+ @mousedown.prevent="toggle()"
161
+ >
162
+ <Icon name="aneditor:image" />
163
+ </button>
164
+ </template>
165
+ <template #menu="{ close }">
166
+ <div class="an-editor-toolbar__submenu">
167
+ <button class="an-editor-toolbar__submenu-item" @click.prevent="onAction('image');
168
+ close()">
169
+ By URL
170
+ </button>
171
+ <button v-if="hasUpload" class="an-editor-toolbar__submenu-item" @mousedown.prevent="onAction('imageUpload');
172
+ close()">
173
+ Upload
174
+ </button>
175
+ </div>
176
+ </template>
177
+ </AnDropdown>
178
+ <button
179
+ class="an-editor-toolbar__btn"
180
+ title="YouTube video"
181
+ @click.prevent="onAction('youtube')"
182
+ >
183
+ <Icon name="aneditor:youtube" />
184
+ </button>
185
+ </div>
186
+ </div>
187
+ </template>
188
+
189
+ <style scoped>
190
+ .an-editor-toolbar{background:var(--an-editor-toolbar-background,#fafafa);border-bottom:1px solid var(--an-editor-border,#e0e0e0);border-radius:8px 8px 0 0;flex-wrap:wrap;padding:8px 12px}.an-editor-toolbar,.an-editor-toolbar__group{align-items:center;display:flex;gap:2px}.an-editor-toolbar__group:not(:last-child):after{background:var(--an-editor-border,#e0e0e0);content:"";height:20px;margin:0 6px;width:1px}.an-editor-toolbar__btn{align-items:center;background:none;border:none;border-radius:6px;color:#555;cursor:pointer;display:flex;font-size:18px;height:32px;justify-content:center;transition:all .15s;width:32px}.an-editor-toolbar__btn:hover{background:rgba(0,0,0,.06);color:#1a1a2e}.an-editor-toolbar__btn.-active{background:rgba(124,131,255,.15);color:#7c83ff}.an-editor-toolbar__submenu{background:#fff;border:1px solid #e8eaed;border-radius:8px;box-shadow:0 4px 16px rgba(0,0,0,.1);min-width:120px;padding:4px}.an-editor-toolbar__submenu-item{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-toolbar__submenu-item:hover{background:#f4f5f7}.an-editor-toolbar__submenu-item.-active{color:#7c83ff;font-weight:600}
191
+ </style>
@@ -0,0 +1,2 @@
1
+ declare const _default: {};
2
+ export default _default;
@@ -0,0 +1,16 @@
1
+ <script setup>
2
+ import { computed } from "vue";
3
+ import { parseMarkdown } from "#imports";
4
+ const props = defineProps({
5
+ content: { type: String, required: true }
6
+ });
7
+ const html = computed(() => parseMarkdown(props.content));
8
+ </script>
9
+
10
+ <template>
11
+ <div class="an-editor-viewer" v-html="html" />
12
+ </template>
13
+
14
+ <style scoped>
15
+ .an-editor-viewer{font-size:var(--an-editor-font-size,15px);line-height:1.6}.an-editor-viewer :deep(h1){font-size:2em;font-weight:700;margin:.5em 0}.an-editor-viewer :deep(h2){font-size:1.5em;font-weight:700;margin:.5em 0}.an-editor-viewer :deep(h3){font-size:1.17em;font-weight:700;margin:.5em 0}.an-editor-viewer :deep(h4){font-size:1em;font-weight:700;margin:.5em 0}.an-editor-viewer :deep(h5){font-size:.83em;font-weight:700;margin:.5em 0}.an-editor-viewer :deep(h6){font-size:.67em;font-weight:700;margin:.5em 0}.an-editor-viewer :deep(strong){font-weight:700}.an-editor-viewer :deep(em){font-style:italic}.an-editor-viewer :deep(s){text-decoration:line-through}.an-editor-viewer :deep(a){color:#7c83ff;text-decoration:underline}.an-editor-viewer :deep(blockquote){border-left:3px solid #e0e0e0;color:#666;margin:12px 0;padding-left:16px}.an-editor-viewer :deep(ol),.an-editor-viewer :deep(ul){margin:8px 0;padding-left:24px}.an-editor-viewer :deep(code){background:#f4f4f5;border-radius:4px;font-family:SF Mono,Fira Code,monospace;font-size:.9em;padding:2px 6px}.an-editor-viewer :deep(pre){background:#1e1e2e;border-radius:8px;color:#cdd6f4;margin:12px 0;min-height:3em;overflow-x:auto;padding:16px}.an-editor-viewer :deep(pre) code{background:none;border-radius:0;color:inherit;display:block;min-height:1.4em;padding:0}.an-editor-viewer :deep(hr){border:none;border-top:2px solid #e0e0e0;margin:16px 0}.an-editor-viewer :deep(.an-editor__youtube){margin:12px 0}.an-editor-viewer :deep(.an-editor__youtube) iframe{aspect-ratio:16/9;border:none;border-radius:8px;width:100%}.an-editor-viewer :deep(.an-editor__image){margin:12px 0}.an-editor-viewer :deep(.an-editor__image) img{border-radius:8px;display:block;max-width:100%}.an-editor-viewer :deep(.an-editor__table){border-collapse:collapse;font-size:.95em;margin:12px 0;width:100%}.an-editor-viewer :deep(.an-editor__table) td,.an-editor-viewer :deep(.an-editor__table) th{border:1px solid #d0d5dd;box-sizing:border-box;line-height:1.5;min-width:60px;padding:8px 12px;text-align:left}.an-editor-viewer :deep(.an-editor__table) th{background:#f4f5f7;font-weight:600}.an-editor-viewer :deep(.an-editor__table) tr:hover td{background:#f9fafb}.an-editor-viewer :deep(p){margin:8px 0}.an-editor-viewer :deep(p):first-child{margin-top:0}.an-editor-viewer :deep(p):last-child{margin-bottom:0}
16
+ </style>
@@ -0,0 +1,2 @@
1
+ declare const _default: {};
2
+ export default _default;
@@ -0,0 +1,15 @@
1
+ import type { Ref } from 'vue';
2
+ import type { useSelection } from './useSelection.js';
3
+ type TSelectionUtils = ReturnType<typeof useSelection>;
4
+ export declare const useBlocks: (refContent: Ref<HTMLDivElement | null>, sel: TSelectionUtils, syncToModel: () => void) => {
5
+ BLOCK_TAGS: Set<string>;
6
+ getCurrentBlock: () => HTMLElement | null;
7
+ isInsideCodeBlock: () => boolean;
8
+ ensureBlockWrapped: () => HTMLElement | null;
9
+ replaceBlock: (tagName: string) => void;
10
+ insertBlockAtCursor: (element: HTMLElement) => void;
11
+ wrapBlockInTag: (tagName: string) => void;
12
+ wrapSelection: (tagName: string, className?: string) => void;
13
+ normalizeContent: () => void;
14
+ };
15
+ export {};
@@ -0,0 +1,258 @@
1
+ const BLOCK_TAGS = /* @__PURE__ */ new Set([
2
+ "p",
3
+ "h1",
4
+ "h2",
5
+ "h3",
6
+ "h4",
7
+ "h5",
8
+ "h6",
9
+ "ul",
10
+ "ol",
11
+ "li",
12
+ "blockquote",
13
+ "pre",
14
+ "hr",
15
+ "table",
16
+ "figure",
17
+ "div"
18
+ ]);
19
+ export const useBlocks = (refContent, sel, syncToModel) => {
20
+ const getCurrentBlock = () => {
21
+ const s = sel.getSelection();
22
+ if (!s) return null;
23
+ let node = s.range.commonAncestorContainer;
24
+ if (node.nodeType === Node.TEXT_NODE) node = node.parentNode;
25
+ while (node && node !== refContent.value && node.parentNode !== refContent.value) {
26
+ node = node.parentNode;
27
+ }
28
+ return node && node !== refContent.value ? node : null;
29
+ };
30
+ const isInsideCodeBlock = () => {
31
+ const s = sel.getSelection();
32
+ if (!s) return false;
33
+ let node = s.range.commonAncestorContainer;
34
+ if (node.nodeType === Node.TEXT_NODE) node = node.parentNode;
35
+ while (node && node !== refContent.value) {
36
+ if (node instanceof HTMLElement && node.tagName.toLowerCase() === "pre") return true;
37
+ node = node.parentNode;
38
+ }
39
+ return false;
40
+ };
41
+ const ensureBlockWrapped = () => {
42
+ const block = getCurrentBlock();
43
+ if (block && BLOCK_TAGS.has(block.tagName.toLowerCase())) return block;
44
+ const s = sel.getSelection();
45
+ if (!s) return null;
46
+ let directChild = s.range.commonAncestorContainer;
47
+ if (directChild.nodeType === Node.TEXT_NODE) directChild = directChild.parentNode;
48
+ while (directChild && directChild.parentNode !== refContent.value) {
49
+ directChild = directChild.parentNode;
50
+ }
51
+ if (!directChild || directChild === refContent.value) {
52
+ if (!refContent.value) return null;
53
+ const p2 = document.createElement("p");
54
+ p2.innerHTML = "<br>";
55
+ refContent.value.appendChild(p2);
56
+ return p2;
57
+ }
58
+ const p = document.createElement("p");
59
+ refContent.value.insertBefore(p, directChild);
60
+ p.appendChild(directChild);
61
+ return p;
62
+ };
63
+ const replaceBlock = (tagName) => {
64
+ if (isInsideCodeBlock()) return;
65
+ const block = ensureBlockWrapped();
66
+ if (!block) return;
67
+ const offset = sel.getCaretOffset(block);
68
+ if (block.tagName.toLowerCase() === tagName.toLowerCase()) {
69
+ const p = document.createElement("p");
70
+ p.innerHTML = block.innerHTML;
71
+ block.replaceWith(p);
72
+ sel.restoreCaretOffset(p, offset);
73
+ syncToModel();
74
+ return;
75
+ }
76
+ const newEl = document.createElement(tagName);
77
+ newEl.innerHTML = block.innerHTML;
78
+ block.replaceWith(newEl);
79
+ sel.restoreCaretOffset(newEl, offset);
80
+ syncToModel();
81
+ };
82
+ const insertBlockAtCursor = (element) => {
83
+ const s = sel.getSelection();
84
+ if (!s) {
85
+ refContent.value?.appendChild(element);
86
+ } else {
87
+ const block = getCurrentBlock();
88
+ if (block) {
89
+ block.parentNode.insertBefore(element, block.nextSibling);
90
+ } else {
91
+ refContent.value.appendChild(element);
92
+ }
93
+ }
94
+ const p = document.createElement("p");
95
+ p.innerHTML = "<br>";
96
+ element.parentNode.insertBefore(p, element.nextSibling);
97
+ const range = document.createRange();
98
+ range.setStart(p, 0);
99
+ range.collapse(true);
100
+ const selection = window.getSelection();
101
+ selection?.removeAllRanges();
102
+ selection?.addRange(range);
103
+ syncToModel();
104
+ };
105
+ const wrapBlockInTag = (tagName) => {
106
+ if (isInsideCodeBlock()) return;
107
+ const block = ensureBlockWrapped();
108
+ if (!block) return;
109
+ const offset = sel.getCaretOffset(block);
110
+ if (block.tagName.toLowerCase() === tagName.toLowerCase()) {
111
+ const p = document.createElement("p");
112
+ p.innerHTML = block.innerHTML;
113
+ block.replaceWith(p);
114
+ sel.restoreCaretOffset(p, offset);
115
+ syncToModel();
116
+ return;
117
+ }
118
+ const wrapper = document.createElement(tagName);
119
+ wrapper.innerHTML = `<p>${block.innerHTML}</p>`;
120
+ block.replaceWith(wrapper);
121
+ sel.restoreCaretOffset(wrapper, offset);
122
+ syncToModel();
123
+ };
124
+ const wrapSelection = (tagName, className) => {
125
+ const s = sel.getSelection();
126
+ if (!s) return;
127
+ if (isInsideCodeBlock()) return;
128
+ const { selection, range } = s;
129
+ let node = range.commonAncestorContainer;
130
+ if (node.nodeType === Node.TEXT_NODE) node = node.parentNode;
131
+ while (node && node !== refContent.value) {
132
+ if (node instanceof HTMLElement && node.tagName.toLowerCase() === tagName.toLowerCase()) {
133
+ const wrapper = node;
134
+ const parent = wrapper.parentNode;
135
+ const wrapperRange = document.createRange();
136
+ wrapperRange.selectNodeContents(wrapper);
137
+ const coversAll = range.compareBoundaryPoints(Range.START_TO_START, wrapperRange) <= 0 && range.compareBoundaryPoints(Range.END_TO_END, wrapperRange) >= 0;
138
+ if (coversAll) {
139
+ const offsets = sel.getSelectionOffsets(parent);
140
+ while (wrapper.firstChild) {
141
+ parent.insertBefore(wrapper.firstChild, wrapper);
142
+ }
143
+ parent.removeChild(wrapper);
144
+ parent.normalize();
145
+ sel.restoreSelectionOffsets(parent, offsets.start, offsets.end);
146
+ } else {
147
+ const beforeRange = document.createRange();
148
+ beforeRange.setStart(wrapperRange.startContainer, wrapperRange.startOffset);
149
+ beforeRange.setEnd(range.startContainer, range.startOffset);
150
+ const afterRange = document.createRange();
151
+ afterRange.setStart(range.endContainer, range.endOffset);
152
+ afterRange.setEnd(wrapperRange.endContainer, wrapperRange.endOffset);
153
+ const beforeContent = beforeRange.cloneContents();
154
+ const selectedContent = range.cloneContents();
155
+ const afterContent = afterRange.cloneContents();
156
+ const frag = document.createDocumentFragment();
157
+ if (beforeContent.textContent) {
158
+ const beforeEl = document.createElement(tagName);
159
+ if (className) beforeEl.className = className;
160
+ beforeEl.appendChild(beforeContent);
161
+ frag.appendChild(beforeEl);
162
+ }
163
+ frag.appendChild(selectedContent);
164
+ if (afterContent.textContent) {
165
+ const afterEl = document.createElement(tagName);
166
+ if (className) afterEl.className = className;
167
+ afterEl.appendChild(afterContent);
168
+ frag.appendChild(afterEl);
169
+ }
170
+ const parentOffsets = sel.getSelectionOffsets(parent);
171
+ parent.replaceChild(frag, wrapper);
172
+ parent.normalize();
173
+ sel.restoreSelectionOffsets(parent, parentOffsets.start, parentOffsets.end);
174
+ }
175
+ syncToModel();
176
+ return;
177
+ }
178
+ node = node.parentNode;
179
+ }
180
+ if (range.collapsed) return;
181
+ try {
182
+ const wrapper = document.createElement(tagName);
183
+ if (className) wrapper.className = className;
184
+ range.surroundContents(wrapper);
185
+ const newRange = document.createRange();
186
+ newRange.selectNodeContents(wrapper);
187
+ selection.removeAllRanges();
188
+ selection.addRange(newRange);
189
+ } catch {
190
+ const fragment = range.extractContents();
191
+ const wrapper = document.createElement(tagName);
192
+ if (className) wrapper.className = className;
193
+ wrapper.appendChild(fragment);
194
+ range.insertNode(wrapper);
195
+ const newRange = document.createRange();
196
+ newRange.selectNodeContents(wrapper);
197
+ selection.removeAllRanges();
198
+ selection.addRange(newRange);
199
+ }
200
+ syncToModel();
201
+ };
202
+ const normalizeContent = () => {
203
+ if (!refContent.value) return;
204
+ let caretSaved = false;
205
+ let caretOffset = 0;
206
+ const saveCaret = () => {
207
+ if (!caretSaved) {
208
+ caretOffset = sel.getCaretOffsetInContent();
209
+ caretSaved = true;
210
+ }
211
+ };
212
+ for (const node of Array.from(refContent.value.childNodes)) {
213
+ if (node.nodeType === Node.ELEMENT_NODE) {
214
+ const el = node;
215
+ if (el.tagName.toLowerCase() === "div" && !el.classList.length) {
216
+ saveCaret();
217
+ const p = document.createElement("p");
218
+ p.innerHTML = el.innerHTML;
219
+ el.replaceWith(p);
220
+ }
221
+ }
222
+ }
223
+ const children = Array.from(refContent.value.childNodes);
224
+ let i = 0;
225
+ while (i < children.length) {
226
+ const node = children[i];
227
+ const isBareInline = node.nodeType === Node.TEXT_NODE ? !!node.textContent?.trim() : node.nodeType === Node.ELEMENT_NODE && !BLOCK_TAGS.has(node.tagName.toLowerCase());
228
+ if (isBareInline) {
229
+ saveCaret();
230
+ const p = document.createElement("p");
231
+ refContent.value.insertBefore(p, node);
232
+ while (i < children.length) {
233
+ const n = children[i];
234
+ const isInline = n.nodeType === Node.TEXT_NODE || n.nodeType === Node.ELEMENT_NODE && !BLOCK_TAGS.has(n.tagName.toLowerCase());
235
+ if (!isInline) break;
236
+ p.appendChild(n);
237
+ i++;
238
+ }
239
+ continue;
240
+ }
241
+ i++;
242
+ }
243
+ if (caretSaved) {
244
+ sel.restoreCaretOffset(refContent.value, caretOffset);
245
+ }
246
+ };
247
+ return {
248
+ BLOCK_TAGS,
249
+ getCurrentBlock,
250
+ isInsideCodeBlock,
251
+ ensureBlockWrapped,
252
+ replaceBlock,
253
+ insertBlockAtCursor,
254
+ wrapBlockInTag,
255
+ wrapSelection,
256
+ normalizeContent
257
+ };
258
+ };
@@ -0,0 +1,12 @@
1
+ import type { Ref } from 'vue';
2
+ import type { useSelection } from './useSelection.js';
3
+ type TSelectionUtils = ReturnType<typeof useSelection>;
4
+ export declare const useHistory: (refContent: Ref<HTMLDivElement | null>, selection: TSelectionUtils, options?: {
5
+ limit?: number;
6
+ }) => {
7
+ pushHistory: () => void;
8
+ undo: (emit: (md: string) => void, toMarkdown: (html: string) => string) => void;
9
+ redo: (emit: (md: string) => void, toMarkdown: (html: string) => string) => void;
10
+ readonly isUndoRedo: boolean;
11
+ };
12
+ export {};
@@ -0,0 +1,56 @@
1
+ export const useHistory = (refContent, selection, options = {}) => {
2
+ const HISTORY_LIMIT = options.limit ?? 50;
3
+ const history = [];
4
+ let historyIndex = -1;
5
+ let isUndoRedo = false;
6
+ const pushHistory = () => {
7
+ if (!refContent.value) return;
8
+ const html = refContent.value.innerHTML;
9
+ const caretOffset = selection.getCaretOffsetInContent();
10
+ if (historyIndex >= 0 && history[historyIndex].html === html) return;
11
+ history.splice(historyIndex + 1);
12
+ history.push({ html, caretOffset });
13
+ if (history.length > HISTORY_LIMIT) {
14
+ history.shift();
15
+ }
16
+ historyIndex = history.length - 1;
17
+ };
18
+ const applyHistory = (emit, toMarkdown) => {
19
+ const entry = history[historyIndex];
20
+ if (!entry || !refContent.value) return;
21
+ isUndoRedo = true;
22
+ refContent.value.innerHTML = entry.html;
23
+ selection.restoreCaretOffset(refContent.value, entry.caretOffset);
24
+ const html = refContent.value.innerHTML;
25
+ const md = toMarkdown(html);
26
+ emit(md);
27
+ isUndoRedo = false;
28
+ };
29
+ const undo = (emit, toMarkdown) => {
30
+ if (historyIndex <= 0 || !refContent.value) return;
31
+ if (historyIndex === history.length - 1) {
32
+ const html = refContent.value.innerHTML;
33
+ const caretOffset = selection.getCaretOffsetInContent();
34
+ if (history[historyIndex].html !== html) {
35
+ history.splice(historyIndex + 1);
36
+ history.push({ html, caretOffset });
37
+ historyIndex = history.length - 1;
38
+ }
39
+ }
40
+ historyIndex--;
41
+ applyHistory(emit, toMarkdown);
42
+ };
43
+ const redo = (emit, toMarkdown) => {
44
+ if (historyIndex >= history.length - 1 || !refContent.value) return;
45
+ historyIndex++;
46
+ applyHistory(emit, toMarkdown);
47
+ };
48
+ return {
49
+ pushHistory,
50
+ undo,
51
+ redo,
52
+ get isUndoRedo() {
53
+ return isUndoRedo;
54
+ }
55
+ };
56
+ };
@@ -0,0 +1,27 @@
1
+ import { type Ref } from 'vue';
2
+ export declare const useImage: (refContent: Ref<HTMLDivElement | null>, syncToModel: () => void) => {
3
+ activeImage: Ref<{
4
+ img: HTMLImageElement;
5
+ figure: HTMLElement;
6
+ } | null, {
7
+ img: HTMLImageElement;
8
+ figure: HTMLElement;
9
+ } | {
10
+ img: HTMLImageElement;
11
+ figure: HTMLElement;
12
+ } | null>;
13
+ createImageElement: (url: string, alt?: string) => HTMLElement;
14
+ imageOverlayStyle: import("vue").ComputedRef<{
15
+ top?: undefined;
16
+ left?: undefined;
17
+ width?: undefined;
18
+ height?: undefined;
19
+ } | {
20
+ top: string;
21
+ left: string;
22
+ width: string;
23
+ height: string;
24
+ }>;
25
+ onImageResizeStart: (e: MouseEvent) => void;
26
+ imageExpandFull: () => void;
27
+ };
@@ -0,0 +1,81 @@
1
+ import { ref, computed, nextTick, onScopeDispose } from "vue";
2
+ import { useThrottleFn } from "@vueuse/core";
3
+ export const useImage = (refContent, syncToModel) => {
4
+ const activeImage = ref(null);
5
+ const imageResizing = ref(null);
6
+ const createImageElement = (url, alt = "") => {
7
+ const figure = document.createElement("figure");
8
+ figure.className = "an-editor__image";
9
+ const img = document.createElement("img");
10
+ img.src = url;
11
+ img.alt = alt;
12
+ figure.appendChild(img);
13
+ return figure;
14
+ };
15
+ const imageOverlayStyle = computed(() => {
16
+ if (!activeImage.value) return {};
17
+ const editorRect = refContent.value?.getBoundingClientRect();
18
+ if (!editorRect) return {};
19
+ const rect = activeImage.value.img.getBoundingClientRect();
20
+ return {
21
+ top: `${rect.top - editorRect.top}px`,
22
+ left: `${rect.left - editorRect.left}px`,
23
+ width: `${rect.width}px`,
24
+ height: `${rect.height}px`
25
+ };
26
+ });
27
+ const onImageResizeMove = useThrottleFn((e) => {
28
+ if (!imageResizing.value) return;
29
+ const { startX, startWidth, img } = imageResizing.value;
30
+ const delta = e.clientX - startX;
31
+ const newWidth = Math.max(80, startWidth + delta);
32
+ const maxWidth = refContent.value?.clientWidth ?? 800;
33
+ img.style.width = `${Math.min(newWidth, maxWidth)}px`;
34
+ activeImage.value = { ...activeImage.value };
35
+ }, 32);
36
+ const handleResizeMove = (e) => onImageResizeMove(e);
37
+ const handleResizeEnd = () => {
38
+ if (!imageResizing.value) return;
39
+ document.body.style.cursor = "";
40
+ document.body.style.userSelect = "";
41
+ imageResizing.value = null;
42
+ document.removeEventListener("mousemove", handleResizeMove);
43
+ document.removeEventListener("mouseup", handleResizeEnd);
44
+ syncToModel();
45
+ };
46
+ const onImageResizeStart = (e) => {
47
+ if (!activeImage.value) return;
48
+ e.preventDefault();
49
+ e.stopPropagation();
50
+ const { img } = activeImage.value;
51
+ document.body.style.cursor = "nwse-resize";
52
+ document.body.style.userSelect = "none";
53
+ imageResizing.value = {
54
+ startX: e.clientX,
55
+ startWidth: img.offsetWidth,
56
+ img
57
+ };
58
+ document.addEventListener("mousemove", handleResizeMove);
59
+ document.addEventListener("mouseup", handleResizeEnd);
60
+ };
61
+ const imageExpandFull = () => {
62
+ if (!activeImage.value) return;
63
+ const { img } = activeImage.value;
64
+ img.style.width = "100%";
65
+ nextTick(() => {
66
+ activeImage.value = { ...activeImage.value };
67
+ });
68
+ syncToModel();
69
+ };
70
+ onScopeDispose(() => {
71
+ document.removeEventListener("mousemove", handleResizeMove);
72
+ document.removeEventListener("mouseup", handleResizeEnd);
73
+ });
74
+ return {
75
+ activeImage,
76
+ createImageElement,
77
+ imageOverlayStyle,
78
+ onImageResizeStart,
79
+ imageExpandFull
80
+ };
81
+ };
@@ -0,0 +1,10 @@
1
+ import type { Ref } from 'vue';
2
+ import type { useSelection } from './useSelection.js';
3
+ type TSelectionUtils = ReturnType<typeof useSelection>;
4
+ export declare const useList: (refContent: Ref<HTMLDivElement | null>, sel: TSelectionUtils, syncToModel: () => void, ensureBlockWrapped: () => HTMLElement | null, isInsideCodeBlock: () => boolean) => {
5
+ findParentLi: () => HTMLElement | null;
6
+ indentListItem: (li: HTMLElement) => void;
7
+ outdentListItem: (li: HTMLElement) => void;
8
+ convertToList: (tagName: "ul" | "ol") => void;
9
+ };
10
+ export {};