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.
Files changed (63) hide show
  1. package/dist/danx.es.js +12797 -8181
  2. package/dist/danx.es.js.map +1 -1
  3. package/dist/danx.umd.js +192 -120
  4. package/dist/danx.umd.js.map +1 -1
  5. package/dist/style.css +1 -1
  6. package/package.json +8 -1
  7. package/src/components/Utility/Code/CodeViewer.vue +31 -14
  8. package/src/components/Utility/Code/CodeViewerCollapsed.vue +2 -0
  9. package/src/components/Utility/Code/CodeViewerFooter.vue +1 -1
  10. package/src/components/Utility/Code/LanguageBadge.vue +278 -5
  11. package/src/components/Utility/Code/index.ts +3 -0
  12. package/src/components/Utility/Markdown/ContextMenu.vue +314 -0
  13. package/src/components/Utility/Markdown/HotkeyHelpPopover.vue +259 -0
  14. package/src/components/Utility/Markdown/LineTypeMenu.vue +226 -0
  15. package/src/components/Utility/Markdown/LinkPopover.vue +331 -0
  16. package/src/components/Utility/Markdown/MarkdownEditor.vue +228 -0
  17. package/src/components/Utility/Markdown/MarkdownEditorContent.vue +235 -0
  18. package/src/components/Utility/Markdown/MarkdownEditorFooter.vue +50 -0
  19. package/src/components/Utility/Markdown/TablePopover.vue +420 -0
  20. package/src/components/Utility/Markdown/index.ts +11 -0
  21. package/src/components/Utility/Markdown/types.ts +27 -0
  22. package/src/components/Utility/index.ts +1 -0
  23. package/src/composables/index.ts +1 -0
  24. package/src/composables/markdown/features/useBlockquotes.spec.ts +428 -0
  25. package/src/composables/markdown/features/useBlockquotes.ts +248 -0
  26. package/src/composables/markdown/features/useCodeBlockManager.ts +369 -0
  27. package/src/composables/markdown/features/useCodeBlocks.spec.ts +779 -0
  28. package/src/composables/markdown/features/useCodeBlocks.ts +774 -0
  29. package/src/composables/markdown/features/useContextMenu.ts +444 -0
  30. package/src/composables/markdown/features/useFocusTracking.ts +116 -0
  31. package/src/composables/markdown/features/useHeadings.spec.ts +834 -0
  32. package/src/composables/markdown/features/useHeadings.ts +290 -0
  33. package/src/composables/markdown/features/useInlineFormatting.spec.ts +705 -0
  34. package/src/composables/markdown/features/useInlineFormatting.ts +402 -0
  35. package/src/composables/markdown/features/useLineTypeMenu.ts +285 -0
  36. package/src/composables/markdown/features/useLinks.spec.ts +369 -0
  37. package/src/composables/markdown/features/useLinks.ts +374 -0
  38. package/src/composables/markdown/features/useLists.spec.ts +834 -0
  39. package/src/composables/markdown/features/useLists.ts +747 -0
  40. package/src/composables/markdown/features/usePopoverManager.ts +181 -0
  41. package/src/composables/markdown/features/useTables.spec.ts +1601 -0
  42. package/src/composables/markdown/features/useTables.ts +1107 -0
  43. package/src/composables/markdown/index.ts +16 -0
  44. package/src/composables/markdown/useMarkdownEditor.spec.ts +332 -0
  45. package/src/composables/markdown/useMarkdownEditor.ts +1068 -0
  46. package/src/composables/markdown/useMarkdownHotkeys.spec.ts +791 -0
  47. package/src/composables/markdown/useMarkdownHotkeys.ts +266 -0
  48. package/src/composables/markdown/useMarkdownSelection.ts +219 -0
  49. package/src/composables/markdown/useMarkdownSync.ts +549 -0
  50. package/src/composables/useCodeViewerEditor.spec.ts +655 -0
  51. package/src/composables/useCodeViewerEditor.ts +174 -20
  52. package/src/helpers/formats/markdown/htmlToMarkdown/convertHeadings.ts +41 -0
  53. package/src/helpers/formats/markdown/htmlToMarkdown/index.spec.ts +489 -0
  54. package/src/helpers/formats/markdown/htmlToMarkdown/index.ts +412 -0
  55. package/src/helpers/formats/markdown/index.ts +7 -0
  56. package/src/helpers/formats/markdown/linePatterns.spec.ts +495 -0
  57. package/src/helpers/formats/markdown/linePatterns.ts +172 -0
  58. package/src/test/helpers/editorTestUtils.spec.ts +296 -0
  59. package/src/test/helpers/editorTestUtils.ts +253 -0
  60. package/src/test/helpers/index.ts +1 -0
  61. package/src/test/setup.test.ts +12 -0
  62. package/src/test/setup.ts +12 -0
  63. package/vitest.config.ts +19 -0
@@ -0,0 +1,226 @@
1
+ <template>
2
+ <div ref="menuRef" class="dx-line-type-menu" :class="{ 'is-open': isOpen }">
3
+ <button
4
+ class="line-type-trigger"
5
+ :title="currentTypeLabel"
6
+ type="button"
7
+ @mousedown.prevent="toggleMenu"
8
+ >
9
+ <span class="type-icon">{{ typeIcon }}</span>
10
+ </button>
11
+
12
+ <div v-if="isOpen" ref="dropdownRef" class="line-type-dropdown" :class="{ 'open-upward': openUpward }">
13
+ <button
14
+ v-for="option in LINE_TYPE_OPTIONS"
15
+ :key="option.value"
16
+ class="line-type-option"
17
+ :class="{ active: option.value === currentType }"
18
+ type="button"
19
+ @mousedown.prevent="selectType(option.value)"
20
+ >
21
+ <span class="option-icon">{{ option.icon }}</span>
22
+ <span class="option-label">{{ option.label }}</span>
23
+ <span class="option-shortcut">{{ option.shortcut }}</span>
24
+ </button>
25
+ </div>
26
+ </div>
27
+ </template>
28
+
29
+ <script setup lang="ts">
30
+ import { computed, nextTick, onUnmounted, ref, watch } from "vue";
31
+ import type { LineType, LineTypeOption } from "./types";
32
+
33
+ export interface LineTypeMenuProps {
34
+ currentType: LineType;
35
+ }
36
+
37
+ const LINE_TYPE_OPTIONS: LineTypeOption[] = [
38
+ { value: "paragraph", label: "Paragraph", icon: "\u00B6", shortcut: "Ctrl+0" },
39
+ { value: "h1", label: "Heading 1", icon: "H1", shortcut: "Ctrl+1" },
40
+ { value: "h2", label: "Heading 2", icon: "H2", shortcut: "Ctrl+2" },
41
+ { value: "h3", label: "Heading 3", icon: "H3", shortcut: "Ctrl+3" },
42
+ { value: "h4", label: "Heading 4", icon: "H4", shortcut: "Ctrl+4" },
43
+ { value: "h5", label: "Heading 5", icon: "H5", shortcut: "Ctrl+5" },
44
+ { value: "h6", label: "Heading 6", icon: "H6", shortcut: "Ctrl+6" },
45
+ { value: "ul", label: "Bullet List", icon: "\u2022", shortcut: "Ctrl+Shift+[" },
46
+ { value: "ol", label: "Numbered List", icon: "1.", shortcut: "Ctrl+Shift+]" },
47
+ { value: "code", label: "Code Block", icon: "</>", shortcut: "Ctrl+Shift+K" },
48
+ { value: "blockquote", label: "Blockquote", icon: ">", shortcut: "Ctrl+Shift+Q" }
49
+ ];
50
+
51
+ const props = defineProps<LineTypeMenuProps>();
52
+
53
+ const emit = defineEmits<{
54
+ change: [type: LineType];
55
+ }>();
56
+
57
+ const isOpen = ref(false);
58
+ const menuRef = ref<HTMLElement | null>(null);
59
+ const dropdownRef = ref<HTMLElement | null>(null);
60
+ const openUpward = ref(false);
61
+
62
+ const currentOption = computed(() => {
63
+ return LINE_TYPE_OPTIONS.find(o => o.value === props.currentType) || LINE_TYPE_OPTIONS[0];
64
+ });
65
+
66
+ const currentTypeLabel = computed(() => currentOption.value.label);
67
+ const typeIcon = computed(() => currentOption.value.icon);
68
+
69
+ function toggleMenu() {
70
+ isOpen.value = !isOpen.value;
71
+ }
72
+
73
+ function selectType(type: LineType) {
74
+ emit("change", type);
75
+ isOpen.value = false;
76
+ }
77
+
78
+ /**
79
+ * Check if dropdown would overflow the editor container and adjust positioning
80
+ */
81
+ function checkDropdownPosition() {
82
+ if (!dropdownRef.value || !menuRef.value) return;
83
+
84
+ // Find the editor container (walk up to find .dx-markdown-editor or similar scrollable container)
85
+ const editor = menuRef.value.closest(".dx-markdown-editor") || menuRef.value.closest("[class*='editor']");
86
+ if (!editor) {
87
+ openUpward.value = false;
88
+ return;
89
+ }
90
+
91
+ const editorRect = editor.getBoundingClientRect();
92
+ const dropdownRect = dropdownRef.value.getBoundingClientRect();
93
+
94
+ // Check if dropdown extends below editor
95
+ if (dropdownRect.bottom > editorRect.bottom) {
96
+ openUpward.value = true;
97
+ } else {
98
+ openUpward.value = false;
99
+ }
100
+ }
101
+
102
+ // Close menu on click outside
103
+ function handleClickOutside(event: MouseEvent) {
104
+ const target = event.target as Node;
105
+ if (menuRef.value && !menuRef.value.contains(target)) {
106
+ isOpen.value = false;
107
+ }
108
+ }
109
+
110
+ // Close menu on Escape key
111
+ function handleKeyDown(event: KeyboardEvent) {
112
+ if (event.key === "Escape") {
113
+ isOpen.value = false;
114
+ }
115
+ }
116
+
117
+ // Add/remove event listeners when menu opens/closes
118
+ watch(isOpen, async (open) => {
119
+ if (open) {
120
+ document.addEventListener("mousedown", handleClickOutside);
121
+ document.addEventListener("keydown", handleKeyDown);
122
+ // Wait for dropdown to render, then check position
123
+ await nextTick();
124
+ checkDropdownPosition();
125
+ } else {
126
+ document.removeEventListener("mousedown", handleClickOutside);
127
+ document.removeEventListener("keydown", handleKeyDown);
128
+ // Reset position when closed
129
+ openUpward.value = false;
130
+ }
131
+ });
132
+
133
+ // Cleanup on unmount
134
+ onUnmounted(() => {
135
+ document.removeEventListener("mousedown", handleClickOutside);
136
+ document.removeEventListener("keydown", handleKeyDown);
137
+ });
138
+ </script>
139
+
140
+ <style lang="scss">
141
+ .dx-line-type-menu {
142
+ position: relative;
143
+ display: inline-block;
144
+
145
+ .line-type-trigger {
146
+ display: flex;
147
+ align-items: center;
148
+ justify-content: center;
149
+ width: 1.25rem;
150
+ height: 1.25rem;
151
+ background: rgba(255, 255, 255, 0.05);
152
+ border: none;
153
+ border-radius: 0.2rem;
154
+ color: #6b7280;
155
+ font-size: 0.625rem;
156
+ font-weight: 600;
157
+ cursor: pointer;
158
+ transition: all 0.15s ease;
159
+
160
+ &:hover {
161
+ background: rgba(255, 255, 255, 0.15);
162
+ color: #9ca3af;
163
+ }
164
+ }
165
+
166
+ .line-type-dropdown {
167
+ position: absolute;
168
+ top: 0;
169
+ left: 100%;
170
+ z-index: 100;
171
+ min-width: 240px;
172
+ margin-left: 0.25rem;
173
+ background: #2d2d2d;
174
+ border: 1px solid #404040;
175
+ border-radius: 0.375rem;
176
+ box-shadow: 0 10px 25px rgba(0, 0, 0, 0.4);
177
+ overflow: hidden;
178
+
179
+ // When dropdown would overflow editor bottom, open upward instead
180
+ &.open-upward {
181
+ top: auto;
182
+ bottom: 0;
183
+ }
184
+ }
185
+
186
+ .line-type-option {
187
+ display: flex;
188
+ align-items: center;
189
+ width: 100%;
190
+ padding: 0.5rem 0.75rem;
191
+ background: transparent;
192
+ border: none;
193
+ color: #d4d4d4;
194
+ font-size: 0.875rem;
195
+ text-align: left;
196
+ cursor: pointer;
197
+ transition: background-color 0.15s ease;
198
+
199
+ &:hover {
200
+ background: rgba(255, 255, 255, 0.1);
201
+ }
202
+
203
+ &.active {
204
+ background: rgba(56, 139, 253, 0.2);
205
+ color: #58a6ff;
206
+ }
207
+
208
+ .option-icon {
209
+ width: 1.5rem;
210
+ font-weight: 700;
211
+ font-size: 0.75rem;
212
+ color: #9ca3af;
213
+ }
214
+
215
+ .option-label {
216
+ flex: 1;
217
+ }
218
+
219
+ .option-shortcut {
220
+ font-size: 0.75rem;
221
+ color: #6b7280;
222
+ font-family: 'Consolas', 'Monaco', monospace;
223
+ }
224
+ }
225
+ }
226
+ </style>
@@ -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>