quasar-ui-danx 0.4.99 → 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 +17884 -12732
- package/dist/danx.es.js.map +1 -1
- package/dist/danx.umd.js +192 -118
- package/dist/danx.umd.js.map +1 -1
- package/dist/style.css +1 -1
- package/package.json +11 -2
- package/scripts/publish.sh +76 -0
- 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/MarkdownContent.vue +160 -6
- 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/index.ts +1 -1
- package/src/helpers/formats/markdown/escapeHtml.ts +15 -0
- package/src/helpers/formats/markdown/escapeSequences.ts +60 -0
- 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 +92 -0
- package/src/helpers/formats/markdown/linePatterns.spec.ts +495 -0
- package/src/helpers/formats/markdown/linePatterns.ts +172 -0
- package/src/helpers/formats/markdown/parseInline.ts +124 -0
- package/src/helpers/formats/markdown/render/index.ts +92 -0
- package/src/helpers/formats/markdown/render/renderFootnotes.ts +30 -0
- package/src/helpers/formats/markdown/render/renderList.ts +69 -0
- package/src/helpers/formats/markdown/render/renderTable.ts +38 -0
- package/src/helpers/formats/markdown/state.ts +58 -0
- package/src/helpers/formats/markdown/tokenize/extractDefinitions.ts +39 -0
- package/src/helpers/formats/markdown/tokenize/index.ts +139 -0
- package/src/helpers/formats/markdown/tokenize/parseBlockquote.ts +34 -0
- package/src/helpers/formats/markdown/tokenize/parseCodeBlock.ts +85 -0
- package/src/helpers/formats/markdown/tokenize/parseDefinitionList.ts +88 -0
- package/src/helpers/formats/markdown/tokenize/parseHeading.ts +65 -0
- package/src/helpers/formats/markdown/tokenize/parseHorizontalRule.ts +22 -0
- package/src/helpers/formats/markdown/tokenize/parseList.ts +119 -0
- package/src/helpers/formats/markdown/tokenize/parseParagraph.ts +59 -0
- package/src/helpers/formats/markdown/tokenize/parseTable.ts +70 -0
- package/src/helpers/formats/markdown/tokenize/parseTaskList.ts +47 -0
- package/src/helpers/formats/markdown/tokenize/utils.ts +25 -0
- package/src/helpers/formats/markdown/types.ts +63 -0
- package/src/styles/danx.scss +1 -0
- package/src/styles/themes/danx/markdown.scss +96 -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
- package/src/helpers/formats/renderMarkdown.ts +0 -338
|
@@ -44,8 +44,29 @@
|
|
|
44
44
|
<li
|
|
45
45
|
v-for="(item, itemIndex) in token.items"
|
|
46
46
|
:key="itemIndex"
|
|
47
|
-
|
|
48
|
-
|
|
47
|
+
>
|
|
48
|
+
<span v-html="parseInlineContent(item.content)" />
|
|
49
|
+
<template v-if="item.children && item.children.length > 0">
|
|
50
|
+
<template v-for="(child, childIndex) in item.children" :key="'child-' + childIndex">
|
|
51
|
+
<!-- Nested unordered list -->
|
|
52
|
+
<ul v-if="child.type === 'ul'">
|
|
53
|
+
<li
|
|
54
|
+
v-for="(nestedItem, nestedIndex) in child.items"
|
|
55
|
+
:key="nestedIndex"
|
|
56
|
+
v-html="renderListItem(nestedItem)"
|
|
57
|
+
/>
|
|
58
|
+
</ul>
|
|
59
|
+
<!-- Nested ordered list -->
|
|
60
|
+
<ol v-else-if="child.type === 'ol'" :start="child.start">
|
|
61
|
+
<li
|
|
62
|
+
v-for="(nestedItem, nestedIndex) in child.items"
|
|
63
|
+
:key="nestedIndex"
|
|
64
|
+
v-html="renderListItem(nestedItem)"
|
|
65
|
+
/>
|
|
66
|
+
</ol>
|
|
67
|
+
</template>
|
|
68
|
+
</template>
|
|
69
|
+
</li>
|
|
49
70
|
</ul>
|
|
50
71
|
|
|
51
72
|
<!-- Ordered lists -->
|
|
@@ -56,10 +77,86 @@
|
|
|
56
77
|
<li
|
|
57
78
|
v-for="(item, itemIndex) in token.items"
|
|
58
79
|
:key="itemIndex"
|
|
59
|
-
|
|
60
|
-
|
|
80
|
+
>
|
|
81
|
+
<span v-html="parseInlineContent(item.content)" />
|
|
82
|
+
<template v-if="item.children && item.children.length > 0">
|
|
83
|
+
<template v-for="(child, childIndex) in item.children" :key="'child-' + childIndex">
|
|
84
|
+
<!-- Nested unordered list -->
|
|
85
|
+
<ul v-if="child.type === 'ul'">
|
|
86
|
+
<li
|
|
87
|
+
v-for="(nestedItem, nestedIndex) in child.items"
|
|
88
|
+
:key="nestedIndex"
|
|
89
|
+
v-html="renderListItem(nestedItem)"
|
|
90
|
+
/>
|
|
91
|
+
</ul>
|
|
92
|
+
<!-- Nested ordered list -->
|
|
93
|
+
<ol v-else-if="child.type === 'ol'" :start="child.start">
|
|
94
|
+
<li
|
|
95
|
+
v-for="(nestedItem, nestedIndex) in child.items"
|
|
96
|
+
:key="nestedIndex"
|
|
97
|
+
v-html="renderListItem(nestedItem)"
|
|
98
|
+
/>
|
|
99
|
+
</ol>
|
|
100
|
+
</template>
|
|
101
|
+
</template>
|
|
102
|
+
</li>
|
|
61
103
|
</ol>
|
|
62
104
|
|
|
105
|
+
<!-- Task lists -->
|
|
106
|
+
<ul
|
|
107
|
+
v-else-if="token.type === 'task_list'"
|
|
108
|
+
class="task-list"
|
|
109
|
+
>
|
|
110
|
+
<li
|
|
111
|
+
v-for="(item, itemIndex) in token.items"
|
|
112
|
+
:key="itemIndex"
|
|
113
|
+
class="task-list-item"
|
|
114
|
+
>
|
|
115
|
+
<input
|
|
116
|
+
type="checkbox"
|
|
117
|
+
:checked="item.checked"
|
|
118
|
+
disabled
|
|
119
|
+
/>
|
|
120
|
+
<span v-html="parseInlineContent(item.content)" />
|
|
121
|
+
</li>
|
|
122
|
+
</ul>
|
|
123
|
+
|
|
124
|
+
<!-- Tables -->
|
|
125
|
+
<table v-else-if="token.type === 'table'">
|
|
126
|
+
<thead>
|
|
127
|
+
<tr>
|
|
128
|
+
<th
|
|
129
|
+
v-for="(header, hIndex) in token.headers"
|
|
130
|
+
:key="hIndex"
|
|
131
|
+
:style="token.alignments[hIndex] ? { textAlign: token.alignments[hIndex] } : {}"
|
|
132
|
+
v-html="parseInlineContent(header)"
|
|
133
|
+
/>
|
|
134
|
+
</tr>
|
|
135
|
+
</thead>
|
|
136
|
+
<tbody>
|
|
137
|
+
<tr v-for="(row, rIndex) in token.rows" :key="rIndex">
|
|
138
|
+
<td
|
|
139
|
+
v-for="(cell, cIndex) in row"
|
|
140
|
+
:key="cIndex"
|
|
141
|
+
:style="token.alignments[cIndex] ? { textAlign: token.alignments[cIndex] } : {}"
|
|
142
|
+
v-html="parseInlineContent(cell)"
|
|
143
|
+
/>
|
|
144
|
+
</tr>
|
|
145
|
+
</tbody>
|
|
146
|
+
</table>
|
|
147
|
+
|
|
148
|
+
<!-- Definition lists -->
|
|
149
|
+
<dl v-else-if="token.type === 'dl'">
|
|
150
|
+
<template v-for="(item, itemIndex) in token.items" :key="itemIndex">
|
|
151
|
+
<dt v-html="parseInlineContent(item.term)" />
|
|
152
|
+
<dd
|
|
153
|
+
v-for="(def, defIndex) in item.definitions"
|
|
154
|
+
:key="'def-' + defIndex"
|
|
155
|
+
v-html="parseInlineContent(def)"
|
|
156
|
+
/>
|
|
157
|
+
</template>
|
|
158
|
+
</dl>
|
|
159
|
+
|
|
63
160
|
<!-- Horizontal rules -->
|
|
64
161
|
<hr v-else-if="token.type === 'hr'" />
|
|
65
162
|
|
|
@@ -69,13 +166,30 @@
|
|
|
69
166
|
v-html="parseInlineContent(token.content).replace(/\n/g, '<br />')"
|
|
70
167
|
/>
|
|
71
168
|
</template>
|
|
169
|
+
|
|
170
|
+
<!-- Footnotes section -->
|
|
171
|
+
<section v-if="hasFootnotes" class="footnotes">
|
|
172
|
+
<hr />
|
|
173
|
+
<ol class="footnote-list">
|
|
174
|
+
<li
|
|
175
|
+
v-for="fn in sortedFootnotes"
|
|
176
|
+
:key="fn.id"
|
|
177
|
+
:id="'fn-' + fn.id"
|
|
178
|
+
class="footnote-item"
|
|
179
|
+
>
|
|
180
|
+
<span v-html="parseInlineContent(fn.content)" />
|
|
181
|
+
<a :href="'#fnref-' + fn.id" class="footnote-backref">↩</a>
|
|
182
|
+
</li>
|
|
183
|
+
</ol>
|
|
184
|
+
</section>
|
|
72
185
|
</div>
|
|
73
186
|
</template>
|
|
74
187
|
|
|
75
188
|
<script setup lang="ts">
|
|
76
189
|
import { computed, reactive } from "vue";
|
|
77
190
|
import { parse as parseYAML, stringify as stringifyYAML } from "yaml";
|
|
78
|
-
import { tokenizeBlocks, parseInline, renderMarkdown,
|
|
191
|
+
import { tokenizeBlocks, parseInline, renderMarkdown, getFootnotes, resetParserState } from "../../../helpers/formats/markdown";
|
|
192
|
+
import type { BlockToken, ListItem } from "../../../helpers/formats/markdown";
|
|
79
193
|
import { highlightJSON, highlightYAML } from "../../../helpers/formats/highlightSyntax";
|
|
80
194
|
import LanguageBadge from "./LanguageBadge.vue";
|
|
81
195
|
|
|
@@ -96,9 +210,30 @@ const convertedContent = reactive<Record<number, string>>({});
|
|
|
96
210
|
// Tokenize the markdown content
|
|
97
211
|
const tokens = computed<BlockToken[]>(() => {
|
|
98
212
|
if (!props.content) return [];
|
|
213
|
+
// Reset parser state before tokenizing to clear refs from previous content
|
|
214
|
+
// This ensures link refs and footnotes are freshly parsed for this content
|
|
215
|
+
resetParserState();
|
|
99
216
|
return tokenizeBlocks(props.content);
|
|
100
217
|
});
|
|
101
218
|
|
|
219
|
+
// Computed properties for footnotes
|
|
220
|
+
// IMPORTANT: Access tokens.value to ensure tokenizeBlocks runs first,
|
|
221
|
+
// which populates currentFootnotes and currentLinkRefs
|
|
222
|
+
const footnotes = computed(() => {
|
|
223
|
+
// Force dependency on tokens - this ensures tokenizeBlocks has run
|
|
224
|
+
// and populated the module-level currentFootnotes before we read it
|
|
225
|
+
tokens.value;
|
|
226
|
+
return getFootnotes();
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
const hasFootnotes = computed(() => Object.keys(footnotes.value).length > 0);
|
|
230
|
+
|
|
231
|
+
const sortedFootnotes = computed(() => {
|
|
232
|
+
return Object.entries(footnotes.value)
|
|
233
|
+
.sort((a, b) => a[1].index - b[1].index)
|
|
234
|
+
.map(([id, fn]) => ({ id, content: fn.content, index: fn.index }));
|
|
235
|
+
});
|
|
236
|
+
|
|
102
237
|
// Check if a language is toggleable (json or yaml)
|
|
103
238
|
function isToggleableLanguage(language: string): boolean {
|
|
104
239
|
if (!language) return false;
|
|
@@ -222,9 +357,28 @@ function parseInlineContent(text: string): string {
|
|
|
222
357
|
return parseInline(text, true);
|
|
223
358
|
}
|
|
224
359
|
|
|
360
|
+
// Render a list item with potential nested children
|
|
361
|
+
function renderListItem(item: ListItem): string {
|
|
362
|
+
let html = parseInline(item.content, true);
|
|
363
|
+
if (item.children && item.children.length > 0) {
|
|
364
|
+
for (const child of item.children) {
|
|
365
|
+
if (child.type === "ul") {
|
|
366
|
+
const items = child.items.map((i) => `<li>${renderListItem(i)}</li>`).join("");
|
|
367
|
+
html += `<ul>${items}</ul>`;
|
|
368
|
+
} else if (child.type === "ol") {
|
|
369
|
+
const items = child.items.map((i) => `<li>${renderListItem(i)}</li>`).join("");
|
|
370
|
+
const startAttr = child.start !== 1 ? ` start="${child.start}"` : "";
|
|
371
|
+
html += `<ol${startAttr}>${items}</ol>`;
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
return html;
|
|
376
|
+
}
|
|
377
|
+
|
|
225
378
|
// Render blockquote content (can contain nested markdown)
|
|
379
|
+
// Use preserveState to keep link refs and footnotes from parent document
|
|
226
380
|
function renderBlockquote(content: string): string {
|
|
227
|
-
return renderMarkdown(content);
|
|
381
|
+
return renderMarkdown(content, { preserveState: true });
|
|
228
382
|
}
|
|
229
383
|
</script>
|
|
230
384
|
|
|
@@ -3,3 +3,6 @@ export { default as CodeViewerCollapsed } from "./CodeViewerCollapsed.vue";
|
|
|
3
3
|
export { default as CodeViewerFooter } from "./CodeViewerFooter.vue";
|
|
4
4
|
export { default as LanguageBadge } from "./LanguageBadge.vue";
|
|
5
5
|
export { default as MarkdownContent } from "./MarkdownContent.vue";
|
|
6
|
+
|
|
7
|
+
// Re-export Markdown editor components
|
|
8
|
+
export * from "../Markdown";
|
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div
|
|
3
|
+
class="dx-context-menu-overlay"
|
|
4
|
+
@click.self="onClose"
|
|
5
|
+
@keydown.escape="onClose"
|
|
6
|
+
>
|
|
7
|
+
<div
|
|
8
|
+
ref="menuRef"
|
|
9
|
+
class="dx-context-menu"
|
|
10
|
+
:style="menuStyle"
|
|
11
|
+
>
|
|
12
|
+
<template v-for="(item, itemIndex) in items" :key="item.id">
|
|
13
|
+
<!-- Divider -->
|
|
14
|
+
<div v-if="item.divider" class="context-menu-divider" />
|
|
15
|
+
|
|
16
|
+
<!-- Regular menu item or submenu trigger -->
|
|
17
|
+
<template v-else>
|
|
18
|
+
<div
|
|
19
|
+
class="context-menu-item-wrapper"
|
|
20
|
+
@mouseenter="handleItemHover(item, itemIndex)"
|
|
21
|
+
@mouseleave="handleItemLeave"
|
|
22
|
+
>
|
|
23
|
+
<button
|
|
24
|
+
class="context-menu-item"
|
|
25
|
+
:class="{ disabled: item.disabled, 'has-children': item.children?.length }"
|
|
26
|
+
type="button"
|
|
27
|
+
:disabled="item.disabled"
|
|
28
|
+
@click="onItemClick(item)"
|
|
29
|
+
>
|
|
30
|
+
<span class="item-label">{{ item.label }}</span>
|
|
31
|
+
<span v-if="item.shortcut && !item.children" class="item-shortcut">{{ item.shortcut }}</span>
|
|
32
|
+
<span v-if="item.children?.length" class="item-chevron">▸</span>
|
|
33
|
+
</button>
|
|
34
|
+
|
|
35
|
+
<!-- Nested submenu -->
|
|
36
|
+
<div
|
|
37
|
+
v-if="item.children?.length && activeSubmenuId === item.id"
|
|
38
|
+
ref="submenuRefs"
|
|
39
|
+
class="dx-context-submenu"
|
|
40
|
+
:class="{ 'open-left': submenuOpenLeft }"
|
|
41
|
+
:data-item-id="item.id"
|
|
42
|
+
@mouseenter="handleSubmenuEnter"
|
|
43
|
+
@mouseleave="handleSubmenuLeave"
|
|
44
|
+
>
|
|
45
|
+
<template v-for="child in item.children" :key="child.id">
|
|
46
|
+
<!-- Child divider -->
|
|
47
|
+
<div v-if="child.divider" class="context-menu-divider" />
|
|
48
|
+
|
|
49
|
+
<!-- Child item -->
|
|
50
|
+
<button
|
|
51
|
+
v-else
|
|
52
|
+
class="context-menu-item"
|
|
53
|
+
:class="{ disabled: child.disabled }"
|
|
54
|
+
type="button"
|
|
55
|
+
:disabled="child.disabled"
|
|
56
|
+
@click="onItemClick(child)"
|
|
57
|
+
>
|
|
58
|
+
<span class="item-label">{{ child.label }}</span>
|
|
59
|
+
<span v-if="child.shortcut" class="item-shortcut">{{ child.shortcut }}</span>
|
|
60
|
+
</button>
|
|
61
|
+
</template>
|
|
62
|
+
</div>
|
|
63
|
+
</div>
|
|
64
|
+
</template>
|
|
65
|
+
</template>
|
|
66
|
+
</div>
|
|
67
|
+
</div>
|
|
68
|
+
</template>
|
|
69
|
+
|
|
70
|
+
<script setup lang="ts">
|
|
71
|
+
import { computed, onMounted, onUnmounted, ref } from "vue";
|
|
72
|
+
import type { ContextMenuItem } from "./types";
|
|
73
|
+
import type { PopoverPosition } from "@/composables/markdown";
|
|
74
|
+
|
|
75
|
+
export interface ContextMenuProps {
|
|
76
|
+
position: PopoverPosition;
|
|
77
|
+
items: ContextMenuItem[];
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const props = defineProps<ContextMenuProps>();
|
|
81
|
+
|
|
82
|
+
const emit = defineEmits<{
|
|
83
|
+
close: [];
|
|
84
|
+
action: [item: ContextMenuItem];
|
|
85
|
+
}>();
|
|
86
|
+
|
|
87
|
+
const menuRef = ref<HTMLElement | null>(null);
|
|
88
|
+
const activeSubmenuId = ref<string | null>(null);
|
|
89
|
+
const submenuOpenLeft = ref(false);
|
|
90
|
+
let hoverTimeout: ReturnType<typeof setTimeout> | null = null;
|
|
91
|
+
|
|
92
|
+
// Calculate menu position with viewport boundary detection
|
|
93
|
+
const menuStyle = computed(() => {
|
|
94
|
+
const menuHeight = 400; // Approximate max height for nested menus
|
|
95
|
+
const menuWidth = 320; // Match CSS max-width
|
|
96
|
+
const padding = 10;
|
|
97
|
+
|
|
98
|
+
let top = props.position.y;
|
|
99
|
+
let left = props.position.x;
|
|
100
|
+
|
|
101
|
+
// Check if menu would extend below viewport
|
|
102
|
+
if (top + menuHeight > window.innerHeight - padding) {
|
|
103
|
+
// Position above the cursor
|
|
104
|
+
top = Math.max(padding, props.position.y - menuHeight);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Ensure menu doesn't go off left edge
|
|
108
|
+
if (left < padding) {
|
|
109
|
+
left = padding;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Ensure menu doesn't go off right edge
|
|
113
|
+
if (left + menuWidth > window.innerWidth - padding) {
|
|
114
|
+
left = window.innerWidth - menuWidth - padding;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Determine if submenus should open to the left
|
|
118
|
+
// (if menu is positioned near right edge, submenus should open left)
|
|
119
|
+
submenuOpenLeft.value = left + menuWidth + menuWidth > window.innerWidth - padding;
|
|
120
|
+
|
|
121
|
+
return {
|
|
122
|
+
top: `${top}px`,
|
|
123
|
+
left: `${left}px`
|
|
124
|
+
};
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
function handleItemHover(item: ContextMenuItem, _index: number): void {
|
|
128
|
+
// Clear any pending timeout
|
|
129
|
+
if (hoverTimeout) {
|
|
130
|
+
clearTimeout(hoverTimeout);
|
|
131
|
+
hoverTimeout = null;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// If item has children, show submenu after a small delay
|
|
135
|
+
if (item.children?.length) {
|
|
136
|
+
hoverTimeout = setTimeout(() => {
|
|
137
|
+
activeSubmenuId.value = item.id;
|
|
138
|
+
}, 100);
|
|
139
|
+
} else {
|
|
140
|
+
// Immediately hide submenu for non-parent items
|
|
141
|
+
activeSubmenuId.value = null;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function handleItemLeave(): void {
|
|
146
|
+
// Clear pending timeout
|
|
147
|
+
if (hoverTimeout) {
|
|
148
|
+
clearTimeout(hoverTimeout);
|
|
149
|
+
hoverTimeout = null;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Set a timeout to close submenu (will be cancelled if mouse enters submenu)
|
|
153
|
+
hoverTimeout = setTimeout(() => {
|
|
154
|
+
activeSubmenuId.value = null;
|
|
155
|
+
}, 150);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function handleSubmenuEnter(): void {
|
|
159
|
+
// Cancel any pending close timeout when entering the submenu
|
|
160
|
+
if (hoverTimeout) {
|
|
161
|
+
clearTimeout(hoverTimeout);
|
|
162
|
+
hoverTimeout = null;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function handleSubmenuLeave(): void {
|
|
167
|
+
// Start timeout to close submenu when leaving
|
|
168
|
+
hoverTimeout = setTimeout(() => {
|
|
169
|
+
activeSubmenuId.value = null;
|
|
170
|
+
}, 150);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function onItemClick(item: ContextMenuItem): void {
|
|
174
|
+
if (item.disabled) return;
|
|
175
|
+
|
|
176
|
+
// If item has children, don't close - just toggle submenu
|
|
177
|
+
if (item.children?.length) {
|
|
178
|
+
activeSubmenuId.value = activeSubmenuId.value === item.id ? null : item.id;
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Execute action if available
|
|
183
|
+
if (item.action) {
|
|
184
|
+
emit("action", item);
|
|
185
|
+
item.action();
|
|
186
|
+
}
|
|
187
|
+
emit("close");
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function onClose(): void {
|
|
191
|
+
emit("close");
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Handle Escape key at document level
|
|
195
|
+
function handleDocumentKeydown(event: KeyboardEvent): void {
|
|
196
|
+
if (event.key === "Escape") {
|
|
197
|
+
onClose();
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
onMounted(() => {
|
|
202
|
+
document.addEventListener("keydown", handleDocumentKeydown);
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
onUnmounted(() => {
|
|
206
|
+
document.removeEventListener("keydown", handleDocumentKeydown);
|
|
207
|
+
if (hoverTimeout) {
|
|
208
|
+
clearTimeout(hoverTimeout);
|
|
209
|
+
}
|
|
210
|
+
});
|
|
211
|
+
</script>
|
|
212
|
+
|
|
213
|
+
<style lang="scss">
|
|
214
|
+
.dx-context-menu-overlay {
|
|
215
|
+
position: fixed;
|
|
216
|
+
inset: 0;
|
|
217
|
+
z-index: 1000;
|
|
218
|
+
// Transparent overlay - no visual background
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
.dx-context-menu {
|
|
222
|
+
position: fixed;
|
|
223
|
+
background: #2d2d2d;
|
|
224
|
+
border: 1px solid #404040;
|
|
225
|
+
border-radius: 0.375rem;
|
|
226
|
+
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.4);
|
|
227
|
+
min-width: 200px;
|
|
228
|
+
max-width: 320px;
|
|
229
|
+
overflow: visible;
|
|
230
|
+
padding: 0.25rem 0;
|
|
231
|
+
|
|
232
|
+
.context-menu-divider {
|
|
233
|
+
height: 1px;
|
|
234
|
+
background: #404040;
|
|
235
|
+
margin: 0.25rem 0;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
.context-menu-item-wrapper {
|
|
239
|
+
position: relative;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
.context-menu-item {
|
|
243
|
+
display: flex;
|
|
244
|
+
align-items: center;
|
|
245
|
+
justify-content: space-between;
|
|
246
|
+
width: 100%;
|
|
247
|
+
padding: 0.5rem 0.75rem;
|
|
248
|
+
background: transparent;
|
|
249
|
+
border: none;
|
|
250
|
+
color: #d4d4d4;
|
|
251
|
+
font-size: 0.875rem;
|
|
252
|
+
text-align: left;
|
|
253
|
+
cursor: pointer;
|
|
254
|
+
transition: background-color 0.15s ease;
|
|
255
|
+
|
|
256
|
+
&:hover:not(.disabled) {
|
|
257
|
+
background: rgba(255, 255, 255, 0.1);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
&.disabled {
|
|
261
|
+
color: #6b7280;
|
|
262
|
+
cursor: not-allowed;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
&.has-children {
|
|
266
|
+
padding-right: 0.5rem;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
.item-label {
|
|
270
|
+
flex: 1;
|
|
271
|
+
white-space: nowrap;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
.item-shortcut {
|
|
275
|
+
font-size: 0.75rem;
|
|
276
|
+
color: #6b7280;
|
|
277
|
+
font-family: 'Consolas', 'Monaco', monospace;
|
|
278
|
+
margin-left: 1rem;
|
|
279
|
+
white-space: nowrap;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
.item-chevron {
|
|
283
|
+
font-size: 0.75rem;
|
|
284
|
+
color: #6b7280;
|
|
285
|
+
margin-left: 0.5rem;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// Submenu styling
|
|
290
|
+
.dx-context-submenu {
|
|
291
|
+
position: absolute;
|
|
292
|
+
top: 0;
|
|
293
|
+
left: 100%;
|
|
294
|
+
margin-left: 2px;
|
|
295
|
+
background: #2d2d2d;
|
|
296
|
+
border: 1px solid #404040;
|
|
297
|
+
border-radius: 0.375rem;
|
|
298
|
+
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.4);
|
|
299
|
+
min-width: 280px;
|
|
300
|
+
max-width: 360px;
|
|
301
|
+
overflow: hidden;
|
|
302
|
+
padding: 0.25rem 0;
|
|
303
|
+
z-index: 1001;
|
|
304
|
+
|
|
305
|
+
// Open to the left when near right viewport edge
|
|
306
|
+
&.open-left {
|
|
307
|
+
left: auto;
|
|
308
|
+
right: 100%;
|
|
309
|
+
margin-left: 0;
|
|
310
|
+
margin-right: 2px;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
</style>
|