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
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div
|
|
3
|
+
ref="containerRef"
|
|
4
|
+
class="dx-markdown-editor-content dx-markdown-content"
|
|
5
|
+
:class="{ 'is-readonly': readonly, 'is-empty': isEmpty }"
|
|
6
|
+
:contenteditable="!readonly"
|
|
7
|
+
:data-placeholder="placeholder"
|
|
8
|
+
@input="$emit('input')"
|
|
9
|
+
@keydown="$emit('keydown', $event)"
|
|
10
|
+
@blur="$emit('blur')"
|
|
11
|
+
@click="handleClick"
|
|
12
|
+
v-html="html"
|
|
13
|
+
/>
|
|
14
|
+
</template>
|
|
15
|
+
|
|
16
|
+
<script setup lang="ts">
|
|
17
|
+
import { computed, ref } from "vue";
|
|
18
|
+
|
|
19
|
+
export interface MarkdownEditorContentProps {
|
|
20
|
+
html: string;
|
|
21
|
+
readonly?: boolean;
|
|
22
|
+
placeholder?: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const props = withDefaults(defineProps<MarkdownEditorContentProps>(), {
|
|
26
|
+
readonly: false,
|
|
27
|
+
placeholder: "Start typing..."
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
defineEmits<{
|
|
31
|
+
input: [];
|
|
32
|
+
keydown: [event: KeyboardEvent];
|
|
33
|
+
blur: [];
|
|
34
|
+
}>();
|
|
35
|
+
|
|
36
|
+
const containerRef = ref<HTMLElement | null>(null);
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Find the anchor element if the click target is inside one
|
|
40
|
+
*/
|
|
41
|
+
function findLinkAncestor(node: Node | null): HTMLAnchorElement | null {
|
|
42
|
+
if (!node || !containerRef.value) return null;
|
|
43
|
+
|
|
44
|
+
let current: Node | null = node;
|
|
45
|
+
while (current && current !== containerRef.value) {
|
|
46
|
+
if (current.nodeType === Node.ELEMENT_NODE && (current as Element).tagName === "A") {
|
|
47
|
+
return current as HTMLAnchorElement;
|
|
48
|
+
}
|
|
49
|
+
current = current.parentNode;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Handle clicks in the editor content.
|
|
57
|
+
* Ctrl+Click (or Cmd+Click on Mac) opens links in a new tab.
|
|
58
|
+
*/
|
|
59
|
+
function handleClick(event: MouseEvent): void {
|
|
60
|
+
// Check if Ctrl (Windows/Linux) or Cmd (Mac) is held
|
|
61
|
+
const isModifierHeld = event.ctrlKey || event.metaKey;
|
|
62
|
+
if (!isModifierHeld) return;
|
|
63
|
+
|
|
64
|
+
// Find if the click was on or inside a link
|
|
65
|
+
const link = findLinkAncestor(event.target as Node);
|
|
66
|
+
if (!link) return;
|
|
67
|
+
|
|
68
|
+
const href = link.getAttribute("href");
|
|
69
|
+
if (!href) return;
|
|
70
|
+
|
|
71
|
+
// Prevent default behavior and open the link in a new tab
|
|
72
|
+
event.preventDefault();
|
|
73
|
+
event.stopPropagation();
|
|
74
|
+
window.open(href, "_blank", "noopener,noreferrer");
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const isEmpty = computed(() => {
|
|
78
|
+
return !props.html || props.html === "<p></p>" || props.html === "<p><br></p>";
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
// Expose containerRef for parent component
|
|
82
|
+
defineExpose({ containerRef });
|
|
83
|
+
</script>
|
|
84
|
+
|
|
85
|
+
<style lang="scss">
|
|
86
|
+
.dx-markdown-editor-content {
|
|
87
|
+
min-height: 100px;
|
|
88
|
+
outline: none;
|
|
89
|
+
cursor: text;
|
|
90
|
+
border: 2px solid transparent;
|
|
91
|
+
border-radius: 0.375rem 0.375rem 0 0;
|
|
92
|
+
padding: 1rem;
|
|
93
|
+
background-color: #1e1e1e;
|
|
94
|
+
color: #d4d4d4;
|
|
95
|
+
transition: border-color 0.2s ease;
|
|
96
|
+
overflow: auto;
|
|
97
|
+
|
|
98
|
+
&:focus {
|
|
99
|
+
border-color: rgba(86, 156, 214, 0.6);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
&:hover:not(:focus):not(.is-readonly) {
|
|
103
|
+
border-color: rgba(86, 156, 214, 0.3);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
&.is-readonly {
|
|
107
|
+
cursor: default;
|
|
108
|
+
border-color: transparent;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Placeholder styling when empty
|
|
112
|
+
&.is-empty::before {
|
|
113
|
+
content: attr(data-placeholder);
|
|
114
|
+
color: #6b7280;
|
|
115
|
+
pointer-events: none;
|
|
116
|
+
position: absolute;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
&.is-empty {
|
|
120
|
+
position: relative;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Caret color
|
|
124
|
+
caret-color: #d4d4d4;
|
|
125
|
+
|
|
126
|
+
// Link styling - show pointer cursor and hint for Ctrl+Click
|
|
127
|
+
a {
|
|
128
|
+
cursor: pointer;
|
|
129
|
+
position: relative;
|
|
130
|
+
|
|
131
|
+
&:hover::after {
|
|
132
|
+
content: "Ctrl+Click to open";
|
|
133
|
+
position: absolute;
|
|
134
|
+
bottom: 100%;
|
|
135
|
+
left: 50%;
|
|
136
|
+
transform: translateX(-50%);
|
|
137
|
+
background: #374151;
|
|
138
|
+
color: #d1d5db;
|
|
139
|
+
padding: 0.25rem 0.5rem;
|
|
140
|
+
border-radius: 0.25rem;
|
|
141
|
+
font-size: 0.75rem;
|
|
142
|
+
white-space: nowrap;
|
|
143
|
+
z-index: 10;
|
|
144
|
+
pointer-events: none;
|
|
145
|
+
opacity: 0;
|
|
146
|
+
animation: fadeIn 0.2s ease 0.5s forwards;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
@keyframes fadeIn {
|
|
151
|
+
to {
|
|
152
|
+
opacity: 1;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Alternating styles for nested ordered lists
|
|
157
|
+
// Level 1: decimal (1, 2, 3)
|
|
158
|
+
ol {
|
|
159
|
+
list-style-type: decimal;
|
|
160
|
+
|
|
161
|
+
// Level 2: lower-roman (i, ii, iii)
|
|
162
|
+
ol {
|
|
163
|
+
list-style-type: lower-roman;
|
|
164
|
+
|
|
165
|
+
// Level 3: lower-alpha (a, b, c)
|
|
166
|
+
ol {
|
|
167
|
+
list-style-type: lower-alpha;
|
|
168
|
+
|
|
169
|
+
// Level 4+: cycle back to decimal
|
|
170
|
+
ol {
|
|
171
|
+
list-style-type: decimal;
|
|
172
|
+
|
|
173
|
+
ol {
|
|
174
|
+
list-style-type: lower-roman;
|
|
175
|
+
|
|
176
|
+
ol {
|
|
177
|
+
list-style-type: lower-alpha;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Alternating styles for nested unordered lists
|
|
186
|
+
// Level 1: disc
|
|
187
|
+
ul {
|
|
188
|
+
list-style-type: disc;
|
|
189
|
+
|
|
190
|
+
// Level 2: circle
|
|
191
|
+
ul {
|
|
192
|
+
list-style-type: circle;
|
|
193
|
+
|
|
194
|
+
// Level 3: square
|
|
195
|
+
ul {
|
|
196
|
+
list-style-type: square;
|
|
197
|
+
|
|
198
|
+
// Level 4+: cycle back to disc
|
|
199
|
+
ul {
|
|
200
|
+
list-style-type: disc;
|
|
201
|
+
|
|
202
|
+
ul {
|
|
203
|
+
list-style-type: circle;
|
|
204
|
+
|
|
205
|
+
ul {
|
|
206
|
+
list-style-type: square;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Code block wrapper styling - distinct background to separate from editor content
|
|
215
|
+
.code-block-wrapper {
|
|
216
|
+
background: #0d1117;
|
|
217
|
+
border-radius: 0.375rem;
|
|
218
|
+
margin: 0.5rem 0;
|
|
219
|
+
border: 1px solid #30363d;
|
|
220
|
+
|
|
221
|
+
// Override CodeViewer backgrounds to be transparent so wrapper controls it
|
|
222
|
+
.dx-code-viewer {
|
|
223
|
+
.code-content {
|
|
224
|
+
background: transparent;
|
|
225
|
+
border-radius: 0.375rem 0.375rem 0 0;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
.code-footer {
|
|
229
|
+
background: #161b22;
|
|
230
|
+
border-radius: 0 0 0.375rem 0.375rem;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
</style>
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="dx-markdown-editor-footer flex items-center justify-between px-2 py-1">
|
|
3
|
+
<span class="char-count text-xs text-gray-500">
|
|
4
|
+
{{ charCount.toLocaleString() }} chars
|
|
5
|
+
</span>
|
|
6
|
+
<button
|
|
7
|
+
class="hotkey-help-btn text-gray-500 hover:text-gray-300 transition-colors p-1 rounded"
|
|
8
|
+
title="Keyboard shortcuts (Ctrl+?)"
|
|
9
|
+
type="button"
|
|
10
|
+
@click="$emit('show-hotkeys')"
|
|
11
|
+
>
|
|
12
|
+
<KeyboardIcon class="w-4 h-4" />
|
|
13
|
+
</button>
|
|
14
|
+
</div>
|
|
15
|
+
</template>
|
|
16
|
+
|
|
17
|
+
<script setup lang="ts">
|
|
18
|
+
import { FaSolidKeyboard as KeyboardIcon } from "danx-icon";
|
|
19
|
+
|
|
20
|
+
export interface MarkdownEditorFooterProps {
|
|
21
|
+
charCount: number;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
defineProps<MarkdownEditorFooterProps>();
|
|
25
|
+
|
|
26
|
+
defineEmits<{
|
|
27
|
+
"show-hotkeys": [];
|
|
28
|
+
}>();
|
|
29
|
+
</script>
|
|
30
|
+
|
|
31
|
+
<style lang="scss">
|
|
32
|
+
.dx-markdown-editor-footer {
|
|
33
|
+
background-color: #252526;
|
|
34
|
+
border-radius: 0 0 0.375rem 0.375rem;
|
|
35
|
+
flex-shrink: 0;
|
|
36
|
+
|
|
37
|
+
.hotkey-help-btn {
|
|
38
|
+
display: flex;
|
|
39
|
+
align-items: center;
|
|
40
|
+
justify-content: center;
|
|
41
|
+
background: transparent;
|
|
42
|
+
border: none;
|
|
43
|
+
cursor: pointer;
|
|
44
|
+
|
|
45
|
+
&:hover {
|
|
46
|
+
background-color: rgba(255, 255, 255, 0.1);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
</style>
|
|
@@ -0,0 +1,420 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div
|
|
3
|
+
class="dx-table-popover-overlay"
|
|
4
|
+
@click.self="onCancel"
|
|
5
|
+
@keydown.escape="onCancel"
|
|
6
|
+
>
|
|
7
|
+
<div
|
|
8
|
+
ref="popoverRef"
|
|
9
|
+
class="dx-table-popover"
|
|
10
|
+
:style="popoverStyle"
|
|
11
|
+
>
|
|
12
|
+
<div class="popover-header">
|
|
13
|
+
<h3>Insert Table</h3>
|
|
14
|
+
<button
|
|
15
|
+
class="close-btn"
|
|
16
|
+
type="button"
|
|
17
|
+
aria-label="Close"
|
|
18
|
+
@click="onCancel"
|
|
19
|
+
>
|
|
20
|
+
<CloseIcon class="w-4 h-4" />
|
|
21
|
+
</button>
|
|
22
|
+
</div>
|
|
23
|
+
|
|
24
|
+
<div class="popover-content">
|
|
25
|
+
<!-- Visual Grid Selector -->
|
|
26
|
+
<div class="grid-selector">
|
|
27
|
+
<div
|
|
28
|
+
v-for="row in GRID_SIZE"
|
|
29
|
+
:key="row"
|
|
30
|
+
class="grid-row"
|
|
31
|
+
>
|
|
32
|
+
<div
|
|
33
|
+
v-for="col in GRID_SIZE"
|
|
34
|
+
:key="col"
|
|
35
|
+
class="grid-cell"
|
|
36
|
+
:class="{ selected: row <= hoverRows && col <= hoverCols }"
|
|
37
|
+
@mouseenter="onCellHover(row, col)"
|
|
38
|
+
@click="onCellClick(row, col)"
|
|
39
|
+
/>
|
|
40
|
+
</div>
|
|
41
|
+
</div>
|
|
42
|
+
|
|
43
|
+
<!-- Dimension Label -->
|
|
44
|
+
<div class="dimension-label">
|
|
45
|
+
{{ hoverRows }} x {{ hoverCols }}
|
|
46
|
+
</div>
|
|
47
|
+
|
|
48
|
+
<!-- Manual Input Divider -->
|
|
49
|
+
<div class="divider">
|
|
50
|
+
<span>or enter manually</span>
|
|
51
|
+
</div>
|
|
52
|
+
|
|
53
|
+
<!-- Manual Input Fields -->
|
|
54
|
+
<div class="manual-inputs">
|
|
55
|
+
<div class="input-group">
|
|
56
|
+
<label for="table-rows">Rows</label>
|
|
57
|
+
<input
|
|
58
|
+
id="table-rows"
|
|
59
|
+
v-model.number="manualRows"
|
|
60
|
+
type="number"
|
|
61
|
+
min="1"
|
|
62
|
+
:max="MAX_ROWS"
|
|
63
|
+
@keydown.enter.prevent="onSubmit"
|
|
64
|
+
@keydown.escape="onCancel"
|
|
65
|
+
>
|
|
66
|
+
</div>
|
|
67
|
+
<div class="input-group">
|
|
68
|
+
<label for="table-cols">Cols</label>
|
|
69
|
+
<input
|
|
70
|
+
id="table-cols"
|
|
71
|
+
v-model.number="manualCols"
|
|
72
|
+
type="number"
|
|
73
|
+
min="1"
|
|
74
|
+
:max="MAX_COLS"
|
|
75
|
+
@keydown.enter.prevent="onSubmit"
|
|
76
|
+
@keydown.escape="onCancel"
|
|
77
|
+
>
|
|
78
|
+
</div>
|
|
79
|
+
</div>
|
|
80
|
+
</div>
|
|
81
|
+
|
|
82
|
+
<div class="popover-footer">
|
|
83
|
+
<button
|
|
84
|
+
type="button"
|
|
85
|
+
class="btn-cancel"
|
|
86
|
+
@click="onCancel"
|
|
87
|
+
>
|
|
88
|
+
Cancel
|
|
89
|
+
</button>
|
|
90
|
+
<button
|
|
91
|
+
type="button"
|
|
92
|
+
class="btn-insert"
|
|
93
|
+
@click="onSubmit"
|
|
94
|
+
>
|
|
95
|
+
Insert
|
|
96
|
+
</button>
|
|
97
|
+
</div>
|
|
98
|
+
</div>
|
|
99
|
+
</div>
|
|
100
|
+
</template>
|
|
101
|
+
|
|
102
|
+
<script setup lang="ts">
|
|
103
|
+
import { FaSolidXmark as CloseIcon } from "danx-icon";
|
|
104
|
+
import { computed, onMounted, onUnmounted, ref } from "vue";
|
|
105
|
+
import type { PopoverPosition } from "@/composables/markdown";
|
|
106
|
+
|
|
107
|
+
export interface TablePopoverProps {
|
|
108
|
+
position: PopoverPosition;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const GRID_SIZE = 5;
|
|
112
|
+
const MAX_ROWS = 20;
|
|
113
|
+
const MAX_COLS = 10;
|
|
114
|
+
const DEFAULT_SIZE = 3;
|
|
115
|
+
|
|
116
|
+
const props = defineProps<TablePopoverProps>();
|
|
117
|
+
|
|
118
|
+
const emit = defineEmits<{
|
|
119
|
+
submit: [rows: number, cols: number];
|
|
120
|
+
cancel: [];
|
|
121
|
+
}>();
|
|
122
|
+
|
|
123
|
+
// Refs
|
|
124
|
+
const popoverRef = ref<HTMLElement | null>(null);
|
|
125
|
+
|
|
126
|
+
// State
|
|
127
|
+
const hoverRows = ref(DEFAULT_SIZE);
|
|
128
|
+
const hoverCols = ref(DEFAULT_SIZE);
|
|
129
|
+
const manualRows = ref(DEFAULT_SIZE);
|
|
130
|
+
const manualCols = ref(DEFAULT_SIZE);
|
|
131
|
+
|
|
132
|
+
// Calculate popover position (below cursor by default, above if at bottom of viewport)
|
|
133
|
+
const popoverStyle = computed(() => {
|
|
134
|
+
const popoverHeight = 340; // Approximate height
|
|
135
|
+
const popoverWidth = 280;
|
|
136
|
+
const padding = 10;
|
|
137
|
+
|
|
138
|
+
let top = props.position.y + padding;
|
|
139
|
+
let left = props.position.x - (popoverWidth / 2);
|
|
140
|
+
|
|
141
|
+
// Check if popover would extend below viewport
|
|
142
|
+
if (top + popoverHeight > window.innerHeight - padding) {
|
|
143
|
+
// Position above the cursor
|
|
144
|
+
top = props.position.y - popoverHeight - padding;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Ensure popover doesn't go off left edge
|
|
148
|
+
if (left < padding) {
|
|
149
|
+
left = padding;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Ensure popover doesn't go off right edge
|
|
153
|
+
if (left + popoverWidth > window.innerWidth - padding) {
|
|
154
|
+
left = window.innerWidth - popoverWidth - padding;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return {
|
|
158
|
+
top: `${top}px`,
|
|
159
|
+
left: `${left}px`
|
|
160
|
+
};
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
// Methods
|
|
164
|
+
function onCellHover(row: number, col: number): void {
|
|
165
|
+
hoverRows.value = row;
|
|
166
|
+
hoverCols.value = col;
|
|
167
|
+
manualRows.value = row;
|
|
168
|
+
manualCols.value = col;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function onCellClick(row: number, col: number): void {
|
|
172
|
+
emit("submit", row, col);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function onSubmit(): void {
|
|
176
|
+
const rows = Math.min(Math.max(1, manualRows.value), MAX_ROWS);
|
|
177
|
+
const cols = Math.min(Math.max(1, manualCols.value), MAX_COLS);
|
|
178
|
+
emit("submit", rows, cols);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function onCancel(): void {
|
|
182
|
+
emit("cancel");
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Handle Escape key at document level
|
|
186
|
+
function handleDocumentKeydown(event: KeyboardEvent): void {
|
|
187
|
+
if (event.key === "Escape") {
|
|
188
|
+
onCancel();
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
onMounted(() => {
|
|
193
|
+
document.addEventListener("keydown", handleDocumentKeydown);
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
onUnmounted(() => {
|
|
197
|
+
document.removeEventListener("keydown", handleDocumentKeydown);
|
|
198
|
+
});
|
|
199
|
+
</script>
|
|
200
|
+
|
|
201
|
+
<style lang="scss">
|
|
202
|
+
.dx-table-popover-overlay {
|
|
203
|
+
position: fixed;
|
|
204
|
+
inset: 0;
|
|
205
|
+
z-index: 1000;
|
|
206
|
+
background: rgba(0, 0, 0, 0.3);
|
|
207
|
+
backdrop-filter: blur(1px);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
.dx-table-popover {
|
|
211
|
+
position: fixed;
|
|
212
|
+
background: #2d2d2d;
|
|
213
|
+
border: 1px solid #404040;
|
|
214
|
+
border-radius: 0.5rem;
|
|
215
|
+
box-shadow: 0 25px 50px rgba(0, 0, 0, 0.5);
|
|
216
|
+
width: 280px;
|
|
217
|
+
overflow: hidden;
|
|
218
|
+
display: flex;
|
|
219
|
+
flex-direction: column;
|
|
220
|
+
|
|
221
|
+
.popover-header {
|
|
222
|
+
display: flex;
|
|
223
|
+
align-items: center;
|
|
224
|
+
justify-content: space-between;
|
|
225
|
+
padding: 0.875rem 1rem;
|
|
226
|
+
border-bottom: 1px solid #404040;
|
|
227
|
+
|
|
228
|
+
h3 {
|
|
229
|
+
margin: 0;
|
|
230
|
+
font-size: 0.9375rem;
|
|
231
|
+
font-weight: 600;
|
|
232
|
+
color: #f3f4f6;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
.close-btn {
|
|
236
|
+
display: flex;
|
|
237
|
+
align-items: center;
|
|
238
|
+
justify-content: center;
|
|
239
|
+
width: 1.5rem;
|
|
240
|
+
height: 1.5rem;
|
|
241
|
+
padding: 0;
|
|
242
|
+
background: transparent;
|
|
243
|
+
border: none;
|
|
244
|
+
border-radius: 0.25rem;
|
|
245
|
+
color: #9ca3af;
|
|
246
|
+
cursor: pointer;
|
|
247
|
+
transition: all 0.15s ease;
|
|
248
|
+
|
|
249
|
+
&:hover {
|
|
250
|
+
background: rgba(255, 255, 255, 0.1);
|
|
251
|
+
color: #f3f4f6;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
.popover-content {
|
|
257
|
+
padding: 1rem;
|
|
258
|
+
display: flex;
|
|
259
|
+
flex-direction: column;
|
|
260
|
+
align-items: center;
|
|
261
|
+
gap: 0.75rem;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
.grid-selector {
|
|
265
|
+
display: flex;
|
|
266
|
+
flex-direction: column;
|
|
267
|
+
gap: 3px;
|
|
268
|
+
padding: 0.5rem;
|
|
269
|
+
background: #1e1e1e;
|
|
270
|
+
border-radius: 0.375rem;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
.grid-row {
|
|
274
|
+
display: flex;
|
|
275
|
+
gap: 3px;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
.grid-cell {
|
|
279
|
+
width: 28px;
|
|
280
|
+
height: 28px;
|
|
281
|
+
background: #3a3a3a;
|
|
282
|
+
border: 1px solid #4a4a4a;
|
|
283
|
+
border-radius: 3px;
|
|
284
|
+
cursor: pointer;
|
|
285
|
+
transition: all 0.1s ease;
|
|
286
|
+
|
|
287
|
+
&:hover {
|
|
288
|
+
border-color: #60a5fa;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
&.selected {
|
|
292
|
+
background: #3b82f6;
|
|
293
|
+
border-color: #60a5fa;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
.dimension-label {
|
|
298
|
+
font-size: 0.875rem;
|
|
299
|
+
font-weight: 600;
|
|
300
|
+
color: #d4d4d4;
|
|
301
|
+
margin-top: 0.25rem;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
.divider {
|
|
305
|
+
display: flex;
|
|
306
|
+
align-items: center;
|
|
307
|
+
width: 100%;
|
|
308
|
+
margin: 0.5rem 0;
|
|
309
|
+
|
|
310
|
+
&::before,
|
|
311
|
+
&::after {
|
|
312
|
+
content: '';
|
|
313
|
+
flex: 1;
|
|
314
|
+
height: 1px;
|
|
315
|
+
background: #404040;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
span {
|
|
319
|
+
padding: 0 0.75rem;
|
|
320
|
+
font-size: 0.75rem;
|
|
321
|
+
color: #6b7280;
|
|
322
|
+
white-space: nowrap;
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
.manual-inputs {
|
|
327
|
+
display: flex;
|
|
328
|
+
gap: 1rem;
|
|
329
|
+
width: 100%;
|
|
330
|
+
justify-content: center;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
.input-group {
|
|
334
|
+
display: flex;
|
|
335
|
+
flex-direction: column;
|
|
336
|
+
gap: 0.25rem;
|
|
337
|
+
|
|
338
|
+
label {
|
|
339
|
+
font-size: 0.75rem;
|
|
340
|
+
font-weight: 500;
|
|
341
|
+
text-transform: uppercase;
|
|
342
|
+
letter-spacing: 0.05em;
|
|
343
|
+
color: #9ca3af;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
input {
|
|
347
|
+
width: 70px;
|
|
348
|
+
padding: 0.5rem 0.75rem;
|
|
349
|
+
background: #1e1e1e;
|
|
350
|
+
border: 1px solid #404040;
|
|
351
|
+
border-radius: 0.375rem;
|
|
352
|
+
font-size: 0.875rem;
|
|
353
|
+
color: #f3f4f6;
|
|
354
|
+
outline: none;
|
|
355
|
+
text-align: center;
|
|
356
|
+
transition: border-color 0.15s ease;
|
|
357
|
+
|
|
358
|
+
&::placeholder {
|
|
359
|
+
color: #6b7280;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
&:focus {
|
|
363
|
+
border-color: #60a5fa;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/* Hide number input spinners */
|
|
367
|
+
&::-webkit-outer-spin-button,
|
|
368
|
+
&::-webkit-inner-spin-button {
|
|
369
|
+
-webkit-appearance: none;
|
|
370
|
+
margin: 0;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/* Firefox */
|
|
374
|
+
&[type=number] {
|
|
375
|
+
-moz-appearance: textfield;
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
.popover-footer {
|
|
381
|
+
display: flex;
|
|
382
|
+
justify-content: flex-end;
|
|
383
|
+
gap: 0.5rem;
|
|
384
|
+
padding: 0.75rem 1rem;
|
|
385
|
+
border-top: 1px solid #404040;
|
|
386
|
+
background: rgba(0, 0, 0, 0.2);
|
|
387
|
+
|
|
388
|
+
button {
|
|
389
|
+
padding: 0.5rem 1rem;
|
|
390
|
+
font-size: 0.875rem;
|
|
391
|
+
font-weight: 500;
|
|
392
|
+
border-radius: 0.375rem;
|
|
393
|
+
cursor: pointer;
|
|
394
|
+
transition: all 0.15s ease;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
.btn-cancel {
|
|
398
|
+
background: transparent;
|
|
399
|
+
border: 1px solid #404040;
|
|
400
|
+
color: #d4d4d4;
|
|
401
|
+
|
|
402
|
+
&:hover {
|
|
403
|
+
background: rgba(255, 255, 255, 0.05);
|
|
404
|
+
border-color: #525252;
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
.btn-insert {
|
|
409
|
+
background: #3b82f6;
|
|
410
|
+
border: 1px solid #3b82f6;
|
|
411
|
+
color: #ffffff;
|
|
412
|
+
|
|
413
|
+
&:hover {
|
|
414
|
+
background: #2563eb;
|
|
415
|
+
border-color: #2563eb;
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
</style>
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export { default as ContextMenu } from "./ContextMenu.vue";
|
|
2
|
+
export { default as HotkeyHelpPopover } from "./HotkeyHelpPopover.vue";
|
|
3
|
+
export { default as LineTypeMenu } from "./LineTypeMenu.vue";
|
|
4
|
+
export { default as LinkPopover } from "./LinkPopover.vue";
|
|
5
|
+
export { default as MarkdownEditor } from "./MarkdownEditor.vue";
|
|
6
|
+
export { default as MarkdownEditorContent } from "./MarkdownEditorContent.vue";
|
|
7
|
+
export { default as MarkdownEditorFooter } from "./MarkdownEditorFooter.vue";
|
|
8
|
+
export { default as TablePopover } from "./TablePopover.vue";
|
|
9
|
+
|
|
10
|
+
// Re-export types
|
|
11
|
+
export * from "./types";
|