quasar-ui-danx 0.5.0 → 0.5.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.
- package/dist/danx.es.js +12797 -8181
- package/dist/danx.es.js.map +1 -1
- package/dist/danx.umd.js +192 -120
- package/dist/danx.umd.js.map +1 -1
- package/dist/style.css +1 -1
- package/package.json +8 -1
- package/src/components/Utility/Code/CodeViewer.vue +31 -14
- package/src/components/Utility/Code/CodeViewerCollapsed.vue +2 -0
- package/src/components/Utility/Code/CodeViewerFooter.vue +1 -1
- package/src/components/Utility/Code/LanguageBadge.vue +278 -5
- package/src/components/Utility/Code/index.ts +3 -0
- package/src/components/Utility/Markdown/ContextMenu.vue +314 -0
- package/src/components/Utility/Markdown/HotkeyHelpPopover.vue +259 -0
- package/src/components/Utility/Markdown/LineTypeMenu.vue +226 -0
- package/src/components/Utility/Markdown/LinkPopover.vue +331 -0
- package/src/components/Utility/Markdown/MarkdownEditor.vue +228 -0
- package/src/components/Utility/Markdown/MarkdownEditorContent.vue +235 -0
- package/src/components/Utility/Markdown/MarkdownEditorFooter.vue +50 -0
- package/src/components/Utility/Markdown/TablePopover.vue +420 -0
- package/src/components/Utility/Markdown/index.ts +11 -0
- package/src/components/Utility/Markdown/types.ts +27 -0
- package/src/components/Utility/index.ts +1 -0
- package/src/composables/index.ts +1 -0
- package/src/composables/markdown/features/useBlockquotes.spec.ts +428 -0
- package/src/composables/markdown/features/useBlockquotes.ts +248 -0
- package/src/composables/markdown/features/useCodeBlockManager.ts +369 -0
- package/src/composables/markdown/features/useCodeBlocks.spec.ts +779 -0
- package/src/composables/markdown/features/useCodeBlocks.ts +774 -0
- package/src/composables/markdown/features/useContextMenu.ts +444 -0
- package/src/composables/markdown/features/useFocusTracking.ts +116 -0
- package/src/composables/markdown/features/useHeadings.spec.ts +834 -0
- package/src/composables/markdown/features/useHeadings.ts +290 -0
- package/src/composables/markdown/features/useInlineFormatting.spec.ts +705 -0
- package/src/composables/markdown/features/useInlineFormatting.ts +402 -0
- package/src/composables/markdown/features/useLineTypeMenu.ts +285 -0
- package/src/composables/markdown/features/useLinks.spec.ts +369 -0
- package/src/composables/markdown/features/useLinks.ts +374 -0
- package/src/composables/markdown/features/useLists.spec.ts +834 -0
- package/src/composables/markdown/features/useLists.ts +747 -0
- package/src/composables/markdown/features/usePopoverManager.ts +181 -0
- package/src/composables/markdown/features/useTables.spec.ts +1601 -0
- package/src/composables/markdown/features/useTables.ts +1107 -0
- package/src/composables/markdown/index.ts +16 -0
- package/src/composables/markdown/useMarkdownEditor.spec.ts +332 -0
- package/src/composables/markdown/useMarkdownEditor.ts +1068 -0
- package/src/composables/markdown/useMarkdownHotkeys.spec.ts +791 -0
- package/src/composables/markdown/useMarkdownHotkeys.ts +266 -0
- package/src/composables/markdown/useMarkdownSelection.ts +219 -0
- package/src/composables/markdown/useMarkdownSync.ts +549 -0
- package/src/composables/useCodeViewerEditor.spec.ts +655 -0
- package/src/composables/useCodeViewerEditor.ts +174 -20
- package/src/helpers/formats/markdown/htmlToMarkdown/convertHeadings.ts +41 -0
- package/src/helpers/formats/markdown/htmlToMarkdown/index.spec.ts +489 -0
- package/src/helpers/formats/markdown/htmlToMarkdown/index.ts +412 -0
- package/src/helpers/formats/markdown/index.ts +7 -0
- package/src/helpers/formats/markdown/linePatterns.spec.ts +495 -0
- package/src/helpers/formats/markdown/linePatterns.ts +172 -0
- package/src/test/helpers/editorTestUtils.spec.ts +296 -0
- package/src/test/helpers/editorTestUtils.ts +253 -0
- package/src/test/helpers/index.ts +1 -0
- package/src/test/setup.test.ts +12 -0
- package/src/test/setup.ts +12 -0
- package/vitest.config.ts +19 -0
|
@@ -0,0 +1,444 @@
|
|
|
1
|
+
import { Ref, ref } from "vue";
|
|
2
|
+
import { ContextMenuContext, ContextMenuItem } from "../../../components/Utility/Markdown/types";
|
|
3
|
+
import { UseMarkdownEditorReturn } from "../useMarkdownEditor";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Options for useContextMenu composable
|
|
7
|
+
*/
|
|
8
|
+
export interface UseContextMenuOptions {
|
|
9
|
+
editor: UseMarkdownEditorReturn;
|
|
10
|
+
readonly?: Ref<boolean>;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Return type for useContextMenu composable
|
|
15
|
+
*/
|
|
16
|
+
export interface UseContextMenuReturn {
|
|
17
|
+
isVisible: Ref<boolean>;
|
|
18
|
+
position: Ref<{ x: number; y: number }>;
|
|
19
|
+
items: Ref<ContextMenuItem[]>;
|
|
20
|
+
show: (event: MouseEvent) => void;
|
|
21
|
+
hide: () => void;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Composable for managing the context menu in the markdown editor.
|
|
26
|
+
* Handles context detection and menu item building based on cursor position.
|
|
27
|
+
*/
|
|
28
|
+
export function useContextMenu(options: UseContextMenuOptions): UseContextMenuReturn {
|
|
29
|
+
const { editor, readonly } = options;
|
|
30
|
+
|
|
31
|
+
// Context menu state
|
|
32
|
+
const isVisible = ref(false);
|
|
33
|
+
const position = ref({ x: 0, y: 0 });
|
|
34
|
+
const items = ref<ContextMenuItem[]>([]);
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Determine the context for the context menu based on cursor position
|
|
38
|
+
*/
|
|
39
|
+
function determineContext(): ContextMenuContext {
|
|
40
|
+
if (editor.tables.isInTable()) return "table";
|
|
41
|
+
if (editor.codeBlocks.isInCodeBlock()) return "code";
|
|
42
|
+
if (editor.lists.getCurrentListType()) return "list";
|
|
43
|
+
return "text";
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Build context menu items with nested submenus based on the current context.
|
|
48
|
+
*
|
|
49
|
+
* Menu items are filtered based on markdown spec constraints:
|
|
50
|
+
* - Code blocks: Only show toggle code block (code is verbatim, no formatting allowed)
|
|
51
|
+
* - Tables: Only inline formatting and table operations (no block-level elements)
|
|
52
|
+
* - Lists: Inline formatting, list toggle, and blockquote (no headings, tables, or code blocks)
|
|
53
|
+
* - Text: Full menu with all options
|
|
54
|
+
*/
|
|
55
|
+
function buildItems(context: ContextMenuContext): ContextMenuItem[] {
|
|
56
|
+
const menuItems: ContextMenuItem[] = [];
|
|
57
|
+
|
|
58
|
+
// In code blocks, show minimal menu - just exit option
|
|
59
|
+
// Code blocks are literal/verbatim, no formatting is allowed
|
|
60
|
+
// Unnested since this is the only action available in this context
|
|
61
|
+
if (context === "code") {
|
|
62
|
+
menuItems.push({
|
|
63
|
+
id: "code-block",
|
|
64
|
+
label: "Toggle Code Block",
|
|
65
|
+
shortcut: "Ctrl+Shift+K",
|
|
66
|
+
action: () => editor.codeBlocks.toggleCodeBlock()
|
|
67
|
+
});
|
|
68
|
+
return menuItems;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// In tables, only show inline formatting and table operations
|
|
72
|
+
// Tables cannot contain block-level elements (headings, code blocks, blockquotes, lists, nested tables)
|
|
73
|
+
// Table operations are unnested since this context is already limited to tables
|
|
74
|
+
if (context === "table") {
|
|
75
|
+
// Format submenu (inline only - keep nested)
|
|
76
|
+
menuItems.push({
|
|
77
|
+
id: "format",
|
|
78
|
+
label: "Format",
|
|
79
|
+
children: [
|
|
80
|
+
{
|
|
81
|
+
id: "bold",
|
|
82
|
+
label: "Bold",
|
|
83
|
+
shortcut: "Ctrl+B",
|
|
84
|
+
action: () => editor.inlineFormatting.toggleBold()
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
id: "italic",
|
|
88
|
+
label: "Italic",
|
|
89
|
+
shortcut: "Ctrl+I",
|
|
90
|
+
action: () => editor.inlineFormatting.toggleItalic()
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
id: "strikethrough",
|
|
94
|
+
label: "Strikethrough",
|
|
95
|
+
shortcut: "Ctrl+Shift+S",
|
|
96
|
+
action: () => editor.inlineFormatting.toggleStrikethrough()
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
id: "inline-code",
|
|
100
|
+
label: "Inline Code",
|
|
101
|
+
shortcut: "Ctrl+E",
|
|
102
|
+
action: () => editor.inlineFormatting.toggleInlineCode()
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
id: "link",
|
|
106
|
+
label: "Link",
|
|
107
|
+
shortcut: "Ctrl+K",
|
|
108
|
+
action: () => editor.links.insertLink()
|
|
109
|
+
}
|
|
110
|
+
]
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
// Table operations - unnested as top-level items with dividers between groups
|
|
114
|
+
// Divider after Format submenu
|
|
115
|
+
menuItems.push({ id: "table-format-divider", label: "", divider: true });
|
|
116
|
+
|
|
117
|
+
// Insert operations
|
|
118
|
+
menuItems.push({
|
|
119
|
+
id: "insert-row-above",
|
|
120
|
+
label: "Insert Row Above",
|
|
121
|
+
shortcut: "Ctrl+Alt+Shift+Up",
|
|
122
|
+
action: () => editor.tables.insertRowAbove()
|
|
123
|
+
});
|
|
124
|
+
menuItems.push({
|
|
125
|
+
id: "insert-row-below",
|
|
126
|
+
label: "Insert Row Below",
|
|
127
|
+
shortcut: "Ctrl+Alt+Shift+Down",
|
|
128
|
+
action: () => editor.tables.insertRowBelow()
|
|
129
|
+
});
|
|
130
|
+
menuItems.push({
|
|
131
|
+
id: "insert-col-left",
|
|
132
|
+
label: "Insert Column Left",
|
|
133
|
+
shortcut: "Ctrl+Alt+Shift+Left",
|
|
134
|
+
action: () => editor.tables.insertColumnLeft()
|
|
135
|
+
});
|
|
136
|
+
menuItems.push({
|
|
137
|
+
id: "insert-col-right",
|
|
138
|
+
label: "Insert Column Right",
|
|
139
|
+
shortcut: "Ctrl+Alt+Shift+Right",
|
|
140
|
+
action: () => editor.tables.insertColumnRight()
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
// Divider before delete operations
|
|
144
|
+
menuItems.push({ id: "table-divider-1", label: "", divider: true });
|
|
145
|
+
|
|
146
|
+
// Delete operations
|
|
147
|
+
menuItems.push({
|
|
148
|
+
id: "delete-row",
|
|
149
|
+
label: "Delete Row",
|
|
150
|
+
shortcut: "Ctrl+Alt+Backspace",
|
|
151
|
+
action: () => editor.tables.deleteCurrentRow()
|
|
152
|
+
});
|
|
153
|
+
menuItems.push({
|
|
154
|
+
id: "delete-col",
|
|
155
|
+
label: "Delete Column",
|
|
156
|
+
shortcut: "Ctrl+Shift+Backspace",
|
|
157
|
+
action: () => editor.tables.deleteCurrentColumn()
|
|
158
|
+
});
|
|
159
|
+
menuItems.push({
|
|
160
|
+
id: "delete-table",
|
|
161
|
+
label: "Delete Table",
|
|
162
|
+
action: () => editor.tables.deleteTable()
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
// Divider before alignment
|
|
166
|
+
menuItems.push({ id: "table-divider-2", label: "", divider: true });
|
|
167
|
+
|
|
168
|
+
// Alignment submenu
|
|
169
|
+
menuItems.push({
|
|
170
|
+
id: "alignment",
|
|
171
|
+
label: "Alignment",
|
|
172
|
+
children: [
|
|
173
|
+
{
|
|
174
|
+
id: "align-left",
|
|
175
|
+
label: "Align Left",
|
|
176
|
+
shortcut: "Ctrl+Alt+L",
|
|
177
|
+
action: () => editor.tables.setColumnAlignmentLeft()
|
|
178
|
+
},
|
|
179
|
+
{
|
|
180
|
+
id: "align-center",
|
|
181
|
+
label: "Align Center",
|
|
182
|
+
shortcut: "Ctrl+Alt+C",
|
|
183
|
+
action: () => editor.tables.setColumnAlignmentCenter()
|
|
184
|
+
},
|
|
185
|
+
{
|
|
186
|
+
id: "align-right",
|
|
187
|
+
label: "Align Right",
|
|
188
|
+
shortcut: "Ctrl+Alt+R",
|
|
189
|
+
action: () => editor.tables.setColumnAlignmentRight()
|
|
190
|
+
}
|
|
191
|
+
]
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
return menuItems;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// In lists, show inline formatting, list operations, and blockquote
|
|
198
|
+
// Lists cannot contain headings, tables, or code blocks inside list items
|
|
199
|
+
if (context === "list") {
|
|
200
|
+
// Format submenu (inline formatting)
|
|
201
|
+
menuItems.push({
|
|
202
|
+
id: "format",
|
|
203
|
+
label: "Format",
|
|
204
|
+
children: [
|
|
205
|
+
{
|
|
206
|
+
id: "bold",
|
|
207
|
+
label: "Bold",
|
|
208
|
+
shortcut: "Ctrl+B",
|
|
209
|
+
action: () => editor.inlineFormatting.toggleBold()
|
|
210
|
+
},
|
|
211
|
+
{
|
|
212
|
+
id: "italic",
|
|
213
|
+
label: "Italic",
|
|
214
|
+
shortcut: "Ctrl+I",
|
|
215
|
+
action: () => editor.inlineFormatting.toggleItalic()
|
|
216
|
+
},
|
|
217
|
+
{
|
|
218
|
+
id: "strikethrough",
|
|
219
|
+
label: "Strikethrough",
|
|
220
|
+
shortcut: "Ctrl+Shift+S",
|
|
221
|
+
action: () => editor.inlineFormatting.toggleStrikethrough()
|
|
222
|
+
},
|
|
223
|
+
{
|
|
224
|
+
id: "inline-code",
|
|
225
|
+
label: "Inline Code",
|
|
226
|
+
shortcut: "Ctrl+E",
|
|
227
|
+
action: () => editor.inlineFormatting.toggleInlineCode()
|
|
228
|
+
},
|
|
229
|
+
{
|
|
230
|
+
id: "link",
|
|
231
|
+
label: "Link",
|
|
232
|
+
shortcut: "Ctrl+K",
|
|
233
|
+
action: () => editor.links.insertLink()
|
|
234
|
+
}
|
|
235
|
+
]
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
// Lists submenu (toggle between bullet/numbered)
|
|
239
|
+
menuItems.push({
|
|
240
|
+
id: "lists",
|
|
241
|
+
label: "Lists",
|
|
242
|
+
children: [
|
|
243
|
+
{
|
|
244
|
+
id: "bullet-list",
|
|
245
|
+
label: "Bullet List",
|
|
246
|
+
shortcut: "Ctrl+Shift+[",
|
|
247
|
+
action: () => editor.lists.toggleUnorderedList()
|
|
248
|
+
},
|
|
249
|
+
{
|
|
250
|
+
id: "numbered-list",
|
|
251
|
+
label: "Numbered List",
|
|
252
|
+
shortcut: "Ctrl+Shift+]",
|
|
253
|
+
action: () => editor.lists.toggleOrderedList()
|
|
254
|
+
}
|
|
255
|
+
]
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
// Blocks submenu (only blockquote, no code blocks or tables)
|
|
259
|
+
menuItems.push({
|
|
260
|
+
id: "blocks",
|
|
261
|
+
label: "Blocks",
|
|
262
|
+
children: [
|
|
263
|
+
{
|
|
264
|
+
id: "blockquote",
|
|
265
|
+
label: "Blockquote",
|
|
266
|
+
shortcut: "Ctrl+Shift+Q",
|
|
267
|
+
action: () => editor.blockquotes.toggleBlockquote()
|
|
268
|
+
}
|
|
269
|
+
]
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
return menuItems;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// Text/Paragraph context - show everything (full menu)
|
|
276
|
+
// Headings submenu
|
|
277
|
+
menuItems.push({
|
|
278
|
+
id: "headings",
|
|
279
|
+
label: "Headings",
|
|
280
|
+
children: [
|
|
281
|
+
{
|
|
282
|
+
id: "paragraph",
|
|
283
|
+
label: "Paragraph",
|
|
284
|
+
shortcut: "Ctrl+0",
|
|
285
|
+
action: () => editor.headings.setHeadingLevel(0)
|
|
286
|
+
},
|
|
287
|
+
{
|
|
288
|
+
id: "h1",
|
|
289
|
+
label: "Heading 1",
|
|
290
|
+
shortcut: "Ctrl+1",
|
|
291
|
+
action: () => editor.headings.setHeadingLevel(1)
|
|
292
|
+
},
|
|
293
|
+
{
|
|
294
|
+
id: "h2",
|
|
295
|
+
label: "Heading 2",
|
|
296
|
+
shortcut: "Ctrl+2",
|
|
297
|
+
action: () => editor.headings.setHeadingLevel(2)
|
|
298
|
+
},
|
|
299
|
+
{
|
|
300
|
+
id: "h3",
|
|
301
|
+
label: "Heading 3",
|
|
302
|
+
shortcut: "Ctrl+3",
|
|
303
|
+
action: () => editor.headings.setHeadingLevel(3)
|
|
304
|
+
},
|
|
305
|
+
{
|
|
306
|
+
id: "h4",
|
|
307
|
+
label: "Heading 4",
|
|
308
|
+
shortcut: "Ctrl+4",
|
|
309
|
+
action: () => editor.headings.setHeadingLevel(4)
|
|
310
|
+
},
|
|
311
|
+
{
|
|
312
|
+
id: "h5",
|
|
313
|
+
label: "Heading 5",
|
|
314
|
+
shortcut: "Ctrl+5",
|
|
315
|
+
action: () => editor.headings.setHeadingLevel(5)
|
|
316
|
+
},
|
|
317
|
+
{
|
|
318
|
+
id: "h6",
|
|
319
|
+
label: "Heading 6",
|
|
320
|
+
shortcut: "Ctrl+6",
|
|
321
|
+
action: () => editor.headings.setHeadingLevel(6)
|
|
322
|
+
}
|
|
323
|
+
]
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
// Format submenu
|
|
327
|
+
menuItems.push({
|
|
328
|
+
id: "format",
|
|
329
|
+
label: "Format",
|
|
330
|
+
children: [
|
|
331
|
+
{
|
|
332
|
+
id: "bold",
|
|
333
|
+
label: "Bold",
|
|
334
|
+
shortcut: "Ctrl+B",
|
|
335
|
+
action: () => editor.inlineFormatting.toggleBold()
|
|
336
|
+
},
|
|
337
|
+
{
|
|
338
|
+
id: "italic",
|
|
339
|
+
label: "Italic",
|
|
340
|
+
shortcut: "Ctrl+I",
|
|
341
|
+
action: () => editor.inlineFormatting.toggleItalic()
|
|
342
|
+
},
|
|
343
|
+
{
|
|
344
|
+
id: "strikethrough",
|
|
345
|
+
label: "Strikethrough",
|
|
346
|
+
shortcut: "Ctrl+Shift+S",
|
|
347
|
+
action: () => editor.inlineFormatting.toggleStrikethrough()
|
|
348
|
+
},
|
|
349
|
+
{
|
|
350
|
+
id: "inline-code",
|
|
351
|
+
label: "Inline Code",
|
|
352
|
+
shortcut: "Ctrl+E",
|
|
353
|
+
action: () => editor.inlineFormatting.toggleInlineCode()
|
|
354
|
+
},
|
|
355
|
+
{
|
|
356
|
+
id: "link",
|
|
357
|
+
label: "Link",
|
|
358
|
+
shortcut: "Ctrl+K",
|
|
359
|
+
action: () => editor.links.insertLink()
|
|
360
|
+
}
|
|
361
|
+
]
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
// Lists submenu
|
|
365
|
+
menuItems.push({
|
|
366
|
+
id: "lists",
|
|
367
|
+
label: "Lists",
|
|
368
|
+
children: [
|
|
369
|
+
{
|
|
370
|
+
id: "bullet-list",
|
|
371
|
+
label: "Bullet List",
|
|
372
|
+
shortcut: "Ctrl+Shift+[",
|
|
373
|
+
action: () => editor.lists.toggleUnorderedList()
|
|
374
|
+
},
|
|
375
|
+
{
|
|
376
|
+
id: "numbered-list",
|
|
377
|
+
label: "Numbered List",
|
|
378
|
+
shortcut: "Ctrl+Shift+]",
|
|
379
|
+
action: () => editor.lists.toggleOrderedList()
|
|
380
|
+
}
|
|
381
|
+
]
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
// Blocks submenu (with all block options)
|
|
385
|
+
menuItems.push({
|
|
386
|
+
id: "blocks",
|
|
387
|
+
label: "Blocks",
|
|
388
|
+
children: [
|
|
389
|
+
{
|
|
390
|
+
id: "code-block",
|
|
391
|
+
label: "Code Block",
|
|
392
|
+
shortcut: "Ctrl+Shift+K",
|
|
393
|
+
action: () => editor.codeBlocks.toggleCodeBlock()
|
|
394
|
+
},
|
|
395
|
+
{
|
|
396
|
+
id: "blockquote",
|
|
397
|
+
label: "Blockquote",
|
|
398
|
+
shortcut: "Ctrl+Shift+Q",
|
|
399
|
+
action: () => editor.blockquotes.toggleBlockquote()
|
|
400
|
+
},
|
|
401
|
+
{
|
|
402
|
+
id: "insert-table",
|
|
403
|
+
label: "Insert Table",
|
|
404
|
+
shortcut: "Ctrl+Alt+Shift+T",
|
|
405
|
+
action: () => editor.tables.insertTable()
|
|
406
|
+
}
|
|
407
|
+
]
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
return menuItems;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
/**
|
|
414
|
+
* Show the context menu at the event position
|
|
415
|
+
*/
|
|
416
|
+
function show(event: MouseEvent): void {
|
|
417
|
+
// Don't show context menu in readonly mode
|
|
418
|
+
if (readonly?.value) return;
|
|
419
|
+
|
|
420
|
+
event.preventDefault();
|
|
421
|
+
|
|
422
|
+
const context = determineContext();
|
|
423
|
+
const menuItems = buildItems(context);
|
|
424
|
+
|
|
425
|
+
position.value = { x: event.clientX, y: event.clientY };
|
|
426
|
+
items.value = menuItems;
|
|
427
|
+
isVisible.value = true;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
/**
|
|
431
|
+
* Hide the context menu
|
|
432
|
+
*/
|
|
433
|
+
function hide(): void {
|
|
434
|
+
isVisible.value = false;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
return {
|
|
438
|
+
isVisible,
|
|
439
|
+
position,
|
|
440
|
+
items,
|
|
441
|
+
show,
|
|
442
|
+
hide
|
|
443
|
+
};
|
|
444
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { onMounted, onUnmounted, Ref, ref, watch } from "vue";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Options for useFocusTracking composable
|
|
5
|
+
*/
|
|
6
|
+
export interface UseFocusTrackingOptions {
|
|
7
|
+
/** Reference to the main content element */
|
|
8
|
+
contentRef: Ref<HTMLElement | null>;
|
|
9
|
+
/** Reference to the menu container (optional, for focus retention when clicking menu) */
|
|
10
|
+
menuContainerRef?: Ref<HTMLElement | null>;
|
|
11
|
+
/** Callback when selection changes in the document */
|
|
12
|
+
onSelectionChange?: () => void;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Return type for useFocusTracking composable
|
|
17
|
+
*/
|
|
18
|
+
export interface UseFocusTrackingReturn {
|
|
19
|
+
/** Whether the editor content area currently has focus */
|
|
20
|
+
isEditorFocused: Ref<boolean>;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Composable for tracking focus state within a contenteditable editor
|
|
25
|
+
*
|
|
26
|
+
* This handles:
|
|
27
|
+
* - Focus in/out tracking for the content area
|
|
28
|
+
* - Optional focus retention when clicking associated UI elements (like menus)
|
|
29
|
+
* - Document-level selection change monitoring
|
|
30
|
+
* - Proper listener cleanup on unmount
|
|
31
|
+
*/
|
|
32
|
+
export function useFocusTracking(options: UseFocusTrackingOptions): UseFocusTrackingReturn {
|
|
33
|
+
const { contentRef, menuContainerRef, onSelectionChange } = options;
|
|
34
|
+
|
|
35
|
+
// Track whether the editor has focus
|
|
36
|
+
const isEditorFocused = ref(false);
|
|
37
|
+
|
|
38
|
+
// Track which element has listeners attached (for cleanup)
|
|
39
|
+
let boundContentEl: HTMLElement | null = null;
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Handle focus entering the content area
|
|
43
|
+
*/
|
|
44
|
+
function handleFocusIn(event: FocusEvent): void {
|
|
45
|
+
const contentEl = contentRef.value;
|
|
46
|
+
if (contentEl && contentEl.contains(event.target as Node)) {
|
|
47
|
+
isEditorFocused.value = true;
|
|
48
|
+
// Notify selection change callback if provided
|
|
49
|
+
onSelectionChange?.();
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Handle focus leaving the content area
|
|
55
|
+
*
|
|
56
|
+
* Note: We check if focus is moving to the menu container to avoid
|
|
57
|
+
* prematurely marking the editor as unfocused when clicking menu items.
|
|
58
|
+
*/
|
|
59
|
+
function handleFocusOut(event: FocusEvent): void {
|
|
60
|
+
const contentEl = contentRef.value;
|
|
61
|
+
const menuEl = menuContainerRef?.value;
|
|
62
|
+
const relatedTarget = event.relatedTarget as Node | null;
|
|
63
|
+
|
|
64
|
+
// Check if focus is moving outside the editor
|
|
65
|
+
if (contentEl && !contentEl.contains(relatedTarget)) {
|
|
66
|
+
// Also check if focus is moving to the menu container (if provided)
|
|
67
|
+
// Keep focused if moving to menu - allows clicking menu without losing focus state
|
|
68
|
+
if (!menuEl || !menuEl.contains(relatedTarget)) {
|
|
69
|
+
isEditorFocused.value = false;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Setup or cleanup focus listeners on content element
|
|
76
|
+
*/
|
|
77
|
+
function setupContentListeners(el: HTMLElement | null): void {
|
|
78
|
+
// Cleanup previous listeners if element changed
|
|
79
|
+
if (boundContentEl && boundContentEl !== el) {
|
|
80
|
+
boundContentEl.removeEventListener("focusin", handleFocusIn);
|
|
81
|
+
boundContentEl.removeEventListener("focusout", handleFocusOut);
|
|
82
|
+
boundContentEl = null;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Setup new listeners
|
|
86
|
+
if (el && el !== boundContentEl) {
|
|
87
|
+
el.addEventListener("focusin", handleFocusIn);
|
|
88
|
+
el.addEventListener("focusout", handleFocusOut);
|
|
89
|
+
boundContentEl = el;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Watch for content element to become available
|
|
94
|
+
watch(contentRef, (newEl) => {
|
|
95
|
+
setupContentListeners(newEl);
|
|
96
|
+
}, { immediate: true });
|
|
97
|
+
|
|
98
|
+
// Listen for document-level selection changes
|
|
99
|
+
onMounted(() => {
|
|
100
|
+
if (onSelectionChange) {
|
|
101
|
+
document.addEventListener("selectionchange", onSelectionChange);
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
onUnmounted(() => {
|
|
106
|
+
if (onSelectionChange) {
|
|
107
|
+
document.removeEventListener("selectionchange", onSelectionChange);
|
|
108
|
+
}
|
|
109
|
+
// Cleanup content listeners
|
|
110
|
+
setupContentListeners(null);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
return {
|
|
114
|
+
isEditorFocused
|
|
115
|
+
};
|
|
116
|
+
}
|