quasar-ui-danx 0.5.0 → 0.5.2
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/.claude/settings.local.json +8 -0
- package/dist/danx.es.js +16119 -10641
- package/dist/danx.es.js.map +1 -1
- package/dist/danx.umd.js +202 -123
- package/dist/danx.umd.js.map +1 -1
- package/dist/style.css +1 -1
- package/package.json +8 -1
- package/src/components/Utility/Buttons/ActionButton.vue +15 -5
- package/src/components/Utility/Code/CodeViewer.vue +41 -16
- package/src/components/Utility/Code/CodeViewerCollapsed.vue +2 -0
- package/src/components/Utility/Code/CodeViewerFooter.vue +3 -1
- package/src/components/Utility/Code/LanguageBadge.vue +278 -5
- package/src/components/Utility/Code/MarkdownContent.vue +31 -163
- 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 +233 -0
- package/src/components/Utility/Markdown/MarkdownEditorContent.vue +296 -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/Widgets/LabelPillWidget.vue +20 -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 +805 -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 +388 -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 +1077 -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/useCodeFormat.ts +17 -10
- package/src/composables/useCodeViewerEditor.spec.ts +655 -0
- package/src/composables/useCodeViewerEditor.ts +174 -20
- package/src/helpers/formats/highlightCSS.ts +236 -0
- package/src/helpers/formats/highlightHTML.ts +483 -0
- package/src/helpers/formats/highlightJavaScript.ts +346 -0
- package/src/helpers/formats/highlightSyntax.ts +15 -4
- package/src/helpers/formats/index.ts +3 -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 +425 -0
- package/src/helpers/formats/markdown/index.ts +7 -0
- package/src/helpers/formats/markdown/linePatterns.spec.ts +498 -0
- package/src/helpers/formats/markdown/linePatterns.ts +172 -0
- package/src/styles/danx.scss +3 -3
- package/src/styles/index.scss +5 -5
- package/src/styles/themes/danx/code.scss +257 -1
- package/src/styles/themes/danx/index.scss +10 -10
- package/src/styles/themes/danx/markdown.scss +59 -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/highlighters.test.ts +153 -0
- package/src/test/setup.test.ts +12 -0
- package/src/test/setup.ts +12 -0
- package/src/types/widgets.d.ts +2 -2
- package/vite.config.js +5 -1
- package/vitest.config.ts +19 -0
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div
|
|
3
|
+
class="dx-link-popover-overlay"
|
|
4
|
+
@click.self="onCancel"
|
|
5
|
+
@keydown.escape="onCancel"
|
|
6
|
+
>
|
|
7
|
+
<div
|
|
8
|
+
ref="popoverRef"
|
|
9
|
+
class="dx-link-popover"
|
|
10
|
+
:style="popoverStyle"
|
|
11
|
+
>
|
|
12
|
+
<div class="popover-header">
|
|
13
|
+
<h3>{{ isEditing ? 'Edit Link' : 'Insert Link' }}</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
|
+
<div class="input-group">
|
|
26
|
+
<label for="link-url">URL</label>
|
|
27
|
+
<input
|
|
28
|
+
id="link-url"
|
|
29
|
+
ref="urlInputRef"
|
|
30
|
+
v-model="urlValue"
|
|
31
|
+
type="text"
|
|
32
|
+
placeholder="https://example.com"
|
|
33
|
+
@keydown.enter.prevent="onSubmit"
|
|
34
|
+
@keydown.escape="onCancel"
|
|
35
|
+
>
|
|
36
|
+
</div>
|
|
37
|
+
|
|
38
|
+
<div
|
|
39
|
+
v-if="!isEditing"
|
|
40
|
+
class="input-group"
|
|
41
|
+
>
|
|
42
|
+
<label for="link-label">Label</label>
|
|
43
|
+
<input
|
|
44
|
+
id="link-label"
|
|
45
|
+
v-model="labelValue"
|
|
46
|
+
type="text"
|
|
47
|
+
:placeholder="labelPlaceholder"
|
|
48
|
+
@keydown.enter.prevent="onSubmit"
|
|
49
|
+
@keydown.escape="onCancel"
|
|
50
|
+
>
|
|
51
|
+
</div>
|
|
52
|
+
|
|
53
|
+
<div
|
|
54
|
+
v-if="isEditing"
|
|
55
|
+
class="edit-hint"
|
|
56
|
+
>
|
|
57
|
+
Enter an empty URL to remove the link.
|
|
58
|
+
</div>
|
|
59
|
+
</div>
|
|
60
|
+
|
|
61
|
+
<div class="popover-footer">
|
|
62
|
+
<button
|
|
63
|
+
type="button"
|
|
64
|
+
class="btn-cancel"
|
|
65
|
+
@click="onCancel"
|
|
66
|
+
>
|
|
67
|
+
Cancel
|
|
68
|
+
</button>
|
|
69
|
+
<button
|
|
70
|
+
type="button"
|
|
71
|
+
class="btn-insert"
|
|
72
|
+
@click="onSubmit"
|
|
73
|
+
>
|
|
74
|
+
{{ isEditing ? 'Update' : 'Insert' }}
|
|
75
|
+
</button>
|
|
76
|
+
</div>
|
|
77
|
+
</div>
|
|
78
|
+
</div>
|
|
79
|
+
</template>
|
|
80
|
+
|
|
81
|
+
<script setup lang="ts">
|
|
82
|
+
import { FaSolidXmark as CloseIcon } from "danx-icon";
|
|
83
|
+
import { computed, nextTick, onMounted, onUnmounted, ref, watch } from "vue";
|
|
84
|
+
import type { PopoverPosition } from "@/composables/markdown";
|
|
85
|
+
|
|
86
|
+
export interface LinkPopoverProps {
|
|
87
|
+
position: PopoverPosition;
|
|
88
|
+
existingUrl?: string;
|
|
89
|
+
selectedText?: string;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const props = withDefaults(defineProps<LinkPopoverProps>(), {
|
|
93
|
+
existingUrl: "",
|
|
94
|
+
selectedText: ""
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
const emit = defineEmits<{
|
|
98
|
+
submit: [url: string, label?: string];
|
|
99
|
+
cancel: [];
|
|
100
|
+
}>();
|
|
101
|
+
|
|
102
|
+
// Refs
|
|
103
|
+
const popoverRef = ref<HTMLElement | null>(null);
|
|
104
|
+
const urlInputRef = ref<HTMLInputElement | null>(null);
|
|
105
|
+
|
|
106
|
+
// State
|
|
107
|
+
const urlValue = ref(props.existingUrl || "");
|
|
108
|
+
const labelValue = ref("");
|
|
109
|
+
|
|
110
|
+
// Computed
|
|
111
|
+
const isEditing = computed(() => !!props.existingUrl);
|
|
112
|
+
|
|
113
|
+
const labelPlaceholder = computed(() => {
|
|
114
|
+
if (props.selectedText) {
|
|
115
|
+
return props.selectedText;
|
|
116
|
+
}
|
|
117
|
+
return "Link text (optional)";
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
// Calculate popover position (below cursor by default, above if at bottom of viewport)
|
|
121
|
+
const popoverStyle = computed(() => {
|
|
122
|
+
const popoverHeight = 200; // Approximate height
|
|
123
|
+
const popoverWidth = 320;
|
|
124
|
+
const padding = 10;
|
|
125
|
+
|
|
126
|
+
let top = props.position.y + padding;
|
|
127
|
+
let left = props.position.x - (popoverWidth / 2);
|
|
128
|
+
|
|
129
|
+
// Check if popover would extend below viewport
|
|
130
|
+
if (top + popoverHeight > window.innerHeight - padding) {
|
|
131
|
+
// Position above the cursor
|
|
132
|
+
top = props.position.y - popoverHeight - padding;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Ensure popover doesn't go off left edge
|
|
136
|
+
if (left < padding) {
|
|
137
|
+
left = padding;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Ensure popover doesn't go off right edge
|
|
141
|
+
if (left + popoverWidth > window.innerWidth - padding) {
|
|
142
|
+
left = window.innerWidth - popoverWidth - padding;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return {
|
|
146
|
+
top: `${top}px`,
|
|
147
|
+
left: `${left}px`
|
|
148
|
+
};
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
// Methods
|
|
152
|
+
function onSubmit(): void {
|
|
153
|
+
const url = urlValue.value.trim();
|
|
154
|
+
const label = labelValue.value.trim() || undefined;
|
|
155
|
+
emit("submit", url, label);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function onCancel(): void {
|
|
159
|
+
emit("cancel");
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Handle Escape key at document level
|
|
163
|
+
function handleDocumentKeydown(event: KeyboardEvent): void {
|
|
164
|
+
if (event.key === "Escape") {
|
|
165
|
+
onCancel();
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Auto-focus URL input on mount
|
|
170
|
+
onMounted(() => {
|
|
171
|
+
nextTick(() => {
|
|
172
|
+
urlInputRef.value?.focus();
|
|
173
|
+
urlInputRef.value?.select();
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
document.addEventListener("keydown", handleDocumentKeydown);
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
onUnmounted(() => {
|
|
180
|
+
document.removeEventListener("keydown", handleDocumentKeydown);
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
// Watch for existingUrl changes to update the input
|
|
184
|
+
watch(() => props.existingUrl, (newUrl) => {
|
|
185
|
+
urlValue.value = newUrl || "";
|
|
186
|
+
});
|
|
187
|
+
</script>
|
|
188
|
+
|
|
189
|
+
<style lang="scss">
|
|
190
|
+
.dx-link-popover-overlay {
|
|
191
|
+
position: fixed;
|
|
192
|
+
inset: 0;
|
|
193
|
+
z-index: 1000;
|
|
194
|
+
background: rgba(0, 0, 0, 0.3);
|
|
195
|
+
backdrop-filter: blur(1px);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
.dx-link-popover {
|
|
199
|
+
position: fixed;
|
|
200
|
+
background: #2d2d2d;
|
|
201
|
+
border: 1px solid #404040;
|
|
202
|
+
border-radius: 0.5rem;
|
|
203
|
+
box-shadow: 0 25px 50px rgba(0, 0, 0, 0.5);
|
|
204
|
+
width: 320px;
|
|
205
|
+
overflow: hidden;
|
|
206
|
+
display: flex;
|
|
207
|
+
flex-direction: column;
|
|
208
|
+
|
|
209
|
+
.popover-header {
|
|
210
|
+
display: flex;
|
|
211
|
+
align-items: center;
|
|
212
|
+
justify-content: space-between;
|
|
213
|
+
padding: 0.875rem 1rem;
|
|
214
|
+
border-bottom: 1px solid #404040;
|
|
215
|
+
|
|
216
|
+
h3 {
|
|
217
|
+
margin: 0;
|
|
218
|
+
font-size: 0.9375rem;
|
|
219
|
+
font-weight: 600;
|
|
220
|
+
color: #f3f4f6;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
.close-btn {
|
|
224
|
+
display: flex;
|
|
225
|
+
align-items: center;
|
|
226
|
+
justify-content: center;
|
|
227
|
+
width: 1.5rem;
|
|
228
|
+
height: 1.5rem;
|
|
229
|
+
padding: 0;
|
|
230
|
+
background: transparent;
|
|
231
|
+
border: none;
|
|
232
|
+
border-radius: 0.25rem;
|
|
233
|
+
color: #9ca3af;
|
|
234
|
+
cursor: pointer;
|
|
235
|
+
transition: all 0.15s ease;
|
|
236
|
+
|
|
237
|
+
&:hover {
|
|
238
|
+
background: rgba(255, 255, 255, 0.1);
|
|
239
|
+
color: #f3f4f6;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
.popover-content {
|
|
245
|
+
padding: 1rem;
|
|
246
|
+
display: flex;
|
|
247
|
+
flex-direction: column;
|
|
248
|
+
gap: 0.875rem;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
.input-group {
|
|
252
|
+
display: flex;
|
|
253
|
+
flex-direction: column;
|
|
254
|
+
gap: 0.375rem;
|
|
255
|
+
|
|
256
|
+
label {
|
|
257
|
+
font-size: 0.75rem;
|
|
258
|
+
font-weight: 500;
|
|
259
|
+
text-transform: uppercase;
|
|
260
|
+
letter-spacing: 0.05em;
|
|
261
|
+
color: #9ca3af;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
input {
|
|
265
|
+
width: 100%;
|
|
266
|
+
padding: 0.5rem 0.75rem;
|
|
267
|
+
background: #1e1e1e;
|
|
268
|
+
border: 1px solid #404040;
|
|
269
|
+
border-radius: 0.375rem;
|
|
270
|
+
font-size: 0.875rem;
|
|
271
|
+
color: #f3f4f6;
|
|
272
|
+
outline: none;
|
|
273
|
+
transition: border-color 0.15s ease;
|
|
274
|
+
|
|
275
|
+
&::placeholder {
|
|
276
|
+
color: #6b7280;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
&:focus {
|
|
280
|
+
border-color: #60a5fa;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
.edit-hint {
|
|
286
|
+
font-size: 0.75rem;
|
|
287
|
+
color: #6b7280;
|
|
288
|
+
font-style: italic;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
.popover-footer {
|
|
292
|
+
display: flex;
|
|
293
|
+
justify-content: flex-end;
|
|
294
|
+
gap: 0.5rem;
|
|
295
|
+
padding: 0.75rem 1rem;
|
|
296
|
+
border-top: 1px solid #404040;
|
|
297
|
+
background: rgba(0, 0, 0, 0.2);
|
|
298
|
+
|
|
299
|
+
button {
|
|
300
|
+
padding: 0.5rem 1rem;
|
|
301
|
+
font-size: 0.875rem;
|
|
302
|
+
font-weight: 500;
|
|
303
|
+
border-radius: 0.375rem;
|
|
304
|
+
cursor: pointer;
|
|
305
|
+
transition: all 0.15s ease;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
.btn-cancel {
|
|
309
|
+
background: transparent;
|
|
310
|
+
border: 1px solid #404040;
|
|
311
|
+
color: #d4d4d4;
|
|
312
|
+
|
|
313
|
+
&:hover {
|
|
314
|
+
background: rgba(255, 255, 255, 0.05);
|
|
315
|
+
border-color: #525252;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
.btn-insert {
|
|
320
|
+
background: #3b82f6;
|
|
321
|
+
border: 1px solid #3b82f6;
|
|
322
|
+
color: #ffffff;
|
|
323
|
+
|
|
324
|
+
&:hover {
|
|
325
|
+
background: #2563eb;
|
|
326
|
+
border-color: #2563eb;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
</style>
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="dx-markdown-editor" :class="[{ 'is-readonly': readonly }, props.theme === 'light' ? 'theme-light' : '']">
|
|
3
|
+
<div class="dx-markdown-editor-body" @contextmenu="contextMenu.show">
|
|
4
|
+
<!-- Floating line type menu positioned next to current block -->
|
|
5
|
+
<div
|
|
6
|
+
ref="menuContainerRef"
|
|
7
|
+
class="dx-line-type-menu-container"
|
|
8
|
+
:style="lineTypeMenu.menuStyle.value"
|
|
9
|
+
>
|
|
10
|
+
<LineTypeMenu
|
|
11
|
+
:current-type="lineTypeMenu.currentLineType.value"
|
|
12
|
+
@change="lineTypeMenu.onLineTypeChange"
|
|
13
|
+
/>
|
|
14
|
+
</div>
|
|
15
|
+
|
|
16
|
+
<MarkdownEditorContent
|
|
17
|
+
ref="contentRef"
|
|
18
|
+
:html="editor.renderedHtml.value"
|
|
19
|
+
:readonly="readonly"
|
|
20
|
+
:placeholder="placeholder"
|
|
21
|
+
@input="editor.onInput"
|
|
22
|
+
@keydown="editor.onKeyDown"
|
|
23
|
+
@blur="editor.onBlur"
|
|
24
|
+
/>
|
|
25
|
+
</div>
|
|
26
|
+
|
|
27
|
+
<MarkdownEditorFooter
|
|
28
|
+
v-if="!hideFooter"
|
|
29
|
+
:char-count="editor.charCount.value"
|
|
30
|
+
@show-hotkeys="editor.showHotkeyHelp"
|
|
31
|
+
/>
|
|
32
|
+
|
|
33
|
+
<HotkeyHelpPopover
|
|
34
|
+
v-if="editor.isShowingHotkeyHelp.value"
|
|
35
|
+
:hotkeys="editor.hotkeyDefinitions.value"
|
|
36
|
+
@close="editor.hideHotkeyHelp"
|
|
37
|
+
/>
|
|
38
|
+
|
|
39
|
+
<LinkPopover
|
|
40
|
+
v-if="linkPopover.isVisible.value"
|
|
41
|
+
:position="linkPopover.position.value"
|
|
42
|
+
:existing-url="linkPopover.existingUrl.value"
|
|
43
|
+
:selected-text="linkPopover.selectedText.value"
|
|
44
|
+
@submit="linkPopover.submit"
|
|
45
|
+
@cancel="linkPopover.cancel"
|
|
46
|
+
/>
|
|
47
|
+
|
|
48
|
+
<TablePopover
|
|
49
|
+
v-if="tablePopover.isVisible.value"
|
|
50
|
+
:position="tablePopover.position.value"
|
|
51
|
+
@submit="tablePopover.submit"
|
|
52
|
+
@cancel="tablePopover.cancel"
|
|
53
|
+
/>
|
|
54
|
+
|
|
55
|
+
<ContextMenu
|
|
56
|
+
v-if="contextMenu.isVisible.value"
|
|
57
|
+
:position="contextMenu.position.value"
|
|
58
|
+
:items="contextMenu.items.value"
|
|
59
|
+
@close="contextMenu.hide"
|
|
60
|
+
/>
|
|
61
|
+
</div>
|
|
62
|
+
</template>
|
|
63
|
+
|
|
64
|
+
<script setup lang="ts">
|
|
65
|
+
import { computed, onMounted, onUnmounted, ref, watch } from "vue";
|
|
66
|
+
import { useContextMenu } from "../../../composables/markdown/features/useContextMenu";
|
|
67
|
+
import { useFocusTracking } from "../../../composables/markdown/features/useFocusTracking";
|
|
68
|
+
import { useLineTypeMenu } from "../../../composables/markdown/features/useLineTypeMenu";
|
|
69
|
+
import { useLinkPopover, useTablePopover } from "../../../composables/markdown/features/usePopoverManager";
|
|
70
|
+
import { useMarkdownEditor } from "../../../composables/markdown/useMarkdownEditor";
|
|
71
|
+
import ContextMenu from "./ContextMenu.vue";
|
|
72
|
+
import HotkeyHelpPopover from "./HotkeyHelpPopover.vue";
|
|
73
|
+
import LineTypeMenu from "./LineTypeMenu.vue";
|
|
74
|
+
import LinkPopover from "./LinkPopover.vue";
|
|
75
|
+
import MarkdownEditorContent from "./MarkdownEditorContent.vue";
|
|
76
|
+
import MarkdownEditorFooter from "./MarkdownEditorFooter.vue";
|
|
77
|
+
import TablePopover from "./TablePopover.vue";
|
|
78
|
+
|
|
79
|
+
export interface MarkdownEditorProps {
|
|
80
|
+
modelValue?: string;
|
|
81
|
+
placeholder?: string;
|
|
82
|
+
readonly?: boolean;
|
|
83
|
+
minHeight?: string;
|
|
84
|
+
maxHeight?: string;
|
|
85
|
+
theme?: "dark" | "light";
|
|
86
|
+
hideFooter?: boolean;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const props = withDefaults(defineProps<MarkdownEditorProps>(), {
|
|
90
|
+
modelValue: "",
|
|
91
|
+
placeholder: "Start typing...",
|
|
92
|
+
readonly: false,
|
|
93
|
+
minHeight: "100px",
|
|
94
|
+
maxHeight: "none",
|
|
95
|
+
theme: "dark",
|
|
96
|
+
hideFooter: false
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
const emit = defineEmits<{
|
|
100
|
+
"update:modelValue": [value: string];
|
|
101
|
+
}>();
|
|
102
|
+
|
|
103
|
+
// Reference to the content component
|
|
104
|
+
const contentRef = ref<InstanceType<typeof MarkdownEditorContent> | null>(null);
|
|
105
|
+
|
|
106
|
+
// Reference to the menu container for focus handling
|
|
107
|
+
const menuContainerRef = ref<HTMLElement | null>(null);
|
|
108
|
+
|
|
109
|
+
// Get the actual HTMLElement from the content component
|
|
110
|
+
const contentElementRef = computed(() => contentRef.value?.containerRef || null);
|
|
111
|
+
|
|
112
|
+
// Initialize popover managers
|
|
113
|
+
const linkPopover = useLinkPopover();
|
|
114
|
+
const tablePopover = useTablePopover();
|
|
115
|
+
|
|
116
|
+
// Initialize the markdown editor composable
|
|
117
|
+
const editor = useMarkdownEditor({
|
|
118
|
+
contentRef: contentElementRef,
|
|
119
|
+
initialValue: props.modelValue,
|
|
120
|
+
onEmitValue: (markdown: string) => {
|
|
121
|
+
emit("update:modelValue", markdown);
|
|
122
|
+
},
|
|
123
|
+
onShowLinkPopover: linkPopover.show,
|
|
124
|
+
onShowTablePopover: tablePopover.show
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
// Initialize focus tracking
|
|
128
|
+
const focusTracking = useFocusTracking({
|
|
129
|
+
contentRef: contentElementRef,
|
|
130
|
+
menuContainerRef,
|
|
131
|
+
onSelectionChange: () => lineTypeMenu.updatePositionAndState()
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
// Initialize line type menu
|
|
135
|
+
const lineTypeMenu = useLineTypeMenu({
|
|
136
|
+
contentRef: contentElementRef,
|
|
137
|
+
editor,
|
|
138
|
+
isEditorFocused: focusTracking.isEditorFocused
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
// Initialize context menu
|
|
142
|
+
const contextMenu = useContextMenu({
|
|
143
|
+
editor,
|
|
144
|
+
readonly: computed(() => props.readonly)
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
// Setup line type menu listeners on mount
|
|
148
|
+
onMounted(() => {
|
|
149
|
+
lineTypeMenu.setupListeners();
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
// Cleanup line type menu listeners on unmount
|
|
153
|
+
onUnmounted(() => {
|
|
154
|
+
lineTypeMenu.cleanupListeners();
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
// Watch for external value changes
|
|
158
|
+
watch(
|
|
159
|
+
() => props.modelValue,
|
|
160
|
+
(newValue) => {
|
|
161
|
+
// Skip if this change originated from the editor itself (internal update)
|
|
162
|
+
// This prevents cursor jumping when the watch triggers after typing
|
|
163
|
+
if (editor.isInternalUpdate.value) {
|
|
164
|
+
editor.isInternalUpdate.value = false;
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Only update if the value is different from current
|
|
169
|
+
if (newValue !== undefined) {
|
|
170
|
+
editor.setMarkdown(newValue);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
);
|
|
174
|
+
|
|
175
|
+
// NOTE: Content is already initialized in useMarkdownEditor with initialValue.
|
|
176
|
+
// The v-html binding renders it, and the MutationObserver mounts CodeViewers.
|
|
177
|
+
// Calling setMarkdown again here would replace the DOM and cause race conditions
|
|
178
|
+
// with CodeViewer mounting. Only call setMarkdown for external value changes.
|
|
179
|
+
|
|
180
|
+
// Expose the editor for parent components that may need access
|
|
181
|
+
defineExpose({
|
|
182
|
+
editor,
|
|
183
|
+
setMarkdown: editor.setMarkdown
|
|
184
|
+
});
|
|
185
|
+
</script>
|
|
186
|
+
|
|
187
|
+
<style lang="scss">
|
|
188
|
+
.dx-markdown-editor {
|
|
189
|
+
display: flex;
|
|
190
|
+
flex-direction: column;
|
|
191
|
+
width: 100%;
|
|
192
|
+
border-radius: 0.375rem;
|
|
193
|
+
overflow: hidden;
|
|
194
|
+
|
|
195
|
+
&.is-readonly {
|
|
196
|
+
.dx-markdown-editor-content {
|
|
197
|
+
cursor: default;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
.dx-line-type-menu-container {
|
|
201
|
+
display: none;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Body container with floating menu and content side by side
|
|
206
|
+
.dx-markdown-editor-body {
|
|
207
|
+
display: flex;
|
|
208
|
+
position: relative;
|
|
209
|
+
flex: 1;
|
|
210
|
+
overflow: visible;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Floating line type menu container - positioned outside editor bounds
|
|
214
|
+
.dx-line-type-menu-container {
|
|
215
|
+
position: absolute;
|
|
216
|
+
left: -1.75rem;
|
|
217
|
+
z-index: 10;
|
|
218
|
+
width: 1.75rem;
|
|
219
|
+
display: flex;
|
|
220
|
+
align-items: flex-start;
|
|
221
|
+
justify-content: center;
|
|
222
|
+
padding-top: 0.25rem;
|
|
223
|
+
transition: top 0.1s ease-out, opacity 0.15s ease;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// Apply min/max height to content area (no left margin needed - menu is outside)
|
|
227
|
+
.dx-markdown-editor-content {
|
|
228
|
+
flex: 1;
|
|
229
|
+
min-height: v-bind(minHeight);
|
|
230
|
+
max-height: v-bind(maxHeight);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
</style>
|