@domternal/vue 0.6.0
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.
- package/README.md +42 -0
- package/dist/index.d.ts +786 -0
- package/dist/index.js +2046 -0
- package/dist/index.js.map +1 -0
- package/package.json +49 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,2046 @@
|
|
|
1
|
+
import { defineComponent, ref, watchEffect, h, watch, computed, Fragment, onMounted, onScopeDispose, inject, shallowRef, provide, getCurrentInstance, markRaw, customRef, shallowReactive, render } from 'vue';
|
|
2
|
+
import { PluginKey, ToolbarController, createFloatingMenuPlugin, Editor, Document, Paragraph, Text, BaseKeymap, History, positionFloatingOnce, defaultIcons, createBubbleMenuPlugin } from '@domternal/core';
|
|
3
|
+
export { Editor, generateHTML, generateJSON, generateText } from '@domternal/core';
|
|
4
|
+
|
|
5
|
+
// src/useEditor.ts
|
|
6
|
+
var DEFAULT_EXTENSIONS = [Document, Paragraph, Text, BaseKeymap, History];
|
|
7
|
+
function useEditor(options = {}) {
|
|
8
|
+
const editor = shallowRef(null);
|
|
9
|
+
const editorRef = ref();
|
|
10
|
+
let pendingContent = null;
|
|
11
|
+
function wireEvents(ed) {
|
|
12
|
+
ed.on("transaction", ({ transaction }) => {
|
|
13
|
+
if (transaction.docChanged) {
|
|
14
|
+
options.onUpdate?.({ editor: ed });
|
|
15
|
+
}
|
|
16
|
+
if (!transaction.docChanged && transaction.selectionSet) {
|
|
17
|
+
options.onSelectionChange?.({ editor: ed });
|
|
18
|
+
}
|
|
19
|
+
});
|
|
20
|
+
ed.on("focus", ({ event }) => {
|
|
21
|
+
options.onFocus?.({ editor: ed, event });
|
|
22
|
+
});
|
|
23
|
+
ed.on("blur", ({ event }) => {
|
|
24
|
+
options.onBlur?.({ editor: ed, event });
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
function createEditorInstance(element, initialContent, focus) {
|
|
28
|
+
const extensions = options.extensions ?? [];
|
|
29
|
+
const editable = options.editable ?? true;
|
|
30
|
+
const ed = new Editor({
|
|
31
|
+
element,
|
|
32
|
+
extensions: [...DEFAULT_EXTENSIONS, ...extensions],
|
|
33
|
+
content: initialContent,
|
|
34
|
+
editable,
|
|
35
|
+
autofocus: focus
|
|
36
|
+
});
|
|
37
|
+
markRaw(ed);
|
|
38
|
+
wireEvents(ed);
|
|
39
|
+
editor.value = ed;
|
|
40
|
+
options.onCreate?.(ed);
|
|
41
|
+
return ed;
|
|
42
|
+
}
|
|
43
|
+
function destroyCurrentEditor() {
|
|
44
|
+
const current = editor.value;
|
|
45
|
+
if (current && !current.isDestroyed) {
|
|
46
|
+
pendingContent = current.getJSON();
|
|
47
|
+
options.onDestroy?.();
|
|
48
|
+
const dom = current.view.dom;
|
|
49
|
+
const parent = dom?.parentNode;
|
|
50
|
+
if (parent) {
|
|
51
|
+
const clone = dom.cloneNode(true);
|
|
52
|
+
clone.style.pointerEvents = "none";
|
|
53
|
+
parent.insertBefore(clone, dom);
|
|
54
|
+
}
|
|
55
|
+
current.destroy();
|
|
56
|
+
}
|
|
57
|
+
editor.value = null;
|
|
58
|
+
}
|
|
59
|
+
if (options.immediatelyRender) {
|
|
60
|
+
const element = document.createElement("div");
|
|
61
|
+
createEditorInstance(element, options.content ?? "", options.autofocus ?? false);
|
|
62
|
+
}
|
|
63
|
+
onMounted(() => {
|
|
64
|
+
if (editor.value) return;
|
|
65
|
+
const element = editorRef.value ?? document.createElement("div");
|
|
66
|
+
const initialContent = pendingContent ?? options.content ?? "";
|
|
67
|
+
pendingContent = null;
|
|
68
|
+
createEditorInstance(element, initialContent, options.autofocus ?? false);
|
|
69
|
+
});
|
|
70
|
+
onScopeDispose(() => {
|
|
71
|
+
destroyCurrentEditor();
|
|
72
|
+
});
|
|
73
|
+
watch(
|
|
74
|
+
() => options.editable ?? true,
|
|
75
|
+
(newEditable) => {
|
|
76
|
+
const ed = editor.value;
|
|
77
|
+
if (ed && !ed.isDestroyed) {
|
|
78
|
+
ed.setEditable(newEditable);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
);
|
|
82
|
+
watch(
|
|
83
|
+
() => options.extensions,
|
|
84
|
+
(newExtensions, oldExtensions) => {
|
|
85
|
+
if (!editor.value || editor.value.isDestroyed) return;
|
|
86
|
+
if (newExtensions === oldExtensions) return;
|
|
87
|
+
const element = editor.value.view.dom.parentElement ?? document.createElement("div");
|
|
88
|
+
destroyCurrentEditor();
|
|
89
|
+
const initialContent = pendingContent ?? "";
|
|
90
|
+
pendingContent = null;
|
|
91
|
+
createEditorInstance(element, initialContent, false);
|
|
92
|
+
}
|
|
93
|
+
);
|
|
94
|
+
watch(
|
|
95
|
+
() => options.content,
|
|
96
|
+
(newContent) => {
|
|
97
|
+
const ed = editor.value;
|
|
98
|
+
if (!ed || ed.isDestroyed || newContent === void 0) return;
|
|
99
|
+
const outputFormat = options.outputFormat ?? "html";
|
|
100
|
+
if (outputFormat === "html") {
|
|
101
|
+
if (newContent !== ed.getHTML()) {
|
|
102
|
+
ed.setContent(newContent, false);
|
|
103
|
+
}
|
|
104
|
+
} else {
|
|
105
|
+
if (JSON.stringify(newContent) !== JSON.stringify(ed.getJSON())) {
|
|
106
|
+
ed.setContent(newContent, false);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
},
|
|
110
|
+
{ flush: "post" }
|
|
111
|
+
);
|
|
112
|
+
return { editor, editorRef };
|
|
113
|
+
}
|
|
114
|
+
function useDebouncedRef(initialValue) {
|
|
115
|
+
let value = initialValue;
|
|
116
|
+
let rafId1;
|
|
117
|
+
let rafId2;
|
|
118
|
+
return customRef((track, trigger) => ({
|
|
119
|
+
get() {
|
|
120
|
+
track();
|
|
121
|
+
return value;
|
|
122
|
+
},
|
|
123
|
+
set(newValue) {
|
|
124
|
+
value = newValue;
|
|
125
|
+
if (rafId1 !== void 0) cancelAnimationFrame(rafId1);
|
|
126
|
+
if (rafId2 !== void 0) cancelAnimationFrame(rafId2);
|
|
127
|
+
rafId1 = requestAnimationFrame(() => {
|
|
128
|
+
rafId2 = requestAnimationFrame(() => {
|
|
129
|
+
rafId1 = rafId2 = void 0;
|
|
130
|
+
trigger();
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
}));
|
|
135
|
+
}
|
|
136
|
+
var appContextStore = /* @__PURE__ */ new WeakMap();
|
|
137
|
+
var pendingAppContextStore = { value: null };
|
|
138
|
+
|
|
139
|
+
// src/useEditorState.ts
|
|
140
|
+
function useEditorState(editor, selector) {
|
|
141
|
+
if (selector) {
|
|
142
|
+
return useEditorStateSelector(editor, selector);
|
|
143
|
+
}
|
|
144
|
+
return useEditorStateFull(editor);
|
|
145
|
+
}
|
|
146
|
+
function getFullState(ed) {
|
|
147
|
+
if (!ed || ed.isDestroyed) {
|
|
148
|
+
return { html: "", json: null, empty: true, focused: false, editable: true };
|
|
149
|
+
}
|
|
150
|
+
return {
|
|
151
|
+
html: ed.getHTML(),
|
|
152
|
+
json: ed.getJSON(),
|
|
153
|
+
empty: ed.isEmpty,
|
|
154
|
+
focused: ed.isFocused,
|
|
155
|
+
editable: ed.isEditable
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
function useEditorStateFull(editor) {
|
|
159
|
+
const initial = getFullState(editor.value);
|
|
160
|
+
const htmlContent = ref(initial.html);
|
|
161
|
+
const jsonContent = ref(initial.json);
|
|
162
|
+
const isEmpty = ref(initial.empty);
|
|
163
|
+
const isFocused = ref(initial.focused);
|
|
164
|
+
const isEditable = ref(initial.editable);
|
|
165
|
+
watch(
|
|
166
|
+
editor,
|
|
167
|
+
(ed, _oldEd, onCleanup) => {
|
|
168
|
+
if (!ed || ed.isDestroyed) {
|
|
169
|
+
htmlContent.value = "";
|
|
170
|
+
jsonContent.value = null;
|
|
171
|
+
isEmpty.value = true;
|
|
172
|
+
isFocused.value = false;
|
|
173
|
+
isEditable.value = true;
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
const state = getFullState(ed);
|
|
177
|
+
htmlContent.value = state.html;
|
|
178
|
+
jsonContent.value = state.json;
|
|
179
|
+
isEmpty.value = state.empty;
|
|
180
|
+
isFocused.value = state.focused;
|
|
181
|
+
isEditable.value = state.editable;
|
|
182
|
+
const onTransaction = () => {
|
|
183
|
+
const html = ed.getHTML();
|
|
184
|
+
const json = ed.getJSON();
|
|
185
|
+
const empty = ed.isEmpty;
|
|
186
|
+
const editable = ed.isEditable;
|
|
187
|
+
if (htmlContent.value !== html) htmlContent.value = html;
|
|
188
|
+
if (isEmpty.value !== empty) isEmpty.value = empty;
|
|
189
|
+
if (isEditable.value !== editable) isEditable.value = editable;
|
|
190
|
+
jsonContent.value = json;
|
|
191
|
+
};
|
|
192
|
+
const onFocus = () => {
|
|
193
|
+
if (!isFocused.value) isFocused.value = true;
|
|
194
|
+
};
|
|
195
|
+
const onBlur = () => {
|
|
196
|
+
if (isFocused.value) isFocused.value = false;
|
|
197
|
+
};
|
|
198
|
+
ed.on("transaction", onTransaction);
|
|
199
|
+
ed.on("focus", onFocus);
|
|
200
|
+
ed.on("blur", onBlur);
|
|
201
|
+
onCleanup(() => {
|
|
202
|
+
ed.off("transaction", onTransaction);
|
|
203
|
+
ed.off("focus", onFocus);
|
|
204
|
+
ed.off("blur", onBlur);
|
|
205
|
+
});
|
|
206
|
+
},
|
|
207
|
+
{ immediate: true }
|
|
208
|
+
);
|
|
209
|
+
return { htmlContent, jsonContent, isEmpty, isFocused, isEditable };
|
|
210
|
+
}
|
|
211
|
+
function useEditorStateSelector(editor, selector) {
|
|
212
|
+
const version = useDebouncedRef(0);
|
|
213
|
+
watch(
|
|
214
|
+
editor,
|
|
215
|
+
(ed, _oldEd, onCleanup) => {
|
|
216
|
+
if (!ed || ed.isDestroyed) return;
|
|
217
|
+
const bump = () => {
|
|
218
|
+
version.value++;
|
|
219
|
+
};
|
|
220
|
+
ed.on("transaction", bump);
|
|
221
|
+
ed.on("focus", bump);
|
|
222
|
+
ed.on("blur", bump);
|
|
223
|
+
onCleanup(() => {
|
|
224
|
+
ed.off("transaction", bump);
|
|
225
|
+
ed.off("focus", bump);
|
|
226
|
+
ed.off("blur", bump);
|
|
227
|
+
});
|
|
228
|
+
},
|
|
229
|
+
{ immediate: true }
|
|
230
|
+
);
|
|
231
|
+
return computed(() => {
|
|
232
|
+
void version.value;
|
|
233
|
+
const ed = editor.value;
|
|
234
|
+
if (!ed || ed.isDestroyed) return void 0;
|
|
235
|
+
return selector(ed);
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
var EDITOR_KEY = /* @__PURE__ */ Symbol("domternal-editor");
|
|
239
|
+
function provideEditor(editor) {
|
|
240
|
+
provide(EDITOR_KEY, editor);
|
|
241
|
+
const instance = getCurrentInstance();
|
|
242
|
+
if (instance) {
|
|
243
|
+
const buildCtx = () => {
|
|
244
|
+
const ctx = Object.create(instance.appContext);
|
|
245
|
+
ctx.provides = instance.provides;
|
|
246
|
+
return ctx;
|
|
247
|
+
};
|
|
248
|
+
pendingAppContextStore.value = buildCtx();
|
|
249
|
+
watchEffect(() => {
|
|
250
|
+
const ed = editor.value;
|
|
251
|
+
if (ed) {
|
|
252
|
+
appContextStore.set(ed, buildCtx());
|
|
253
|
+
pendingAppContextStore.value = null;
|
|
254
|
+
}
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
function useCurrentEditor() {
|
|
259
|
+
const editor = inject(EDITOR_KEY, shallowRef(null));
|
|
260
|
+
return { editor };
|
|
261
|
+
}
|
|
262
|
+
var Domternal = defineComponent({
|
|
263
|
+
name: "Domternal",
|
|
264
|
+
props: {
|
|
265
|
+
extensions: { type: Array, default: void 0 },
|
|
266
|
+
content: { type: [String, Object], default: "" },
|
|
267
|
+
editable: { type: Boolean, default: true },
|
|
268
|
+
autofocus: { type: [Boolean, String, Number], default: false },
|
|
269
|
+
outputFormat: { type: String, default: "html" },
|
|
270
|
+
immediatelyRender: { type: Boolean, default: false },
|
|
271
|
+
onCreate: { type: Function, default: void 0 },
|
|
272
|
+
onUpdate: { type: Function, default: void 0 },
|
|
273
|
+
onSelectionChange: { type: Function, default: void 0 },
|
|
274
|
+
onFocus: { type: Function, default: void 0 },
|
|
275
|
+
onBlur: { type: Function, default: void 0 },
|
|
276
|
+
onDestroy: { type: Function, default: void 0 }
|
|
277
|
+
},
|
|
278
|
+
setup(props, { slots }) {
|
|
279
|
+
const { editor } = useEditor({
|
|
280
|
+
...props.extensions && { extensions: props.extensions },
|
|
281
|
+
content: props.content,
|
|
282
|
+
editable: props.editable,
|
|
283
|
+
autofocus: props.autofocus,
|
|
284
|
+
outputFormat: props.outputFormat,
|
|
285
|
+
immediatelyRender: props.immediatelyRender,
|
|
286
|
+
...props.onCreate && { onCreate: props.onCreate },
|
|
287
|
+
...props.onUpdate && { onUpdate: props.onUpdate },
|
|
288
|
+
...props.onSelectionChange && { onSelectionChange: props.onSelectionChange },
|
|
289
|
+
...props.onFocus && { onFocus: props.onFocus },
|
|
290
|
+
...props.onBlur && { onBlur: props.onBlur },
|
|
291
|
+
...props.onDestroy && { onDestroy: props.onDestroy }
|
|
292
|
+
});
|
|
293
|
+
provideEditor(editor);
|
|
294
|
+
return () => slots["default"]?.();
|
|
295
|
+
}
|
|
296
|
+
});
|
|
297
|
+
var DomternalContent = defineComponent({
|
|
298
|
+
name: "DomternalContent",
|
|
299
|
+
props: {
|
|
300
|
+
class: { type: String, default: void 0 }
|
|
301
|
+
},
|
|
302
|
+
setup(props) {
|
|
303
|
+
const { editor } = useCurrentEditor();
|
|
304
|
+
const containerRef = ref();
|
|
305
|
+
watchEffect(() => {
|
|
306
|
+
const container = containerRef.value;
|
|
307
|
+
const ed = editor.value;
|
|
308
|
+
if (!container || !ed || ed.isDestroyed) return;
|
|
309
|
+
const editorDom = ed.view.dom;
|
|
310
|
+
if (editorDom.parentElement !== container) {
|
|
311
|
+
container.appendChild(editorDom);
|
|
312
|
+
}
|
|
313
|
+
});
|
|
314
|
+
return () => {
|
|
315
|
+
const classes = props.class ? `dm-editor ${props.class}` : "dm-editor";
|
|
316
|
+
return h("div", { class: classes, "data-dm-editor-ui": "" }, [h("div", { ref: containerRef })]);
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
});
|
|
320
|
+
var DomternalLoading = defineComponent({
|
|
321
|
+
name: "DomternalLoading",
|
|
322
|
+
setup(_props, { slots }) {
|
|
323
|
+
const { editor } = useCurrentEditor();
|
|
324
|
+
return () => editor.value ? null : slots["default"]?.();
|
|
325
|
+
}
|
|
326
|
+
});
|
|
327
|
+
Domternal.Content = DomternalContent;
|
|
328
|
+
Domternal.Loading = DomternalLoading;
|
|
329
|
+
var DomternalEditor = defineComponent({
|
|
330
|
+
name: "DomternalEditor",
|
|
331
|
+
props: {
|
|
332
|
+
extensions: { type: Array, default: void 0 },
|
|
333
|
+
content: { type: [String, Object], default: void 0 },
|
|
334
|
+
editable: { type: Boolean, default: true },
|
|
335
|
+
autofocus: { type: [Boolean, String, Number], default: false },
|
|
336
|
+
immediatelyRender: { type: Boolean, default: false },
|
|
337
|
+
outputFormat: { type: String, default: "html" },
|
|
338
|
+
modelValue: { type: [String, Object], default: void 0 },
|
|
339
|
+
class: { type: String, default: void 0 },
|
|
340
|
+
onCreate: { type: Function, default: void 0 },
|
|
341
|
+
onUpdate: { type: Function, default: void 0 },
|
|
342
|
+
onSelectionChange: { type: Function, default: void 0 },
|
|
343
|
+
onFocus: { type: Function, default: void 0 },
|
|
344
|
+
onBlur: { type: Function, default: void 0 },
|
|
345
|
+
onDestroy: { type: Function, default: void 0 }
|
|
346
|
+
},
|
|
347
|
+
emits: {
|
|
348
|
+
"update:modelValue": (_value) => true
|
|
349
|
+
},
|
|
350
|
+
setup(props, { slots, emit, expose }) {
|
|
351
|
+
const { editor, editorRef } = useEditor({
|
|
352
|
+
...props.extensions && { extensions: props.extensions },
|
|
353
|
+
content: props.modelValue ?? props.content ?? "",
|
|
354
|
+
editable: props.editable,
|
|
355
|
+
autofocus: props.autofocus,
|
|
356
|
+
immediatelyRender: props.immediatelyRender,
|
|
357
|
+
outputFormat: props.outputFormat,
|
|
358
|
+
...props.onCreate && { onCreate: props.onCreate },
|
|
359
|
+
...props.onUpdate && { onUpdate: props.onUpdate },
|
|
360
|
+
...props.onSelectionChange && { onSelectionChange: props.onSelectionChange },
|
|
361
|
+
...props.onFocus && { onFocus: props.onFocus },
|
|
362
|
+
...props.onBlur && { onBlur: props.onBlur },
|
|
363
|
+
...props.onDestroy && { onDestroy: props.onDestroy }
|
|
364
|
+
});
|
|
365
|
+
const state = useEditorState(editor);
|
|
366
|
+
expose({
|
|
367
|
+
editor,
|
|
368
|
+
htmlContent: state.htmlContent,
|
|
369
|
+
jsonContent: state.jsonContent,
|
|
370
|
+
isEmpty: state.isEmpty,
|
|
371
|
+
isFocused: state.isFocused
|
|
372
|
+
});
|
|
373
|
+
provideEditor(editor);
|
|
374
|
+
const prevModelValue = ref(props.modelValue);
|
|
375
|
+
watch(
|
|
376
|
+
() => props.modelValue,
|
|
377
|
+
(newValue) => {
|
|
378
|
+
if (newValue === void 0) return;
|
|
379
|
+
const ed = editor.value;
|
|
380
|
+
if (!ed || ed.isDestroyed) return;
|
|
381
|
+
if (newValue === prevModelValue.value) return;
|
|
382
|
+
prevModelValue.value = newValue;
|
|
383
|
+
if (props.outputFormat === "html") {
|
|
384
|
+
if (newValue !== ed.getHTML()) {
|
|
385
|
+
ed.setContent(newValue, false);
|
|
386
|
+
}
|
|
387
|
+
} else {
|
|
388
|
+
if (JSON.stringify(newValue) !== JSON.stringify(ed.getJSON())) {
|
|
389
|
+
ed.setContent(newValue, false);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
},
|
|
393
|
+
{ flush: "post" }
|
|
394
|
+
);
|
|
395
|
+
watch(editor, (ed, _oldEd, onCleanup) => {
|
|
396
|
+
if (!ed || ed.isDestroyed) return;
|
|
397
|
+
const handler = () => {
|
|
398
|
+
const val = props.outputFormat === "html" ? ed.getHTML() : ed.getJSON();
|
|
399
|
+
prevModelValue.value = val;
|
|
400
|
+
emit("update:modelValue", val);
|
|
401
|
+
};
|
|
402
|
+
ed.on("update", handler);
|
|
403
|
+
onCleanup(() => {
|
|
404
|
+
ed.off("update", handler);
|
|
405
|
+
});
|
|
406
|
+
}, { immediate: true });
|
|
407
|
+
return () => {
|
|
408
|
+
const classes = props.class ? `dm-editor ${props.class}` : "dm-editor";
|
|
409
|
+
return [
|
|
410
|
+
h("div", { class: classes, "data-dm-editor-ui": "" }, [h("div", { ref: editorRef })]),
|
|
411
|
+
slots["default"]?.()
|
|
412
|
+
];
|
|
413
|
+
};
|
|
414
|
+
}
|
|
415
|
+
});
|
|
416
|
+
var EditorContent = defineComponent({
|
|
417
|
+
name: "EditorContent",
|
|
418
|
+
props: {
|
|
419
|
+
editor: {
|
|
420
|
+
type: Object,
|
|
421
|
+
default: null
|
|
422
|
+
},
|
|
423
|
+
class: {
|
|
424
|
+
type: String,
|
|
425
|
+
default: void 0
|
|
426
|
+
}
|
|
427
|
+
},
|
|
428
|
+
setup(props) {
|
|
429
|
+
const containerRef = ref();
|
|
430
|
+
watchEffect(() => {
|
|
431
|
+
const container = containerRef.value;
|
|
432
|
+
const editor = props.editor;
|
|
433
|
+
if (!container || !editor || editor.isDestroyed) return;
|
|
434
|
+
const editorDom = editor.view.dom;
|
|
435
|
+
if (editorDom.parentElement !== container) {
|
|
436
|
+
container.appendChild(editorDom);
|
|
437
|
+
}
|
|
438
|
+
});
|
|
439
|
+
return () => h("div", {
|
|
440
|
+
ref: containerRef,
|
|
441
|
+
class: props.class
|
|
442
|
+
});
|
|
443
|
+
}
|
|
444
|
+
});
|
|
445
|
+
function useToolbarController(editor, layout) {
|
|
446
|
+
const groups = shallowRef([]);
|
|
447
|
+
const focusedIndex = ref(0);
|
|
448
|
+
const openDropdown = ref(null);
|
|
449
|
+
const activeVersion = useDebouncedRef(0);
|
|
450
|
+
let controller = null;
|
|
451
|
+
const toolbarRef = ref();
|
|
452
|
+
let cleanupFloating = null;
|
|
453
|
+
let clickOutsideHandler = null;
|
|
454
|
+
let dismissOverlayHandler = null;
|
|
455
|
+
let editorEl = null;
|
|
456
|
+
let syncStateRaf = 0;
|
|
457
|
+
function syncState() {
|
|
458
|
+
cancelAnimationFrame(syncStateRaf);
|
|
459
|
+
syncStateRaf = requestAnimationFrame(() => {
|
|
460
|
+
if (!controller) return;
|
|
461
|
+
const controllerGroups = controller.groups;
|
|
462
|
+
if (groups.value.length !== controllerGroups.length) {
|
|
463
|
+
groups.value = controllerGroups;
|
|
464
|
+
}
|
|
465
|
+
focusedIndex.value = controller.focusedIndex;
|
|
466
|
+
openDropdown.value = controller.openDropdown;
|
|
467
|
+
activeVersion.value++;
|
|
468
|
+
});
|
|
469
|
+
}
|
|
470
|
+
watch(
|
|
471
|
+
editor,
|
|
472
|
+
(ed) => {
|
|
473
|
+
if (controller || !ed || ed.isDestroyed) return;
|
|
474
|
+
controller = new ToolbarController(
|
|
475
|
+
ed,
|
|
476
|
+
syncState,
|
|
477
|
+
layout
|
|
478
|
+
);
|
|
479
|
+
controller.subscribe();
|
|
480
|
+
syncState();
|
|
481
|
+
clickOutsideHandler = (e) => {
|
|
482
|
+
if (controller?.openDropdown && toolbarRef.value && !toolbarRef.value.contains(e.target)) {
|
|
483
|
+
cleanupFloating?.();
|
|
484
|
+
cleanupFloating = null;
|
|
485
|
+
controller.closeDropdown();
|
|
486
|
+
syncState();
|
|
487
|
+
}
|
|
488
|
+
};
|
|
489
|
+
document.addEventListener("mousedown", clickOutsideHandler);
|
|
490
|
+
editorEl = ed.view.dom.closest(".dm-editor");
|
|
491
|
+
if (editorEl) {
|
|
492
|
+
dismissOverlayHandler = () => {
|
|
493
|
+
if (controller?.openDropdown) {
|
|
494
|
+
cleanupFloating?.();
|
|
495
|
+
cleanupFloating = null;
|
|
496
|
+
controller.closeDropdown();
|
|
497
|
+
syncState();
|
|
498
|
+
}
|
|
499
|
+
};
|
|
500
|
+
editorEl.addEventListener("dm:dismiss-overlays", dismissOverlayHandler);
|
|
501
|
+
}
|
|
502
|
+
},
|
|
503
|
+
{ immediate: true }
|
|
504
|
+
);
|
|
505
|
+
onScopeDispose(() => {
|
|
506
|
+
cancelAnimationFrame(syncStateRaf);
|
|
507
|
+
cleanupFloating?.();
|
|
508
|
+
cleanupFloating = null;
|
|
509
|
+
if (clickOutsideHandler) {
|
|
510
|
+
document.removeEventListener("mousedown", clickOutsideHandler);
|
|
511
|
+
clickOutsideHandler = null;
|
|
512
|
+
}
|
|
513
|
+
if (dismissOverlayHandler && editorEl) {
|
|
514
|
+
editorEl.removeEventListener("dm:dismiss-overlays", dismissOverlayHandler);
|
|
515
|
+
dismissOverlayHandler = null;
|
|
516
|
+
editorEl = null;
|
|
517
|
+
}
|
|
518
|
+
controller?.destroy();
|
|
519
|
+
controller = null;
|
|
520
|
+
});
|
|
521
|
+
function isActive(name) {
|
|
522
|
+
return controller?.activeMap.get(name) ?? false;
|
|
523
|
+
}
|
|
524
|
+
function isDisabled(name) {
|
|
525
|
+
return controller?.disabledMap.get(name) ?? false;
|
|
526
|
+
}
|
|
527
|
+
function isDropdownActive(dropdown) {
|
|
528
|
+
if (dropdown.layout === "grid") return false;
|
|
529
|
+
if (dropdown.dynamicLabel) return false;
|
|
530
|
+
if (!controller) return false;
|
|
531
|
+
return dropdown.items.some((item) => controller.activeMap.get(item.name) ?? false);
|
|
532
|
+
}
|
|
533
|
+
function getAriaExpanded(item) {
|
|
534
|
+
if (!item.emitEvent) return null;
|
|
535
|
+
return controller?.expandedMap.get(item.name) ? "true" : null;
|
|
536
|
+
}
|
|
537
|
+
function getFlatIndex(name) {
|
|
538
|
+
return controller?.getFlatIndex(name) ?? -1;
|
|
539
|
+
}
|
|
540
|
+
function handleDropdownToggle(dropdown) {
|
|
541
|
+
if (!controller) return;
|
|
542
|
+
cleanupFloating?.();
|
|
543
|
+
cleanupFloating = null;
|
|
544
|
+
controller.toggleDropdown(dropdown.name);
|
|
545
|
+
syncState();
|
|
546
|
+
if (controller.openDropdown) {
|
|
547
|
+
requestAnimationFrame(() => {
|
|
548
|
+
const trigger = toolbarRef.value?.querySelector('[aria-expanded="true"]');
|
|
549
|
+
const panel = trigger?.parentElement?.querySelector(".dm-toolbar-dropdown-panel");
|
|
550
|
+
if (trigger && panel) {
|
|
551
|
+
const placement = dropdown.layout === "grid" ? "bottom" : "bottom-start";
|
|
552
|
+
cleanupFloating = positionFloatingOnce(trigger, panel, {
|
|
553
|
+
placement,
|
|
554
|
+
offsetValue: 4
|
|
555
|
+
});
|
|
556
|
+
}
|
|
557
|
+
});
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
function closeDropdown() {
|
|
561
|
+
cleanupFloating?.();
|
|
562
|
+
cleanupFloating = null;
|
|
563
|
+
controller?.closeDropdown();
|
|
564
|
+
syncState();
|
|
565
|
+
}
|
|
566
|
+
function executeCommand(item) {
|
|
567
|
+
controller?.executeCommand(item);
|
|
568
|
+
}
|
|
569
|
+
return {
|
|
570
|
+
controller: { get current() {
|
|
571
|
+
return controller;
|
|
572
|
+
} },
|
|
573
|
+
groups,
|
|
574
|
+
focusedIndex,
|
|
575
|
+
openDropdown,
|
|
576
|
+
activeVersion,
|
|
577
|
+
toolbarRef,
|
|
578
|
+
isActive,
|
|
579
|
+
isDisabled,
|
|
580
|
+
isDropdownActive,
|
|
581
|
+
getAriaExpanded,
|
|
582
|
+
getFlatIndex,
|
|
583
|
+
handleDropdownToggle,
|
|
584
|
+
closeDropdown,
|
|
585
|
+
executeCommand,
|
|
586
|
+
syncState
|
|
587
|
+
};
|
|
588
|
+
}
|
|
589
|
+
var DROPDOWN_CARET = '<svg class="dm-dropdown-caret" width="10" height="10" viewBox="0 0 10 10"><path d="M2 4l3 3 3-3" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>';
|
|
590
|
+
function useToolbarIcons(icons) {
|
|
591
|
+
const cache = /* @__PURE__ */ new Map();
|
|
592
|
+
let prevIcons = icons;
|
|
593
|
+
function checkCacheInvalidation(currentIcons) {
|
|
594
|
+
if (currentIcons !== prevIcons) {
|
|
595
|
+
cache.clear();
|
|
596
|
+
prevIcons = currentIcons;
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
function resolveIconSvg(name) {
|
|
600
|
+
if (icons) {
|
|
601
|
+
return icons[name] ?? "";
|
|
602
|
+
}
|
|
603
|
+
return defaultIcons[name] ?? "";
|
|
604
|
+
}
|
|
605
|
+
function getCachedIcon(name) {
|
|
606
|
+
checkCacheInvalidation(icons);
|
|
607
|
+
const key = `i:${name}`;
|
|
608
|
+
let cached = cache.get(key);
|
|
609
|
+
if (!cached) {
|
|
610
|
+
cached = resolveIconSvg(name);
|
|
611
|
+
cache.set(key, cached);
|
|
612
|
+
}
|
|
613
|
+
return cached;
|
|
614
|
+
}
|
|
615
|
+
function getCachedTriggerLabel(label, isIcon) {
|
|
616
|
+
checkCacheInvalidation(icons);
|
|
617
|
+
const key = `tl:${label}:${isIcon ? "1" : "0"}`;
|
|
618
|
+
let cached = cache.get(key);
|
|
619
|
+
if (!cached) {
|
|
620
|
+
const content = isIcon ? resolveIconSvg(label) : label;
|
|
621
|
+
cached = `<span class="dm-toolbar-trigger-label">${content}</span>${DROPDOWN_CARET}`;
|
|
622
|
+
cache.set(key, cached);
|
|
623
|
+
}
|
|
624
|
+
return cached;
|
|
625
|
+
}
|
|
626
|
+
function getCachedTriggerIcon(iconName) {
|
|
627
|
+
checkCacheInvalidation(icons);
|
|
628
|
+
const key = `t:${iconName}`;
|
|
629
|
+
let cached = cache.get(key);
|
|
630
|
+
if (!cached) {
|
|
631
|
+
cached = resolveIconSvg(iconName) + DROPDOWN_CARET;
|
|
632
|
+
cache.set(key, cached);
|
|
633
|
+
}
|
|
634
|
+
return cached;
|
|
635
|
+
}
|
|
636
|
+
function getCachedItemContent(iconName, label, displayMode) {
|
|
637
|
+
const mode = displayMode ?? "icon-text";
|
|
638
|
+
checkCacheInvalidation(icons);
|
|
639
|
+
const key = `dc:${iconName}:${label}:${mode}`;
|
|
640
|
+
let cached = cache.get(key);
|
|
641
|
+
if (!cached) {
|
|
642
|
+
if (mode === "text") {
|
|
643
|
+
cached = label;
|
|
644
|
+
} else if (mode === "icon") {
|
|
645
|
+
cached = resolveIconSvg(iconName);
|
|
646
|
+
} else {
|
|
647
|
+
cached = resolveIconSvg(iconName) + " " + label;
|
|
648
|
+
}
|
|
649
|
+
cache.set(key, cached);
|
|
650
|
+
}
|
|
651
|
+
return cached;
|
|
652
|
+
}
|
|
653
|
+
function getDropdownTriggerHtml(dropdown, activeItem) {
|
|
654
|
+
checkCacheInvalidation(icons);
|
|
655
|
+
if (dropdown.layout === "grid") {
|
|
656
|
+
const color = activeItem?.color ?? dropdown.defaultIndicatorColor ?? null;
|
|
657
|
+
const key = `tr:${dropdown.icon}:${color ?? ""}`;
|
|
658
|
+
let cached = cache.get(key);
|
|
659
|
+
if (!cached) {
|
|
660
|
+
cached = resolveIconSvg(dropdown.icon) + DROPDOWN_CARET;
|
|
661
|
+
if (color) {
|
|
662
|
+
cached += `<span class="dm-toolbar-color-indicator" style="background-color: ${color}"></span>`;
|
|
663
|
+
}
|
|
664
|
+
cache.set(key, cached);
|
|
665
|
+
}
|
|
666
|
+
return cached;
|
|
667
|
+
}
|
|
668
|
+
if (dropdown.dynamicLabel) {
|
|
669
|
+
if (activeItem) return getCachedTriggerLabel(activeItem.label);
|
|
670
|
+
if (dropdown.dynamicLabelFallback) return getCachedTriggerLabel(dropdown.dynamicLabelFallback);
|
|
671
|
+
return getCachedTriggerLabel(dropdown.icon, true);
|
|
672
|
+
}
|
|
673
|
+
const icon = dropdown.dynamicIcon && activeItem ? activeItem.icon : dropdown.icon;
|
|
674
|
+
return getCachedTriggerIcon(icon);
|
|
675
|
+
}
|
|
676
|
+
return {
|
|
677
|
+
resolveIconSvg,
|
|
678
|
+
getCachedIcon,
|
|
679
|
+
getCachedTriggerLabel,
|
|
680
|
+
getCachedTriggerIcon,
|
|
681
|
+
getCachedItemContent,
|
|
682
|
+
getDropdownTriggerHtml
|
|
683
|
+
};
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
// src/toolbar/useTooltip.ts
|
|
687
|
+
var isMac = typeof navigator !== "undefined" && /Mac|iPhone|iPad|iPod/.test(navigator.userAgent);
|
|
688
|
+
var MODIFIER_MAP = isMac ? { Mod: "\u2318", Shift: "\u21E7", Alt: "\u2325" } : { Mod: "Ctrl", Shift: "Shift", Alt: "Alt" };
|
|
689
|
+
function useTooltip() {
|
|
690
|
+
function getTooltip(item) {
|
|
691
|
+
if (!item.shortcut) return item.label;
|
|
692
|
+
const parts = item.shortcut.split("-").map((part) => MODIFIER_MAP[part] ?? part);
|
|
693
|
+
const shortcut = isMac ? parts.join("") : parts.join("+");
|
|
694
|
+
return `${item.label} (${shortcut})`;
|
|
695
|
+
}
|
|
696
|
+
return { getTooltip };
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
// src/toolbar/useKeyboardNav.ts
|
|
700
|
+
function useKeyboardNav(controllerRef, toolbarRef, closeDropdown) {
|
|
701
|
+
function focusCurrentButton() {
|
|
702
|
+
const buttons = toolbarRef.value?.querySelectorAll(".dm-toolbar-button");
|
|
703
|
+
const controller = controllerRef.current;
|
|
704
|
+
if (buttons && controller) {
|
|
705
|
+
const btn = buttons[controller.focusedIndex];
|
|
706
|
+
btn?.focus();
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
function focusDropdownItem(direction, first) {
|
|
710
|
+
const panel = toolbarRef.value?.querySelector(".dm-toolbar-dropdown-panel");
|
|
711
|
+
if (!panel) return;
|
|
712
|
+
const items = Array.from(panel.querySelectorAll('[role="menuitem"]'));
|
|
713
|
+
if (!items.length) return;
|
|
714
|
+
if (first) {
|
|
715
|
+
items[0]?.focus();
|
|
716
|
+
return;
|
|
717
|
+
}
|
|
718
|
+
const current = document.activeElement;
|
|
719
|
+
const idx = items.indexOf(current);
|
|
720
|
+
const next = idx === -1 ? direction > 0 ? 0 : items.length - 1 : (idx + direction + items.length) % items.length;
|
|
721
|
+
items[next]?.focus();
|
|
722
|
+
}
|
|
723
|
+
function onKeyDown(event) {
|
|
724
|
+
const controller = controllerRef.current;
|
|
725
|
+
if (!controller) return;
|
|
726
|
+
switch (event.key) {
|
|
727
|
+
case "ArrowRight":
|
|
728
|
+
event.preventDefault();
|
|
729
|
+
controller.navigateNext();
|
|
730
|
+
focusCurrentButton();
|
|
731
|
+
break;
|
|
732
|
+
case "ArrowLeft":
|
|
733
|
+
event.preventDefault();
|
|
734
|
+
controller.navigatePrev();
|
|
735
|
+
focusCurrentButton();
|
|
736
|
+
break;
|
|
737
|
+
case "ArrowDown": {
|
|
738
|
+
event.preventDefault();
|
|
739
|
+
if (controller.openDropdown) {
|
|
740
|
+
focusDropdownItem(1);
|
|
741
|
+
} else {
|
|
742
|
+
const btn = document.activeElement;
|
|
743
|
+
if (btn?.getAttribute("aria-haspopup") && btn.closest(".dm-toolbar")) {
|
|
744
|
+
btn.click();
|
|
745
|
+
requestAnimationFrame(() => focusDropdownItem(0, true));
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
break;
|
|
749
|
+
}
|
|
750
|
+
case "ArrowUp": {
|
|
751
|
+
event.preventDefault();
|
|
752
|
+
if (controller.openDropdown) {
|
|
753
|
+
focusDropdownItem(-1);
|
|
754
|
+
}
|
|
755
|
+
break;
|
|
756
|
+
}
|
|
757
|
+
case "Home":
|
|
758
|
+
event.preventDefault();
|
|
759
|
+
controller.navigateFirst();
|
|
760
|
+
focusCurrentButton();
|
|
761
|
+
break;
|
|
762
|
+
case "End":
|
|
763
|
+
event.preventDefault();
|
|
764
|
+
controller.navigateLast();
|
|
765
|
+
focusCurrentButton();
|
|
766
|
+
break;
|
|
767
|
+
case "Escape":
|
|
768
|
+
if (controller.openDropdown) {
|
|
769
|
+
event.preventDefault();
|
|
770
|
+
closeDropdown();
|
|
771
|
+
focusCurrentButton();
|
|
772
|
+
}
|
|
773
|
+
break;
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
return { onKeyDown, focusCurrentButton };
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
// src/toolbar/useComputedStyle.ts
|
|
780
|
+
function getComputedStyleAtCursor(editor, prop) {
|
|
781
|
+
try {
|
|
782
|
+
const { from } = editor.state.selection;
|
|
783
|
+
const domAtPos = editor.view.domAtPos(from);
|
|
784
|
+
let node = domAtPos.node;
|
|
785
|
+
if (!(node instanceof HTMLElement)) {
|
|
786
|
+
node = node.parentElement;
|
|
787
|
+
}
|
|
788
|
+
if (!node) return null;
|
|
789
|
+
const el = node;
|
|
790
|
+
const inline = el.style.getPropertyValue(prop);
|
|
791
|
+
if (inline) return inline;
|
|
792
|
+
return window.getComputedStyle(el).getPropertyValue(prop) || null;
|
|
793
|
+
} catch {
|
|
794
|
+
return null;
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
function getInlineStyleAtCursor(editor, prop) {
|
|
798
|
+
try {
|
|
799
|
+
const { from } = editor.state.selection;
|
|
800
|
+
const domAtPos = editor.view.domAtPos(from);
|
|
801
|
+
let node = domAtPos.node;
|
|
802
|
+
if (!(node instanceof HTMLElement)) {
|
|
803
|
+
node = node.parentElement;
|
|
804
|
+
}
|
|
805
|
+
if (!node) return null;
|
|
806
|
+
return node.style.getPropertyValue(prop) || null;
|
|
807
|
+
} catch {
|
|
808
|
+
return null;
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
var ToolbarButton = defineComponent({
|
|
812
|
+
name: "ToolbarButton",
|
|
813
|
+
props: {
|
|
814
|
+
item: { type: Object, required: true },
|
|
815
|
+
isActive: { type: Boolean, required: true },
|
|
816
|
+
isDisabled: { type: Boolean, required: true },
|
|
817
|
+
tabIndex: { type: Number, required: true },
|
|
818
|
+
tooltip: { type: String, required: true },
|
|
819
|
+
iconHtml: { type: String, required: true },
|
|
820
|
+
ariaExpanded: { type: String, default: null }
|
|
821
|
+
},
|
|
822
|
+
emits: {
|
|
823
|
+
click: (_item, _event) => true,
|
|
824
|
+
focus: (_name) => true
|
|
825
|
+
},
|
|
826
|
+
setup(props, { emit }) {
|
|
827
|
+
return () => h("button", {
|
|
828
|
+
type: "button",
|
|
829
|
+
class: ["dm-toolbar-button", props.isActive && "dm-toolbar-button--active"],
|
|
830
|
+
disabled: props.isDisabled,
|
|
831
|
+
tabindex: props.tabIndex,
|
|
832
|
+
innerHTML: props.iconHtml,
|
|
833
|
+
"aria-pressed": props.isActive,
|
|
834
|
+
"aria-expanded": props.ariaExpanded === "true" ? true : void 0,
|
|
835
|
+
"aria-label": props.item.label,
|
|
836
|
+
title: props.tooltip,
|
|
837
|
+
onMousedown: (e) => e.preventDefault(),
|
|
838
|
+
onClick: (e) => emit("click", props.item, e),
|
|
839
|
+
onFocus: () => emit("focus", props.item.name)
|
|
840
|
+
});
|
|
841
|
+
}
|
|
842
|
+
});
|
|
843
|
+
var ToolbarDropdownPanel = defineComponent({
|
|
844
|
+
name: "ToolbarDropdownPanel",
|
|
845
|
+
props: {
|
|
846
|
+
dropdown: { type: Object, required: true },
|
|
847
|
+
isActive: { type: Function, required: true },
|
|
848
|
+
getCachedItemContent: {
|
|
849
|
+
type: Function,
|
|
850
|
+
required: true
|
|
851
|
+
}
|
|
852
|
+
},
|
|
853
|
+
emits: ["itemClick"],
|
|
854
|
+
setup(props, { emit }) {
|
|
855
|
+
return () => {
|
|
856
|
+
const { dropdown, isActive, getCachedItemContent } = props;
|
|
857
|
+
if (dropdown.layout === "grid") {
|
|
858
|
+
return h(
|
|
859
|
+
"div",
|
|
860
|
+
{
|
|
861
|
+
class: "dm-toolbar-dropdown-panel dm-color-palette",
|
|
862
|
+
role: "menu",
|
|
863
|
+
style: { "--dm-palette-columns": String(dropdown.gridColumns ?? 10) }
|
|
864
|
+
},
|
|
865
|
+
dropdown.items.map(
|
|
866
|
+
(sub) => sub.color ? h("button", {
|
|
867
|
+
key: sub.name,
|
|
868
|
+
type: "button",
|
|
869
|
+
class: ["dm-color-swatch", isActive(sub.name) && "dm-color-swatch--active"],
|
|
870
|
+
role: "menuitem",
|
|
871
|
+
tabindex: -1,
|
|
872
|
+
"aria-label": sub.label,
|
|
873
|
+
title: sub.label,
|
|
874
|
+
style: { backgroundColor: sub.color },
|
|
875
|
+
onMousedown: (e) => e.preventDefault(),
|
|
876
|
+
onClick: (e) => emit("itemClick", sub, e)
|
|
877
|
+
}) : h("button", {
|
|
878
|
+
key: sub.name,
|
|
879
|
+
type: "button",
|
|
880
|
+
class: "dm-color-palette-reset",
|
|
881
|
+
role: "menuitem",
|
|
882
|
+
tabindex: -1,
|
|
883
|
+
"aria-label": sub.label,
|
|
884
|
+
innerHTML: getCachedItemContent(sub.icon, sub.label),
|
|
885
|
+
onMousedown: (e) => e.preventDefault(),
|
|
886
|
+
onClick: (e) => emit("itemClick", sub, e)
|
|
887
|
+
})
|
|
888
|
+
)
|
|
889
|
+
);
|
|
890
|
+
}
|
|
891
|
+
return h(
|
|
892
|
+
"div",
|
|
893
|
+
{
|
|
894
|
+
class: "dm-toolbar-dropdown-panel",
|
|
895
|
+
role: "menu",
|
|
896
|
+
"data-display-mode": dropdown.displayMode ?? null
|
|
897
|
+
},
|
|
898
|
+
dropdown.items.map(
|
|
899
|
+
(sub) => h("button", {
|
|
900
|
+
key: sub.name,
|
|
901
|
+
type: "button",
|
|
902
|
+
class: ["dm-toolbar-dropdown-item", isActive(sub.name) && "dm-toolbar-dropdown-item--active"],
|
|
903
|
+
role: "menuitem",
|
|
904
|
+
tabindex: -1,
|
|
905
|
+
"aria-label": sub.label,
|
|
906
|
+
title: sub.label,
|
|
907
|
+
innerHTML: getCachedItemContent(sub.icon, sub.label, dropdown.displayMode),
|
|
908
|
+
onVnodeMounted: (vnode) => {
|
|
909
|
+
if (sub.style && vnode.el) vnode.el.setAttribute("style", sub.style);
|
|
910
|
+
},
|
|
911
|
+
onMousedown: (e) => e.preventDefault(),
|
|
912
|
+
onClick: (e) => emit("itemClick", sub, e)
|
|
913
|
+
})
|
|
914
|
+
)
|
|
915
|
+
);
|
|
916
|
+
};
|
|
917
|
+
}
|
|
918
|
+
});
|
|
919
|
+
|
|
920
|
+
// src/toolbar/ToolbarDropdown.ts
|
|
921
|
+
var ToolbarDropdown = defineComponent({
|
|
922
|
+
name: "ToolbarDropdown",
|
|
923
|
+
props: {
|
|
924
|
+
dropdown: { type: Object, required: true },
|
|
925
|
+
isOpen: { type: Boolean, required: true },
|
|
926
|
+
isActive: { type: Function, required: true },
|
|
927
|
+
isDropdownActive: { type: Boolean, required: true },
|
|
928
|
+
isDisabled: { type: Boolean, required: true },
|
|
929
|
+
tabIndex: { type: Number, required: true },
|
|
930
|
+
triggerHtml: { type: String, required: true },
|
|
931
|
+
getCachedItemContent: {
|
|
932
|
+
type: Function,
|
|
933
|
+
required: true
|
|
934
|
+
}
|
|
935
|
+
},
|
|
936
|
+
emits: ["toggle", "itemClick", "focus"],
|
|
937
|
+
setup(props, { emit }) {
|
|
938
|
+
return () => {
|
|
939
|
+
const children = [
|
|
940
|
+
h("button", {
|
|
941
|
+
type: "button",
|
|
942
|
+
class: ["dm-toolbar-button", "dm-toolbar-dropdown-trigger", props.isDropdownActive && "dm-toolbar-button--active"],
|
|
943
|
+
"aria-expanded": props.isOpen,
|
|
944
|
+
"aria-haspopup": "true",
|
|
945
|
+
"aria-label": props.dropdown.label,
|
|
946
|
+
title: props.dropdown.label,
|
|
947
|
+
tabindex: props.tabIndex,
|
|
948
|
+
disabled: props.isDisabled,
|
|
949
|
+
"data-dropdown": props.dropdown.name,
|
|
950
|
+
innerHTML: props.triggerHtml,
|
|
951
|
+
onMousedown: (e) => e.preventDefault(),
|
|
952
|
+
onClick: () => emit("toggle", props.dropdown),
|
|
953
|
+
onFocus: () => emit("focus", props.dropdown.name)
|
|
954
|
+
})
|
|
955
|
+
];
|
|
956
|
+
if (props.isOpen) {
|
|
957
|
+
children.push(
|
|
958
|
+
h(ToolbarDropdownPanel, {
|
|
959
|
+
dropdown: props.dropdown,
|
|
960
|
+
isActive: props.isActive,
|
|
961
|
+
getCachedItemContent: props.getCachedItemContent,
|
|
962
|
+
onItemClick: (item, event) => emit("itemClick", item, event)
|
|
963
|
+
})
|
|
964
|
+
);
|
|
965
|
+
}
|
|
966
|
+
return h("div", { class: "dm-toolbar-dropdown-wrapper" }, children);
|
|
967
|
+
};
|
|
968
|
+
}
|
|
969
|
+
});
|
|
970
|
+
|
|
971
|
+
// src/toolbar/DomternalToolbar.ts
|
|
972
|
+
var DomternalToolbar = defineComponent({
|
|
973
|
+
name: "DomternalToolbar",
|
|
974
|
+
props: {
|
|
975
|
+
editor: { type: Object, default: void 0 },
|
|
976
|
+
icons: { type: Object, default: void 0 },
|
|
977
|
+
layout: { type: Array, default: void 0 }
|
|
978
|
+
},
|
|
979
|
+
setup(props) {
|
|
980
|
+
const { editor: contextEditor } = useCurrentEditor();
|
|
981
|
+
const {
|
|
982
|
+
controller: controllerRef,
|
|
983
|
+
groups,
|
|
984
|
+
focusedIndex,
|
|
985
|
+
openDropdown,
|
|
986
|
+
activeVersion,
|
|
987
|
+
toolbarRef,
|
|
988
|
+
isActive,
|
|
989
|
+
isDisabled,
|
|
990
|
+
isDropdownActive,
|
|
991
|
+
getAriaExpanded,
|
|
992
|
+
getFlatIndex,
|
|
993
|
+
handleDropdownToggle,
|
|
994
|
+
closeDropdown,
|
|
995
|
+
executeCommand
|
|
996
|
+
} = useToolbarController(
|
|
997
|
+
computed(() => props.editor ?? contextEditor.value),
|
|
998
|
+
props.layout
|
|
999
|
+
);
|
|
1000
|
+
const {
|
|
1001
|
+
getCachedIcon,
|
|
1002
|
+
getCachedItemContent,
|
|
1003
|
+
getDropdownTriggerHtml
|
|
1004
|
+
} = useToolbarIcons(props.icons);
|
|
1005
|
+
const { getTooltip } = useTooltip();
|
|
1006
|
+
const { onKeyDown } = useKeyboardNav(controllerRef, toolbarRef, closeDropdown);
|
|
1007
|
+
function onButtonClick(item, event) {
|
|
1008
|
+
const editor = props.editor ?? contextEditor.value;
|
|
1009
|
+
if (!editor) return;
|
|
1010
|
+
if (controllerRef.current?.openDropdown) {
|
|
1011
|
+
closeDropdown();
|
|
1012
|
+
}
|
|
1013
|
+
if (item.emitEvent) {
|
|
1014
|
+
const anchor = event?.target?.closest?.(".dm-toolbar-button") ?? event?.target;
|
|
1015
|
+
editor.emit(item.emitEvent, { anchorElement: anchor });
|
|
1016
|
+
return;
|
|
1017
|
+
}
|
|
1018
|
+
executeCommand(item);
|
|
1019
|
+
requestAnimationFrame(() => editor.view.focus());
|
|
1020
|
+
}
|
|
1021
|
+
function onDropdownItemClick(item, event) {
|
|
1022
|
+
const editor = props.editor ?? contextEditor.value;
|
|
1023
|
+
if (!editor) return;
|
|
1024
|
+
let anchor;
|
|
1025
|
+
if (item.emitEvent) {
|
|
1026
|
+
const wrapper = event.target?.closest?.(".dm-toolbar-dropdown-wrapper");
|
|
1027
|
+
anchor = wrapper?.querySelector(".dm-toolbar-dropdown-trigger");
|
|
1028
|
+
}
|
|
1029
|
+
closeDropdown();
|
|
1030
|
+
if (item.emitEvent) {
|
|
1031
|
+
editor.emit(item.emitEvent, { anchorElement: anchor });
|
|
1032
|
+
} else {
|
|
1033
|
+
executeCommand(item);
|
|
1034
|
+
}
|
|
1035
|
+
requestAnimationFrame(() => editor.view.focus());
|
|
1036
|
+
}
|
|
1037
|
+
function onButtonFocus(name) {
|
|
1038
|
+
const index = controllerRef.current?.getFlatIndex(name) ?? -1;
|
|
1039
|
+
if (index >= 0) {
|
|
1040
|
+
controllerRef.current?.setFocusedIndex(index);
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
return () => {
|
|
1044
|
+
const editor = props.editor ?? contextEditor.value;
|
|
1045
|
+
if (!editor) return null;
|
|
1046
|
+
void activeVersion.value;
|
|
1047
|
+
return h(
|
|
1048
|
+
"div",
|
|
1049
|
+
{
|
|
1050
|
+
ref: toolbarRef,
|
|
1051
|
+
class: "dm-toolbar",
|
|
1052
|
+
role: "toolbar",
|
|
1053
|
+
"aria-label": "Editor formatting",
|
|
1054
|
+
"data-dm-editor-ui": "",
|
|
1055
|
+
onKeydown: onKeyDown
|
|
1056
|
+
},
|
|
1057
|
+
groups.value.map(
|
|
1058
|
+
(group, gi) => h(Fragment, { key: group.name }, [
|
|
1059
|
+
gi > 0 ? h("div", { class: "dm-toolbar-separator", role: "separator" }) : null,
|
|
1060
|
+
h(
|
|
1061
|
+
"div",
|
|
1062
|
+
{ class: "dm-toolbar-group", role: "group", "aria-label": group.name || "Tools" },
|
|
1063
|
+
group.items.map((item) => {
|
|
1064
|
+
if (item.type === "button") {
|
|
1065
|
+
const btn = item;
|
|
1066
|
+
return h(ToolbarButton, {
|
|
1067
|
+
key: btn.name,
|
|
1068
|
+
item: btn,
|
|
1069
|
+
isActive: isActive(btn.name),
|
|
1070
|
+
isDisabled: isDisabled(btn.name),
|
|
1071
|
+
tabIndex: getFlatIndex(btn.name) === focusedIndex.value ? 0 : -1,
|
|
1072
|
+
tooltip: getTooltip(btn),
|
|
1073
|
+
iconHtml: getCachedIcon(btn.icon),
|
|
1074
|
+
ariaExpanded: getAriaExpanded(btn),
|
|
1075
|
+
onClick: (clickedItem, event) => onButtonClick(clickedItem, event),
|
|
1076
|
+
onFocus: onButtonFocus
|
|
1077
|
+
});
|
|
1078
|
+
}
|
|
1079
|
+
if (item.type === "dropdown") {
|
|
1080
|
+
const dd = item;
|
|
1081
|
+
const activeItem = dd.items.find((sub) => controllerRef.current?.activeMap.get(sub.name));
|
|
1082
|
+
let triggerHtml = getDropdownTriggerHtml(dd, activeItem);
|
|
1083
|
+
if (dd.dynamicLabel && !activeItem && dd.computedStyleProperty) {
|
|
1084
|
+
let computed6;
|
|
1085
|
+
if (dd.computedStyleProperty === "font-family") {
|
|
1086
|
+
computed6 = getInlineStyleAtCursor(editor, dd.computedStyleProperty);
|
|
1087
|
+
if (computed6) {
|
|
1088
|
+
const first = computed6.split(",")[0]?.replace(/['"]+/g, "").trim();
|
|
1089
|
+
computed6 = first || null;
|
|
1090
|
+
}
|
|
1091
|
+
} else {
|
|
1092
|
+
computed6 = getComputedStyleAtCursor(editor, dd.computedStyleProperty);
|
|
1093
|
+
}
|
|
1094
|
+
if (computed6) {
|
|
1095
|
+
triggerHtml = `<span class="dm-toolbar-trigger-label">${computed6}</span>${DROPDOWN_CARET}`;
|
|
1096
|
+
}
|
|
1097
|
+
}
|
|
1098
|
+
return h(ToolbarDropdown, {
|
|
1099
|
+
key: dd.name,
|
|
1100
|
+
dropdown: dd,
|
|
1101
|
+
isOpen: openDropdown.value === dd.name,
|
|
1102
|
+
isActive,
|
|
1103
|
+
isDropdownActive: isDropdownActive(dd),
|
|
1104
|
+
isDisabled: isDisabled(dd.name),
|
|
1105
|
+
tabIndex: getFlatIndex(dd.name) === focusedIndex.value ? 0 : -1,
|
|
1106
|
+
triggerHtml,
|
|
1107
|
+
getCachedItemContent,
|
|
1108
|
+
onToggle: handleDropdownToggle,
|
|
1109
|
+
onItemClick: onDropdownItemClick,
|
|
1110
|
+
onFocus: onButtonFocus
|
|
1111
|
+
});
|
|
1112
|
+
}
|
|
1113
|
+
return null;
|
|
1114
|
+
})
|
|
1115
|
+
)
|
|
1116
|
+
])
|
|
1117
|
+
)
|
|
1118
|
+
);
|
|
1119
|
+
};
|
|
1120
|
+
}
|
|
1121
|
+
});
|
|
1122
|
+
function isInsideTableCell($pos) {
|
|
1123
|
+
for (let d = $pos.depth; d > 0; d--) {
|
|
1124
|
+
const name = $pos.node(d).type.name;
|
|
1125
|
+
if (name === "tableCell" || name === "tableHeader") return true;
|
|
1126
|
+
}
|
|
1127
|
+
return false;
|
|
1128
|
+
}
|
|
1129
|
+
function findCellNode(pos) {
|
|
1130
|
+
for (let d = pos.depth; d > 0; d--) {
|
|
1131
|
+
const node = pos.node(d);
|
|
1132
|
+
if (node.type.name === "tableCell" || node.type.name === "tableHeader") return node;
|
|
1133
|
+
}
|
|
1134
|
+
return null;
|
|
1135
|
+
}
|
|
1136
|
+
function useBubbleMenu(options) {
|
|
1137
|
+
const { editor, shouldShow, placement = "top", offset = 8, updateDelay = 0, items, contexts } = options;
|
|
1138
|
+
const menuRef = ref();
|
|
1139
|
+
const pluginKey = new PluginKey("vueBubbleMenu-" + Math.random().toString(36).slice(2, 8));
|
|
1140
|
+
const resolvedItems = shallowRef([]);
|
|
1141
|
+
const activeVersion = useDebouncedRef(0);
|
|
1142
|
+
const activeMapRef = /* @__PURE__ */ new Map();
|
|
1143
|
+
const disabledMapRef = /* @__PURE__ */ new Map();
|
|
1144
|
+
let itemMap;
|
|
1145
|
+
let bubbleDefaults;
|
|
1146
|
+
let currentResolvedItems = [];
|
|
1147
|
+
let initialized = false;
|
|
1148
|
+
let stopEditorWatch = null;
|
|
1149
|
+
const doInit = (ed) => {
|
|
1150
|
+
if (initialized || !ed || ed.isDestroyed || !menuRef.value) return;
|
|
1151
|
+
initialized = true;
|
|
1152
|
+
itemMap = /* @__PURE__ */ new Map();
|
|
1153
|
+
for (const item of ed.toolbarItems) {
|
|
1154
|
+
if (item.type === "button") {
|
|
1155
|
+
itemMap.set(item.name, item);
|
|
1156
|
+
} else if (item.type === "dropdown") {
|
|
1157
|
+
for (const sub of item.items) {
|
|
1158
|
+
itemMap.set(sub.name, sub);
|
|
1159
|
+
}
|
|
1160
|
+
}
|
|
1161
|
+
}
|
|
1162
|
+
bubbleDefaults = /* @__PURE__ */ new Map();
|
|
1163
|
+
const byCtx = /* @__PURE__ */ new Map();
|
|
1164
|
+
const addItem = (btn) => {
|
|
1165
|
+
const ctx = btn["bubbleMenu"];
|
|
1166
|
+
if (!ctx) return;
|
|
1167
|
+
let arr = byCtx.get(ctx);
|
|
1168
|
+
if (!arr) {
|
|
1169
|
+
arr = [];
|
|
1170
|
+
byCtx.set(ctx, arr);
|
|
1171
|
+
}
|
|
1172
|
+
arr.push(btn);
|
|
1173
|
+
};
|
|
1174
|
+
for (const item of ed.toolbarItems) {
|
|
1175
|
+
if (item.type === "button") addItem(item);
|
|
1176
|
+
else if (item.type === "dropdown") {
|
|
1177
|
+
for (const sub of item.items) addItem(sub);
|
|
1178
|
+
}
|
|
1179
|
+
}
|
|
1180
|
+
for (const [ctx, ctxItems] of byCtx) {
|
|
1181
|
+
ctxItems.sort((a, b) => (b.priority ?? 100) - (a.priority ?? 100));
|
|
1182
|
+
const result = [];
|
|
1183
|
+
let lastGroup;
|
|
1184
|
+
let sepIdx = 0;
|
|
1185
|
+
for (const item of ctxItems) {
|
|
1186
|
+
if (lastGroup !== void 0 && item.group !== lastGroup) {
|
|
1187
|
+
result.push({ type: "separator", name: `bsep-${sepIdx++}` });
|
|
1188
|
+
}
|
|
1189
|
+
result.push(item);
|
|
1190
|
+
lastGroup = item.group;
|
|
1191
|
+
}
|
|
1192
|
+
bubbleDefaults.set(ctx, result);
|
|
1193
|
+
}
|
|
1194
|
+
const resolveNames = (names) => {
|
|
1195
|
+
const result = [];
|
|
1196
|
+
let sepIdx = 0;
|
|
1197
|
+
for (const name of names) {
|
|
1198
|
+
if (name === "|") {
|
|
1199
|
+
result.push({ type: "separator", name: `sep-${sepIdx++}` });
|
|
1200
|
+
} else {
|
|
1201
|
+
const item = itemMap.get(name);
|
|
1202
|
+
if (item) result.push(item);
|
|
1203
|
+
}
|
|
1204
|
+
}
|
|
1205
|
+
return result;
|
|
1206
|
+
};
|
|
1207
|
+
const getFormatItems = () => {
|
|
1208
|
+
return Array.from(itemMap.values()).filter((item) => item.group === "format").sort((a, b) => (b.priority ?? 100) - (a.priority ?? 100));
|
|
1209
|
+
};
|
|
1210
|
+
const detectContext = (selection, ctxs) => {
|
|
1211
|
+
if ("$anchorCell" in selection) return null;
|
|
1212
|
+
if (selection.node) return selection.node.type.name;
|
|
1213
|
+
if (selection.empty) return null;
|
|
1214
|
+
const fromCell = findCellNode(selection.$from);
|
|
1215
|
+
if (fromCell) {
|
|
1216
|
+
const toCell = findCellNode(selection.$to);
|
|
1217
|
+
if (toCell && fromCell !== toCell) return null;
|
|
1218
|
+
return "table";
|
|
1219
|
+
}
|
|
1220
|
+
const fromName = selection.$from.parent.type.name;
|
|
1221
|
+
if (fromName in ctxs) return fromName;
|
|
1222
|
+
if ("text" in ctxs && selection.$from.parent.type.spec.marks !== "") return "text";
|
|
1223
|
+
const toName = selection.$to.parent.type.name;
|
|
1224
|
+
if (toName in ctxs) return toName;
|
|
1225
|
+
if ("text" in ctxs && selection.$to.parent.type.spec.marks !== "") return "text";
|
|
1226
|
+
return null;
|
|
1227
|
+
};
|
|
1228
|
+
const filterBySchema = (contextName, schemaItems) => {
|
|
1229
|
+
if (contextName === "text" || contextName === "table") return schemaItems;
|
|
1230
|
+
const schema = ed.state.schema;
|
|
1231
|
+
if (!schema) return schemaItems;
|
|
1232
|
+
const nodeType = schema.nodes[contextName];
|
|
1233
|
+
if (!nodeType) return schemaItems;
|
|
1234
|
+
return schemaItems.filter((item) => {
|
|
1235
|
+
const markName = typeof item.isActive === "string" ? item.isActive : null;
|
|
1236
|
+
if (!markName) return true;
|
|
1237
|
+
const markType = schema.marks?.[markName];
|
|
1238
|
+
if (!markType) return true;
|
|
1239
|
+
return nodeType.allowsMarkType(markType);
|
|
1240
|
+
});
|
|
1241
|
+
};
|
|
1242
|
+
let shouldShowFn = shouldShow;
|
|
1243
|
+
if (!shouldShowFn) {
|
|
1244
|
+
if (contexts) {
|
|
1245
|
+
shouldShowFn = ({ state }) => {
|
|
1246
|
+
const context = detectContext(state.selection, contexts);
|
|
1247
|
+
if (!context) return false;
|
|
1248
|
+
if (context in contexts) {
|
|
1249
|
+
const val = contexts[context];
|
|
1250
|
+
if (val === null) return false;
|
|
1251
|
+
return val === true || Array.isArray(val) && val.length > 0;
|
|
1252
|
+
}
|
|
1253
|
+
return bubbleDefaults.has(context);
|
|
1254
|
+
};
|
|
1255
|
+
} else {
|
|
1256
|
+
shouldShowFn = ({ state }) => {
|
|
1257
|
+
if (state.selection.empty || state.selection.node) return false;
|
|
1258
|
+
if (isInsideTableCell(state.selection.$from)) return false;
|
|
1259
|
+
return state.selection.$from.parent.type.spec.marks !== "" || state.selection.$to.parent.type.spec.marks !== "";
|
|
1260
|
+
};
|
|
1261
|
+
}
|
|
1262
|
+
}
|
|
1263
|
+
const plugin = createBubbleMenuPlugin({
|
|
1264
|
+
pluginKey,
|
|
1265
|
+
editor: ed,
|
|
1266
|
+
element: menuRef.value,
|
|
1267
|
+
shouldShow: shouldShowFn,
|
|
1268
|
+
placement,
|
|
1269
|
+
offset,
|
|
1270
|
+
updateDelay
|
|
1271
|
+
});
|
|
1272
|
+
ed.registerPlugin(plugin);
|
|
1273
|
+
const setItems = (newItems) => {
|
|
1274
|
+
currentResolvedItems = newItems;
|
|
1275
|
+
resolvedItems.value = newItems;
|
|
1276
|
+
};
|
|
1277
|
+
if (contexts) {
|
|
1278
|
+
updateContextItems(ed, contexts, detectContext, resolveNames, getFormatItems, filterBySchema, bubbleDefaults, setItems);
|
|
1279
|
+
} else if (items) {
|
|
1280
|
+
setItems(resolveNames(items));
|
|
1281
|
+
} else {
|
|
1282
|
+
setItems(resolveNames(["bold", "italic", "underline"]));
|
|
1283
|
+
}
|
|
1284
|
+
const updateStates = (currentEd) => {
|
|
1285
|
+
let canProxy = null;
|
|
1286
|
+
try {
|
|
1287
|
+
canProxy = currentEd.can();
|
|
1288
|
+
} catch {
|
|
1289
|
+
}
|
|
1290
|
+
for (const item of currentResolvedItems) {
|
|
1291
|
+
if (item.type === "separator") continue;
|
|
1292
|
+
activeMapRef.set(item.name, ToolbarController.resolveActive(currentEd, item));
|
|
1293
|
+
try {
|
|
1294
|
+
const canCmd = canProxy?.[item.command];
|
|
1295
|
+
disabledMapRef.set(item.name, canCmd ? !(item.commandArgs?.length ? canCmd(...item.commandArgs) : canCmd()) : false);
|
|
1296
|
+
} catch {
|
|
1297
|
+
disabledMapRef.set(item.name, false);
|
|
1298
|
+
}
|
|
1299
|
+
}
|
|
1300
|
+
};
|
|
1301
|
+
const transactionHandler = () => {
|
|
1302
|
+
if (contexts) {
|
|
1303
|
+
updateContextItems(ed, contexts, detectContext, resolveNames, getFormatItems, filterBySchema, bubbleDefaults, setItems);
|
|
1304
|
+
}
|
|
1305
|
+
updateStates(ed);
|
|
1306
|
+
activeVersion.value++;
|
|
1307
|
+
};
|
|
1308
|
+
ed.on("transaction", transactionHandler);
|
|
1309
|
+
updateStates(ed);
|
|
1310
|
+
initializedEditor = ed;
|
|
1311
|
+
initializedHandler = transactionHandler;
|
|
1312
|
+
};
|
|
1313
|
+
let initializedEditor = null;
|
|
1314
|
+
let initializedHandler = null;
|
|
1315
|
+
onMounted(() => {
|
|
1316
|
+
if (editor.value) {
|
|
1317
|
+
doInit(editor.value);
|
|
1318
|
+
} else {
|
|
1319
|
+
stopEditorWatch = watch(editor, (ed) => {
|
|
1320
|
+
if (ed) {
|
|
1321
|
+
doInit(ed);
|
|
1322
|
+
stopEditorWatch?.();
|
|
1323
|
+
stopEditorWatch = null;
|
|
1324
|
+
}
|
|
1325
|
+
});
|
|
1326
|
+
}
|
|
1327
|
+
});
|
|
1328
|
+
onScopeDispose(() => {
|
|
1329
|
+
stopEditorWatch?.();
|
|
1330
|
+
if (initializedEditor && initializedHandler) {
|
|
1331
|
+
initializedEditor.off("transaction", initializedHandler);
|
|
1332
|
+
if (!initializedEditor.isDestroyed) {
|
|
1333
|
+
initializedEditor.unregisterPlugin(pluginKey);
|
|
1334
|
+
}
|
|
1335
|
+
}
|
|
1336
|
+
});
|
|
1337
|
+
function updateContextItems(ed, ctxs, detectContext, resolveNames, getFormatItems, filterBySchema, defaults, setItems) {
|
|
1338
|
+
const ctx = detectContext(ed.state.selection, ctxs);
|
|
1339
|
+
if (!ctx) {
|
|
1340
|
+
setItems([]);
|
|
1341
|
+
return;
|
|
1342
|
+
}
|
|
1343
|
+
if (ctx in ctxs) {
|
|
1344
|
+
const val = ctxs[ctx];
|
|
1345
|
+
if (val === null || Array.isArray(val) && val.length === 0) {
|
|
1346
|
+
setItems([]);
|
|
1347
|
+
return;
|
|
1348
|
+
}
|
|
1349
|
+
if (val === true) {
|
|
1350
|
+
setItems(filterBySchema(ctx, getFormatItems()));
|
|
1351
|
+
} else if (Array.isArray(val)) {
|
|
1352
|
+
const resolved = resolveNames(val);
|
|
1353
|
+
const buttons = resolved.filter((i) => i.type !== "separator");
|
|
1354
|
+
const filtered = new Set(filterBySchema(ctx, buttons).map((b) => b.name));
|
|
1355
|
+
setItems(resolved.filter((i) => i.type === "separator" || filtered.has(i.name)));
|
|
1356
|
+
}
|
|
1357
|
+
} else {
|
|
1358
|
+
setItems(defaults.get(ctx) ?? []);
|
|
1359
|
+
}
|
|
1360
|
+
}
|
|
1361
|
+
const isItemActive = (item) => {
|
|
1362
|
+
return activeMapRef.get(item.name) ?? false;
|
|
1363
|
+
};
|
|
1364
|
+
const isItemDisabled = (item) => {
|
|
1365
|
+
return disabledMapRef.get(item.name) ?? false;
|
|
1366
|
+
};
|
|
1367
|
+
const executeCommand = (item) => {
|
|
1368
|
+
const ed = editor.value;
|
|
1369
|
+
if (!ed) return;
|
|
1370
|
+
if (item.emitEvent) {
|
|
1371
|
+
ed.emit(item.emitEvent, {});
|
|
1372
|
+
return;
|
|
1373
|
+
}
|
|
1374
|
+
ToolbarController.executeItem(ed, item);
|
|
1375
|
+
};
|
|
1376
|
+
const getCachedIcon = (name) => {
|
|
1377
|
+
return defaultIcons[name] ?? "";
|
|
1378
|
+
};
|
|
1379
|
+
return {
|
|
1380
|
+
menuRef,
|
|
1381
|
+
resolvedItems,
|
|
1382
|
+
isItemActive,
|
|
1383
|
+
isItemDisabled,
|
|
1384
|
+
executeCommand,
|
|
1385
|
+
activeVersion,
|
|
1386
|
+
getCachedIcon
|
|
1387
|
+
};
|
|
1388
|
+
}
|
|
1389
|
+
|
|
1390
|
+
// src/bubble-menu/DomternalBubbleMenu.ts
|
|
1391
|
+
var DomternalBubbleMenu = defineComponent({
|
|
1392
|
+
name: "DomternalBubbleMenu",
|
|
1393
|
+
props: {
|
|
1394
|
+
editor: { type: Object, default: void 0 },
|
|
1395
|
+
shouldShow: { type: Function, default: void 0 },
|
|
1396
|
+
placement: { type: String, default: "top" },
|
|
1397
|
+
offset: { type: Number, default: 8 },
|
|
1398
|
+
updateDelay: { type: Number, default: 0 },
|
|
1399
|
+
items: { type: Array, default: void 0 },
|
|
1400
|
+
contexts: { type: Object, default: void 0 }
|
|
1401
|
+
},
|
|
1402
|
+
setup(props, { slots }) {
|
|
1403
|
+
const { editor: contextEditor } = useCurrentEditor();
|
|
1404
|
+
const {
|
|
1405
|
+
menuRef,
|
|
1406
|
+
resolvedItems,
|
|
1407
|
+
isItemActive,
|
|
1408
|
+
isItemDisabled,
|
|
1409
|
+
executeCommand,
|
|
1410
|
+
activeVersion,
|
|
1411
|
+
getCachedIcon
|
|
1412
|
+
} = useBubbleMenu({
|
|
1413
|
+
editor: computed(() => props.editor ?? contextEditor.value),
|
|
1414
|
+
shouldShow: props.shouldShow,
|
|
1415
|
+
placement: props.placement,
|
|
1416
|
+
offset: props.offset,
|
|
1417
|
+
updateDelay: props.updateDelay,
|
|
1418
|
+
items: props.items,
|
|
1419
|
+
contexts: props.contexts
|
|
1420
|
+
});
|
|
1421
|
+
return () => {
|
|
1422
|
+
void activeVersion.value;
|
|
1423
|
+
return h("div", { ref: menuRef, class: "dm-bubble-menu", role: "toolbar", "aria-label": "Text formatting" }, [
|
|
1424
|
+
...resolvedItems.value.map((item) => {
|
|
1425
|
+
if (item.type === "separator") {
|
|
1426
|
+
return h("span", { key: item.name, class: "dm-toolbar-separator", role: "separator" });
|
|
1427
|
+
}
|
|
1428
|
+
const btn = item;
|
|
1429
|
+
const active = isItemActive(btn);
|
|
1430
|
+
return h("button", {
|
|
1431
|
+
key: btn.name,
|
|
1432
|
+
type: "button",
|
|
1433
|
+
class: ["dm-toolbar-button", active && "dm-toolbar-button--active"],
|
|
1434
|
+
disabled: isItemDisabled(btn),
|
|
1435
|
+
"aria-label": btn.label,
|
|
1436
|
+
"aria-pressed": active,
|
|
1437
|
+
title: btn.label,
|
|
1438
|
+
innerHTML: getCachedIcon(btn.icon),
|
|
1439
|
+
onMousedown: (e) => e.preventDefault(),
|
|
1440
|
+
onClick: () => executeCommand(btn)
|
|
1441
|
+
});
|
|
1442
|
+
}),
|
|
1443
|
+
slots["default"]?.()
|
|
1444
|
+
]);
|
|
1445
|
+
};
|
|
1446
|
+
}
|
|
1447
|
+
});
|
|
1448
|
+
var DomternalFloatingMenu = defineComponent({
|
|
1449
|
+
name: "DomternalFloatingMenu",
|
|
1450
|
+
props: {
|
|
1451
|
+
editor: { type: Object, default: void 0 },
|
|
1452
|
+
shouldShow: { type: Function, default: void 0 },
|
|
1453
|
+
offset: { type: Number, default: 0 }
|
|
1454
|
+
},
|
|
1455
|
+
setup(props, { slots }) {
|
|
1456
|
+
const { editor: contextEditor } = useCurrentEditor();
|
|
1457
|
+
const menuRef = ref();
|
|
1458
|
+
const pluginKey = new PluginKey("vueFloatingMenu-" + Math.random().toString(36).slice(2, 8));
|
|
1459
|
+
let registered = false;
|
|
1460
|
+
let stopWatch = null;
|
|
1461
|
+
const doRegister = (editor) => {
|
|
1462
|
+
if (registered || editor.isDestroyed || !menuRef.value) return;
|
|
1463
|
+
registered = true;
|
|
1464
|
+
const plugin = createFloatingMenuPlugin({
|
|
1465
|
+
pluginKey,
|
|
1466
|
+
editor,
|
|
1467
|
+
element: menuRef.value,
|
|
1468
|
+
...props.shouldShow && { shouldShow: props.shouldShow },
|
|
1469
|
+
offset: props.offset
|
|
1470
|
+
});
|
|
1471
|
+
editor.registerPlugin(plugin);
|
|
1472
|
+
};
|
|
1473
|
+
onMounted(() => {
|
|
1474
|
+
const ed = props.editor ?? contextEditor.value;
|
|
1475
|
+
if (ed) {
|
|
1476
|
+
doRegister(ed);
|
|
1477
|
+
} else {
|
|
1478
|
+
stopWatch = watch(
|
|
1479
|
+
() => props.editor ?? contextEditor.value,
|
|
1480
|
+
(editor) => {
|
|
1481
|
+
if (editor) {
|
|
1482
|
+
doRegister(editor);
|
|
1483
|
+
stopWatch?.();
|
|
1484
|
+
stopWatch = null;
|
|
1485
|
+
}
|
|
1486
|
+
}
|
|
1487
|
+
);
|
|
1488
|
+
}
|
|
1489
|
+
});
|
|
1490
|
+
onScopeDispose(() => {
|
|
1491
|
+
stopWatch?.();
|
|
1492
|
+
const editor = props.editor ?? contextEditor.value;
|
|
1493
|
+
if (editor && !editor.isDestroyed) {
|
|
1494
|
+
editor.unregisterPlugin(pluginKey);
|
|
1495
|
+
}
|
|
1496
|
+
});
|
|
1497
|
+
return () => h("div", { ref: menuRef, class: "dm-floating-menu" }, slots["default"]?.());
|
|
1498
|
+
}
|
|
1499
|
+
});
|
|
1500
|
+
var SCROLL_SETTLE_MS = 50;
|
|
1501
|
+
function useEmojiPicker(editor, emojis) {
|
|
1502
|
+
const isOpen = ref(false);
|
|
1503
|
+
const searchQuery = ref("");
|
|
1504
|
+
const activeCategory = ref("");
|
|
1505
|
+
const pickerRef = ref();
|
|
1506
|
+
let anchorEl = null;
|
|
1507
|
+
let cleanupFloating = null;
|
|
1508
|
+
let clickOutsideHandler = null;
|
|
1509
|
+
let keydownHandler = null;
|
|
1510
|
+
const categories = computed(() => {
|
|
1511
|
+
const map = /* @__PURE__ */ new Map();
|
|
1512
|
+
for (const item of emojis) {
|
|
1513
|
+
let list = map.get(item.group);
|
|
1514
|
+
if (!list) {
|
|
1515
|
+
list = [];
|
|
1516
|
+
map.set(item.group, list);
|
|
1517
|
+
}
|
|
1518
|
+
list.push(item);
|
|
1519
|
+
}
|
|
1520
|
+
return map;
|
|
1521
|
+
});
|
|
1522
|
+
const categoryNames = computed(() => [...categories.value.keys()]);
|
|
1523
|
+
const filteredEmojis = computed(() => {
|
|
1524
|
+
const query = searchQuery.value.toLowerCase();
|
|
1525
|
+
if (!query) return [];
|
|
1526
|
+
const storage = getEmojiStorage(editor.value);
|
|
1527
|
+
const searchFn = storage?.["searchEmoji"];
|
|
1528
|
+
if (searchFn) return searchFn(query);
|
|
1529
|
+
return emojis.filter(
|
|
1530
|
+
(item) => item.name.includes(query) || item.group.toLowerCase().includes(query)
|
|
1531
|
+
);
|
|
1532
|
+
});
|
|
1533
|
+
const frequentlyUsed = computed(() => {
|
|
1534
|
+
if (!isOpen.value) return [];
|
|
1535
|
+
const storage = getEmojiStorage(editor.value);
|
|
1536
|
+
const getFreq = storage?.["getFrequentlyUsed"];
|
|
1537
|
+
if (!getFreq) return [];
|
|
1538
|
+
const names = getFreq();
|
|
1539
|
+
if (!names.length) return [];
|
|
1540
|
+
const nameMap = storage["_nameMap"];
|
|
1541
|
+
if (!nameMap) return [];
|
|
1542
|
+
return names.slice(0, 16).map((n) => nameMap.get(n)).filter(Boolean);
|
|
1543
|
+
});
|
|
1544
|
+
function removeGlobalListeners() {
|
|
1545
|
+
if (clickOutsideHandler) {
|
|
1546
|
+
document.removeEventListener("mousedown", clickOutsideHandler);
|
|
1547
|
+
clickOutsideHandler = null;
|
|
1548
|
+
}
|
|
1549
|
+
if (keydownHandler) {
|
|
1550
|
+
document.removeEventListener("keydown", keydownHandler);
|
|
1551
|
+
keydownHandler = null;
|
|
1552
|
+
}
|
|
1553
|
+
}
|
|
1554
|
+
function close() {
|
|
1555
|
+
cleanupFloating?.();
|
|
1556
|
+
cleanupFloating = null;
|
|
1557
|
+
isOpen.value = false;
|
|
1558
|
+
setStorageOpen(editor.value, false);
|
|
1559
|
+
searchQuery.value = "";
|
|
1560
|
+
anchorEl = null;
|
|
1561
|
+
removeGlobalListeners();
|
|
1562
|
+
editor.value?.view.focus();
|
|
1563
|
+
}
|
|
1564
|
+
function addGlobalListeners() {
|
|
1565
|
+
clickOutsideHandler = (e) => {
|
|
1566
|
+
const target = e.target;
|
|
1567
|
+
if (pickerRef.value && !pickerRef.value.contains(target) && target !== anchorEl && !anchorEl?.contains(target)) {
|
|
1568
|
+
requestAnimationFrame(() => close());
|
|
1569
|
+
}
|
|
1570
|
+
};
|
|
1571
|
+
document.addEventListener("mousedown", clickOutsideHandler);
|
|
1572
|
+
keydownHandler = (e) => {
|
|
1573
|
+
if (e.key === "Escape") {
|
|
1574
|
+
e.preventDefault();
|
|
1575
|
+
close();
|
|
1576
|
+
}
|
|
1577
|
+
};
|
|
1578
|
+
document.addEventListener("keydown", keydownHandler);
|
|
1579
|
+
}
|
|
1580
|
+
watch(
|
|
1581
|
+
editor,
|
|
1582
|
+
(ed, _oldEd, onCleanup) => {
|
|
1583
|
+
if (!ed || ed.isDestroyed) return;
|
|
1584
|
+
const handler = (...args) => {
|
|
1585
|
+
const data = args[0];
|
|
1586
|
+
if (isOpen.value) {
|
|
1587
|
+
close();
|
|
1588
|
+
return;
|
|
1589
|
+
}
|
|
1590
|
+
anchorEl = data?.anchorElement ?? null;
|
|
1591
|
+
isOpen.value = true;
|
|
1592
|
+
setStorageOpen(ed, true);
|
|
1593
|
+
searchQuery.value = "";
|
|
1594
|
+
if (categoryNames.value.length > 0 && categoryNames.value[0]) {
|
|
1595
|
+
activeCategory.value = categoryNames.value[0];
|
|
1596
|
+
}
|
|
1597
|
+
addGlobalListeners();
|
|
1598
|
+
requestAnimationFrame(() => {
|
|
1599
|
+
const panel = pickerRef.value?.querySelector(".dm-emoji-picker");
|
|
1600
|
+
if (panel && anchorEl) {
|
|
1601
|
+
cleanupFloating?.();
|
|
1602
|
+
cleanupFloating = positionFloatingOnce(anchorEl, panel, {
|
|
1603
|
+
placement: "bottom",
|
|
1604
|
+
offsetValue: 4
|
|
1605
|
+
});
|
|
1606
|
+
}
|
|
1607
|
+
const input = pickerRef.value?.querySelector(".dm-emoji-picker-search input");
|
|
1608
|
+
input?.focus({ preventScroll: true });
|
|
1609
|
+
});
|
|
1610
|
+
};
|
|
1611
|
+
ed.on("insertEmoji", handler);
|
|
1612
|
+
onCleanup(() => {
|
|
1613
|
+
removeGlobalListeners();
|
|
1614
|
+
ed.off("insertEmoji", handler);
|
|
1615
|
+
});
|
|
1616
|
+
},
|
|
1617
|
+
{ immediate: true }
|
|
1618
|
+
);
|
|
1619
|
+
onScopeDispose(() => {
|
|
1620
|
+
removeGlobalListeners();
|
|
1621
|
+
cleanupFloating?.();
|
|
1622
|
+
cleanupFloating = null;
|
|
1623
|
+
});
|
|
1624
|
+
function selectEmoji(item) {
|
|
1625
|
+
const ed = editor.value;
|
|
1626
|
+
if (!ed) return;
|
|
1627
|
+
const cmd = ed.commands;
|
|
1628
|
+
if (cmd["insertEmoji"]) {
|
|
1629
|
+
cmd["insertEmoji"](item.name);
|
|
1630
|
+
}
|
|
1631
|
+
close();
|
|
1632
|
+
}
|
|
1633
|
+
function onSearch(event) {
|
|
1634
|
+
searchQuery.value = event.target.value;
|
|
1635
|
+
}
|
|
1636
|
+
function scrollToCategory(cat) {
|
|
1637
|
+
searchQuery.value = "";
|
|
1638
|
+
activeCategory.value = cat;
|
|
1639
|
+
requestAnimationFrame(() => {
|
|
1640
|
+
const grid = pickerRef.value?.querySelector(".dm-emoji-picker-grid");
|
|
1641
|
+
if (!grid) return;
|
|
1642
|
+
const label = grid.querySelector(`[data-category="${cat}"]`);
|
|
1643
|
+
if (label) {
|
|
1644
|
+
grid.scrollTo({ top: label.offsetTop - grid.offsetTop });
|
|
1645
|
+
setTimeout(() => {
|
|
1646
|
+
const firstSwatch = label.nextElementSibling;
|
|
1647
|
+
if (firstSwatch instanceof HTMLElement && firstSwatch.classList.contains("dm-emoji-swatch")) {
|
|
1648
|
+
firstSwatch.focus();
|
|
1649
|
+
}
|
|
1650
|
+
}, SCROLL_SETTLE_MS);
|
|
1651
|
+
}
|
|
1652
|
+
});
|
|
1653
|
+
}
|
|
1654
|
+
function onGridScroll() {
|
|
1655
|
+
if (searchQuery.value) return;
|
|
1656
|
+
const grid = pickerRef.value?.querySelector(".dm-emoji-picker-grid");
|
|
1657
|
+
if (!grid) return;
|
|
1658
|
+
const labels = Array.from(grid.querySelectorAll(".dm-emoji-picker-category-label[data-category]"));
|
|
1659
|
+
let currentCat = "";
|
|
1660
|
+
for (const label of labels) {
|
|
1661
|
+
if (label.offsetTop - grid.offsetTop <= grid.scrollTop + 20) {
|
|
1662
|
+
currentCat = label.getAttribute("data-category") ?? "";
|
|
1663
|
+
}
|
|
1664
|
+
}
|
|
1665
|
+
if (currentCat && currentCat !== activeCategory.value) {
|
|
1666
|
+
activeCategory.value = currentCat;
|
|
1667
|
+
}
|
|
1668
|
+
}
|
|
1669
|
+
return {
|
|
1670
|
+
isOpen,
|
|
1671
|
+
searchQuery,
|
|
1672
|
+
activeCategory,
|
|
1673
|
+
categories,
|
|
1674
|
+
categoryNames,
|
|
1675
|
+
filteredEmojis,
|
|
1676
|
+
frequentlyUsed,
|
|
1677
|
+
pickerRef,
|
|
1678
|
+
selectEmoji,
|
|
1679
|
+
onSearch,
|
|
1680
|
+
scrollToCategory,
|
|
1681
|
+
onGridScroll,
|
|
1682
|
+
close
|
|
1683
|
+
};
|
|
1684
|
+
}
|
|
1685
|
+
function getEmojiStorage(editor) {
|
|
1686
|
+
if (!editor) return null;
|
|
1687
|
+
const storage = editor.storage;
|
|
1688
|
+
return storage["emoji"] ?? null;
|
|
1689
|
+
}
|
|
1690
|
+
function setStorageOpen(editor, open) {
|
|
1691
|
+
if (!editor) return;
|
|
1692
|
+
const storage = getEmojiStorage(editor);
|
|
1693
|
+
if (storage) storage["isOpen"] = open;
|
|
1694
|
+
editor.view.dispatch(editor.view.state.tr);
|
|
1695
|
+
}
|
|
1696
|
+
|
|
1697
|
+
// src/emoji-picker/DomternalEmojiPicker.ts
|
|
1698
|
+
var CATEGORY_ICONS = {
|
|
1699
|
+
"Smileys & Emotion": "\u{1F600}",
|
|
1700
|
+
"People & Body": "\u{1F44B}",
|
|
1701
|
+
"Animals & Nature": "\u{1F431}",
|
|
1702
|
+
"Food & Drink": "\u{1F355}",
|
|
1703
|
+
"Travel & Places": "\u{1F697}",
|
|
1704
|
+
"Activities": "\u26BD",
|
|
1705
|
+
"Objects": "\u{1F4A1}",
|
|
1706
|
+
"Symbols": "\u{1F523}",
|
|
1707
|
+
"Flags": "\u{1F3C1}"
|
|
1708
|
+
};
|
|
1709
|
+
function categoryIcon(cat) {
|
|
1710
|
+
return CATEGORY_ICONS[cat] ?? cat.charAt(0);
|
|
1711
|
+
}
|
|
1712
|
+
function formatName(name) {
|
|
1713
|
+
return name.replace(/_/g, " ");
|
|
1714
|
+
}
|
|
1715
|
+
var DomternalEmojiPicker = defineComponent({
|
|
1716
|
+
name: "DomternalEmojiPicker",
|
|
1717
|
+
props: {
|
|
1718
|
+
editor: { type: Object, default: void 0 },
|
|
1719
|
+
emojis: { type: Array, required: true }
|
|
1720
|
+
},
|
|
1721
|
+
setup(props) {
|
|
1722
|
+
const { editor: contextEditor } = useCurrentEditor();
|
|
1723
|
+
const {
|
|
1724
|
+
isOpen,
|
|
1725
|
+
searchQuery,
|
|
1726
|
+
activeCategory,
|
|
1727
|
+
categoryNames,
|
|
1728
|
+
filteredEmojis,
|
|
1729
|
+
frequentlyUsed,
|
|
1730
|
+
pickerRef,
|
|
1731
|
+
selectEmoji,
|
|
1732
|
+
onSearch,
|
|
1733
|
+
scrollToCategory,
|
|
1734
|
+
onGridScroll,
|
|
1735
|
+
close,
|
|
1736
|
+
categories
|
|
1737
|
+
} = useEmojiPicker(
|
|
1738
|
+
computed(() => props.editor ?? contextEditor.value),
|
|
1739
|
+
props.emojis
|
|
1740
|
+
);
|
|
1741
|
+
function onGridKeyDown(event) {
|
|
1742
|
+
const grid = event.currentTarget;
|
|
1743
|
+
const swatches = Array.from(grid.querySelectorAll(".dm-emoji-swatch"));
|
|
1744
|
+
if (!swatches.length) return;
|
|
1745
|
+
const current = document.activeElement;
|
|
1746
|
+
let idx = swatches.indexOf(current);
|
|
1747
|
+
if (idx === -1) {
|
|
1748
|
+
if (["ArrowRight", "ArrowDown", "ArrowLeft", "ArrowUp"].includes(event.key)) {
|
|
1749
|
+
event.preventDefault();
|
|
1750
|
+
swatches[0]?.focus();
|
|
1751
|
+
}
|
|
1752
|
+
return;
|
|
1753
|
+
}
|
|
1754
|
+
const cols = 8;
|
|
1755
|
+
let next = idx;
|
|
1756
|
+
switch (event.key) {
|
|
1757
|
+
case "ArrowRight":
|
|
1758
|
+
event.preventDefault();
|
|
1759
|
+
next = Math.min(idx + 1, swatches.length - 1);
|
|
1760
|
+
break;
|
|
1761
|
+
case "ArrowLeft":
|
|
1762
|
+
event.preventDefault();
|
|
1763
|
+
next = Math.max(idx - 1, 0);
|
|
1764
|
+
break;
|
|
1765
|
+
case "ArrowDown":
|
|
1766
|
+
event.preventDefault();
|
|
1767
|
+
next = Math.min(idx + cols, swatches.length - 1);
|
|
1768
|
+
break;
|
|
1769
|
+
case "ArrowUp":
|
|
1770
|
+
event.preventDefault();
|
|
1771
|
+
next = Math.max(idx - cols, 0);
|
|
1772
|
+
break;
|
|
1773
|
+
case "Enter":
|
|
1774
|
+
case " ":
|
|
1775
|
+
event.preventDefault();
|
|
1776
|
+
swatches[idx]?.click();
|
|
1777
|
+
return;
|
|
1778
|
+
default:
|
|
1779
|
+
return;
|
|
1780
|
+
}
|
|
1781
|
+
swatches[next]?.focus();
|
|
1782
|
+
}
|
|
1783
|
+
function renderEmojiButton(item) {
|
|
1784
|
+
return h("button", {
|
|
1785
|
+
key: item.name,
|
|
1786
|
+
type: "button",
|
|
1787
|
+
class: "dm-emoji-swatch",
|
|
1788
|
+
tabindex: -1,
|
|
1789
|
+
title: formatName(item.name),
|
|
1790
|
+
"aria-label": formatName(item.name),
|
|
1791
|
+
onMousedown: (e) => e.preventDefault(),
|
|
1792
|
+
onClick: () => selectEmoji(item)
|
|
1793
|
+
}, item.emoji);
|
|
1794
|
+
}
|
|
1795
|
+
return () => {
|
|
1796
|
+
if (!isOpen.value) {
|
|
1797
|
+
return h("div", { ref: pickerRef, class: "dm-emoji-picker-host" });
|
|
1798
|
+
}
|
|
1799
|
+
return h("div", { ref: pickerRef, class: "dm-emoji-picker-host" }, [
|
|
1800
|
+
h("div", { class: "dm-emoji-picker" }, [
|
|
1801
|
+
// Search
|
|
1802
|
+
h("div", { class: "dm-emoji-picker-search" }, [
|
|
1803
|
+
h("input", {
|
|
1804
|
+
type: "text",
|
|
1805
|
+
placeholder: "Search emoji...",
|
|
1806
|
+
"aria-label": "Search emoji",
|
|
1807
|
+
value: searchQuery.value,
|
|
1808
|
+
onInput: onSearch,
|
|
1809
|
+
onKeydown: (e) => {
|
|
1810
|
+
if (e.key === "Escape") close();
|
|
1811
|
+
}
|
|
1812
|
+
})
|
|
1813
|
+
]),
|
|
1814
|
+
// Category tabs
|
|
1815
|
+
h(
|
|
1816
|
+
"div",
|
|
1817
|
+
{ class: "dm-emoji-picker-tabs", role: "tablist" },
|
|
1818
|
+
categoryNames.value.map(
|
|
1819
|
+
(cat) => h("button", {
|
|
1820
|
+
key: cat,
|
|
1821
|
+
type: "button",
|
|
1822
|
+
class: ["dm-emoji-picker-tab", activeCategory.value === cat && "dm-emoji-picker-tab--active"],
|
|
1823
|
+
role: "tab",
|
|
1824
|
+
"aria-selected": activeCategory.value === cat,
|
|
1825
|
+
title: cat,
|
|
1826
|
+
"aria-label": cat,
|
|
1827
|
+
onMousedown: (e) => e.preventDefault(),
|
|
1828
|
+
onClick: () => scrollToCategory(cat)
|
|
1829
|
+
}, categoryIcon(cat))
|
|
1830
|
+
)
|
|
1831
|
+
),
|
|
1832
|
+
// Grid
|
|
1833
|
+
h(
|
|
1834
|
+
"div",
|
|
1835
|
+
{ class: "dm-emoji-picker-grid", onScroll: onGridScroll, onKeydown: onGridKeyDown },
|
|
1836
|
+
searchQuery.value ? filteredEmojis.value.length > 0 ? filteredEmojis.value.map(renderEmojiButton) : [h("div", { class: "dm-emoji-picker-empty" }, "No emoji found")] : [
|
|
1837
|
+
// Frequently used
|
|
1838
|
+
...frequentlyUsed.value.length > 0 ? [
|
|
1839
|
+
h("div", { class: "dm-emoji-picker-category-label" }, "Frequently Used"),
|
|
1840
|
+
...frequentlyUsed.value.map(renderEmojiButton)
|
|
1841
|
+
] : [],
|
|
1842
|
+
// All categories
|
|
1843
|
+
...categoryNames.value.flatMap((cat) => [
|
|
1844
|
+
h("div", {
|
|
1845
|
+
key: `label-${cat}`,
|
|
1846
|
+
class: "dm-emoji-picker-category-label",
|
|
1847
|
+
"data-category": cat
|
|
1848
|
+
}, cat),
|
|
1849
|
+
...(categories.value.get(cat) ?? []).map(renderEmojiButton)
|
|
1850
|
+
])
|
|
1851
|
+
]
|
|
1852
|
+
)
|
|
1853
|
+
])
|
|
1854
|
+
]);
|
|
1855
|
+
};
|
|
1856
|
+
}
|
|
1857
|
+
});
|
|
1858
|
+
var NODE_VIEW_ON_DRAG_START = /* @__PURE__ */ Symbol("domternal-node-view-drag");
|
|
1859
|
+
var NODE_VIEW_CONTENT_REF = /* @__PURE__ */ Symbol("domternal-node-view-content");
|
|
1860
|
+
function useVueNodeView() {
|
|
1861
|
+
const onDragStart = inject(NODE_VIEW_ON_DRAG_START, void 0);
|
|
1862
|
+
const nodeViewContentRef = inject(NODE_VIEW_CONTENT_REF, void 0);
|
|
1863
|
+
return { onDragStart, nodeViewContentRef };
|
|
1864
|
+
}
|
|
1865
|
+
|
|
1866
|
+
// src/node-views/VueNodeViewRenderer.ts
|
|
1867
|
+
function VueNodeViewRenderer(component, options = {}) {
|
|
1868
|
+
const normalizedComponent = typeof component === "function" && "__vccOpts" in component ? component["__vccOpts"] ?? component : component;
|
|
1869
|
+
markRaw(normalizedComponent);
|
|
1870
|
+
const constructor = (node, _view, getPos, decorations) => {
|
|
1871
|
+
const ctx = constructor.__domternalContext;
|
|
1872
|
+
const editor = ctx?.editor;
|
|
1873
|
+
const extension = ctx?.extension ?? { name: node.type.name, options: {} };
|
|
1874
|
+
let appContext = editor ? appContextStore.get(editor) : void 0;
|
|
1875
|
+
if (!appContext) {
|
|
1876
|
+
appContext = pendingAppContextStore.value ?? void 0;
|
|
1877
|
+
if (appContext && editor) {
|
|
1878
|
+
appContextStore.set(editor, appContext);
|
|
1879
|
+
}
|
|
1880
|
+
}
|
|
1881
|
+
if (!appContext) {
|
|
1882
|
+
if (typeof globalThis !== "undefined" && globalThis.__DEV__ !== false) {
|
|
1883
|
+
console.warn(
|
|
1884
|
+
"[VueNodeViewRenderer] appContext not found for editor. Custom Vue node views require provideEditor(editor) to be called, either manually after useEditor() or automatically via <Domternal> root."
|
|
1885
|
+
);
|
|
1886
|
+
}
|
|
1887
|
+
const dom = document.createElement("div");
|
|
1888
|
+
return { dom, update: () => false, destroy: () => {
|
|
1889
|
+
} };
|
|
1890
|
+
}
|
|
1891
|
+
return new VueNodeView(normalizedComponent, {
|
|
1892
|
+
editor,
|
|
1893
|
+
node,
|
|
1894
|
+
getPos,
|
|
1895
|
+
decorations,
|
|
1896
|
+
extension
|
|
1897
|
+
}, options, appContext);
|
|
1898
|
+
};
|
|
1899
|
+
return constructor;
|
|
1900
|
+
}
|
|
1901
|
+
var VueNodeView = class {
|
|
1902
|
+
dom;
|
|
1903
|
+
contentDOM = null;
|
|
1904
|
+
props;
|
|
1905
|
+
editor;
|
|
1906
|
+
appContext;
|
|
1907
|
+
constructor(component, init, options, appContext) {
|
|
1908
|
+
this.editor = init.editor;
|
|
1909
|
+
this.appContext = appContext;
|
|
1910
|
+
const isInline = init.node.type.spec.group === "inline";
|
|
1911
|
+
const tag = options.as ?? (isInline ? "span" : "div");
|
|
1912
|
+
this.dom = document.createElement(tag);
|
|
1913
|
+
this.dom.setAttribute("data-node-view-wrapper", "");
|
|
1914
|
+
if (options.className) {
|
|
1915
|
+
this.dom.className = options.className;
|
|
1916
|
+
}
|
|
1917
|
+
if (options.contentDOMElement !== null) {
|
|
1918
|
+
const contentTag = options.contentDOMElement ?? (isInline ? "span" : "div");
|
|
1919
|
+
this.contentDOM = document.createElement(contentTag);
|
|
1920
|
+
this.contentDOM.setAttribute("data-node-view-content", "");
|
|
1921
|
+
this.contentDOM.style.whiteSpace = "pre-wrap";
|
|
1922
|
+
}
|
|
1923
|
+
const contentDOM = this.contentDOM;
|
|
1924
|
+
this.props = shallowReactive({
|
|
1925
|
+
editor: markRaw(init.editor),
|
|
1926
|
+
node: markRaw(init.node),
|
|
1927
|
+
selected: false,
|
|
1928
|
+
getPos: init.getPos,
|
|
1929
|
+
extension: init.extension,
|
|
1930
|
+
decorations: init.decorations,
|
|
1931
|
+
updateAttributes: (attrs) => {
|
|
1932
|
+
const pos = init.getPos();
|
|
1933
|
+
if (pos === void 0) return;
|
|
1934
|
+
const { tr } = this.editor.view.state;
|
|
1935
|
+
tr.setNodeMarkup(pos, void 0, { ...this.props.node.attrs, ...attrs });
|
|
1936
|
+
this.editor.view.dispatch(tr);
|
|
1937
|
+
},
|
|
1938
|
+
deleteNode: () => {
|
|
1939
|
+
const pos = init.getPos();
|
|
1940
|
+
if (pos === void 0) return;
|
|
1941
|
+
const { tr } = this.editor.view.state;
|
|
1942
|
+
tr.delete(pos, pos + this.props.node.nodeSize);
|
|
1943
|
+
this.editor.view.dispatch(tr);
|
|
1944
|
+
}
|
|
1945
|
+
});
|
|
1946
|
+
const onDragStart = (event) => {
|
|
1947
|
+
if (this.editor.view.dragging) {
|
|
1948
|
+
event.dataTransfer?.setData("text/plain", this.props.node.textContent);
|
|
1949
|
+
}
|
|
1950
|
+
};
|
|
1951
|
+
const contentRefCallback = (el) => {
|
|
1952
|
+
if (el && contentDOM && !el.contains(contentDOM)) {
|
|
1953
|
+
el.appendChild(contentDOM);
|
|
1954
|
+
}
|
|
1955
|
+
};
|
|
1956
|
+
const reactiveProps = this.props;
|
|
1957
|
+
const extended = defineComponent({
|
|
1958
|
+
setup() {
|
|
1959
|
+
provide(NODE_VIEW_ON_DRAG_START, onDragStart);
|
|
1960
|
+
provide(NODE_VIEW_CONTENT_REF, contentRefCallback);
|
|
1961
|
+
return () => h(component, {
|
|
1962
|
+
editor: reactiveProps.editor,
|
|
1963
|
+
node: reactiveProps.node,
|
|
1964
|
+
selected: reactiveProps.selected,
|
|
1965
|
+
getPos: reactiveProps.getPos,
|
|
1966
|
+
updateAttributes: reactiveProps.updateAttributes,
|
|
1967
|
+
deleteNode: reactiveProps.deleteNode,
|
|
1968
|
+
extension: reactiveProps.extension,
|
|
1969
|
+
decorations: reactiveProps.decorations
|
|
1970
|
+
});
|
|
1971
|
+
}
|
|
1972
|
+
});
|
|
1973
|
+
const vNode = h(extended);
|
|
1974
|
+
vNode.appContext = this.appContext;
|
|
1975
|
+
render(vNode, this.dom);
|
|
1976
|
+
}
|
|
1977
|
+
update(node, decorations) {
|
|
1978
|
+
if (node.type.name !== this.props.node.type.name) return false;
|
|
1979
|
+
this.props.node = markRaw(node);
|
|
1980
|
+
this.props.decorations = decorations;
|
|
1981
|
+
return true;
|
|
1982
|
+
}
|
|
1983
|
+
selectNode() {
|
|
1984
|
+
this.props.selected = true;
|
|
1985
|
+
}
|
|
1986
|
+
deselectNode() {
|
|
1987
|
+
this.props.selected = false;
|
|
1988
|
+
}
|
|
1989
|
+
destroy() {
|
|
1990
|
+
render(null, this.dom);
|
|
1991
|
+
}
|
|
1992
|
+
ignoreMutation(mutation) {
|
|
1993
|
+
if (!this.contentDOM) return true;
|
|
1994
|
+
return !this.contentDOM.contains(mutation.target);
|
|
1995
|
+
}
|
|
1996
|
+
stopEvent() {
|
|
1997
|
+
return false;
|
|
1998
|
+
}
|
|
1999
|
+
};
|
|
2000
|
+
var NodeViewWrapper = defineComponent({
|
|
2001
|
+
name: "NodeViewWrapper",
|
|
2002
|
+
props: {
|
|
2003
|
+
as: { type: String, default: "div" }
|
|
2004
|
+
},
|
|
2005
|
+
setup(props, { slots, attrs }) {
|
|
2006
|
+
const onDragStart = inject(NODE_VIEW_ON_DRAG_START, void 0);
|
|
2007
|
+
return () => h(
|
|
2008
|
+
props.as,
|
|
2009
|
+
{
|
|
2010
|
+
...attrs,
|
|
2011
|
+
"data-node-view-wrapper": "",
|
|
2012
|
+
style: { whiteSpace: "normal", ...attrs.style },
|
|
2013
|
+
onDragstart: onDragStart
|
|
2014
|
+
},
|
|
2015
|
+
slots["default"]?.()
|
|
2016
|
+
);
|
|
2017
|
+
}
|
|
2018
|
+
});
|
|
2019
|
+
var NodeViewContent = defineComponent({
|
|
2020
|
+
name: "NodeViewContent",
|
|
2021
|
+
props: {
|
|
2022
|
+
as: { type: String, default: "div" }
|
|
2023
|
+
},
|
|
2024
|
+
setup(props, { attrs }) {
|
|
2025
|
+
const nodeViewContentRef = inject(NODE_VIEW_CONTENT_REF, void 0);
|
|
2026
|
+
return () => {
|
|
2027
|
+
const baseProps = {
|
|
2028
|
+
...attrs,
|
|
2029
|
+
"data-node-view-content": "",
|
|
2030
|
+
style: { whiteSpace: "pre-wrap", ...attrs.style }
|
|
2031
|
+
};
|
|
2032
|
+
if (nodeViewContentRef) {
|
|
2033
|
+
baseProps["ref"] = nodeViewContentRef;
|
|
2034
|
+
}
|
|
2035
|
+
return h(props.as, baseProps);
|
|
2036
|
+
};
|
|
2037
|
+
}
|
|
2038
|
+
});
|
|
2039
|
+
Domternal.Toolbar = DomternalToolbar;
|
|
2040
|
+
Domternal.BubbleMenu = DomternalBubbleMenu;
|
|
2041
|
+
Domternal.FloatingMenu = DomternalFloatingMenu;
|
|
2042
|
+
Domternal.EmojiPicker = DomternalEmojiPicker;
|
|
2043
|
+
|
|
2044
|
+
export { DEFAULT_EXTENSIONS, Domternal, DomternalBubbleMenu, DomternalEditor, DomternalEmojiPicker, DomternalFloatingMenu, DomternalToolbar, EDITOR_KEY, EditorContent, NodeViewContent, NodeViewWrapper, VueNodeViewRenderer, provideEditor, useCurrentEditor, useEditor, useEditorState, useVueNodeView };
|
|
2045
|
+
//# sourceMappingURL=index.js.map
|
|
2046
|
+
//# sourceMappingURL=index.js.map
|