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.
Files changed (81) hide show
  1. package/.claude/settings.local.json +8 -0
  2. package/dist/danx.es.js +16119 -10641
  3. package/dist/danx.es.js.map +1 -1
  4. package/dist/danx.umd.js +202 -123
  5. package/dist/danx.umd.js.map +1 -1
  6. package/dist/style.css +1 -1
  7. package/package.json +8 -1
  8. package/src/components/Utility/Buttons/ActionButton.vue +15 -5
  9. package/src/components/Utility/Code/CodeViewer.vue +41 -16
  10. package/src/components/Utility/Code/CodeViewerCollapsed.vue +2 -0
  11. package/src/components/Utility/Code/CodeViewerFooter.vue +3 -1
  12. package/src/components/Utility/Code/LanguageBadge.vue +278 -5
  13. package/src/components/Utility/Code/MarkdownContent.vue +31 -163
  14. package/src/components/Utility/Code/index.ts +3 -0
  15. package/src/components/Utility/Markdown/ContextMenu.vue +314 -0
  16. package/src/components/Utility/Markdown/HotkeyHelpPopover.vue +259 -0
  17. package/src/components/Utility/Markdown/LineTypeMenu.vue +226 -0
  18. package/src/components/Utility/Markdown/LinkPopover.vue +331 -0
  19. package/src/components/Utility/Markdown/MarkdownEditor.vue +233 -0
  20. package/src/components/Utility/Markdown/MarkdownEditorContent.vue +296 -0
  21. package/src/components/Utility/Markdown/MarkdownEditorFooter.vue +50 -0
  22. package/src/components/Utility/Markdown/TablePopover.vue +420 -0
  23. package/src/components/Utility/Markdown/index.ts +11 -0
  24. package/src/components/Utility/Markdown/types.ts +27 -0
  25. package/src/components/Utility/Widgets/LabelPillWidget.vue +20 -0
  26. package/src/components/Utility/index.ts +1 -0
  27. package/src/composables/index.ts +1 -0
  28. package/src/composables/markdown/features/useBlockquotes.spec.ts +428 -0
  29. package/src/composables/markdown/features/useBlockquotes.ts +248 -0
  30. package/src/composables/markdown/features/useCodeBlockManager.ts +369 -0
  31. package/src/composables/markdown/features/useCodeBlocks.spec.ts +805 -0
  32. package/src/composables/markdown/features/useCodeBlocks.ts +774 -0
  33. package/src/composables/markdown/features/useContextMenu.ts +444 -0
  34. package/src/composables/markdown/features/useFocusTracking.ts +116 -0
  35. package/src/composables/markdown/features/useHeadings.spec.ts +834 -0
  36. package/src/composables/markdown/features/useHeadings.ts +290 -0
  37. package/src/composables/markdown/features/useInlineFormatting.spec.ts +705 -0
  38. package/src/composables/markdown/features/useInlineFormatting.ts +402 -0
  39. package/src/composables/markdown/features/useLineTypeMenu.ts +285 -0
  40. package/src/composables/markdown/features/useLinks.spec.ts +388 -0
  41. package/src/composables/markdown/features/useLinks.ts +374 -0
  42. package/src/composables/markdown/features/useLists.spec.ts +834 -0
  43. package/src/composables/markdown/features/useLists.ts +747 -0
  44. package/src/composables/markdown/features/usePopoverManager.ts +181 -0
  45. package/src/composables/markdown/features/useTables.spec.ts +1601 -0
  46. package/src/composables/markdown/features/useTables.ts +1107 -0
  47. package/src/composables/markdown/index.ts +16 -0
  48. package/src/composables/markdown/useMarkdownEditor.spec.ts +332 -0
  49. package/src/composables/markdown/useMarkdownEditor.ts +1077 -0
  50. package/src/composables/markdown/useMarkdownHotkeys.spec.ts +791 -0
  51. package/src/composables/markdown/useMarkdownHotkeys.ts +266 -0
  52. package/src/composables/markdown/useMarkdownSelection.ts +219 -0
  53. package/src/composables/markdown/useMarkdownSync.ts +549 -0
  54. package/src/composables/useCodeFormat.ts +17 -10
  55. package/src/composables/useCodeViewerEditor.spec.ts +655 -0
  56. package/src/composables/useCodeViewerEditor.ts +174 -20
  57. package/src/helpers/formats/highlightCSS.ts +236 -0
  58. package/src/helpers/formats/highlightHTML.ts +483 -0
  59. package/src/helpers/formats/highlightJavaScript.ts +346 -0
  60. package/src/helpers/formats/highlightSyntax.ts +15 -4
  61. package/src/helpers/formats/index.ts +3 -0
  62. package/src/helpers/formats/markdown/htmlToMarkdown/convertHeadings.ts +41 -0
  63. package/src/helpers/formats/markdown/htmlToMarkdown/index.spec.ts +489 -0
  64. package/src/helpers/formats/markdown/htmlToMarkdown/index.ts +425 -0
  65. package/src/helpers/formats/markdown/index.ts +7 -0
  66. package/src/helpers/formats/markdown/linePatterns.spec.ts +498 -0
  67. package/src/helpers/formats/markdown/linePatterns.ts +172 -0
  68. package/src/styles/danx.scss +3 -3
  69. package/src/styles/index.scss +5 -5
  70. package/src/styles/themes/danx/code.scss +257 -1
  71. package/src/styles/themes/danx/index.scss +10 -10
  72. package/src/styles/themes/danx/markdown.scss +59 -0
  73. package/src/test/helpers/editorTestUtils.spec.ts +296 -0
  74. package/src/test/helpers/editorTestUtils.ts +253 -0
  75. package/src/test/helpers/index.ts +1 -0
  76. package/src/test/highlighters.test.ts +153 -0
  77. package/src/test/setup.test.ts +12 -0
  78. package/src/test/setup.ts +12 -0
  79. package/src/types/widgets.d.ts +2 -2
  80. package/vite.config.js +5 -1
  81. package/vitest.config.ts +19 -0
@@ -0,0 +1,374 @@
1
+ import { Ref } from "vue";
2
+
3
+ /**
4
+ * Position for link popover
5
+ */
6
+ export interface LinkPopoverPosition {
7
+ x: number;
8
+ y: number;
9
+ }
10
+
11
+ /**
12
+ * Options passed to the onShowLinkPopover callback
13
+ */
14
+ export interface ShowLinkPopoverOptions {
15
+ /** Position in viewport where popover should appear */
16
+ position: LinkPopoverPosition;
17
+ /** If editing an existing link, the current URL */
18
+ existingUrl?: string;
19
+ /** If text is selected, the selected text (for label preview) */
20
+ selectedText?: string;
21
+ /** Callback to complete the link insertion/update */
22
+ onSubmit: (url: string, label?: string) => void;
23
+ /** Callback to cancel the operation */
24
+ onCancel: () => void;
25
+ }
26
+
27
+ /**
28
+ * Options for useLinks composable
29
+ */
30
+ export interface UseLinksOptions {
31
+ contentRef: Ref<HTMLElement | null>;
32
+ onContentChange: () => void;
33
+ /** Callback to show the link popover UI */
34
+ onShowLinkPopover?: (options: ShowLinkPopoverOptions) => void;
35
+ }
36
+
37
+ /**
38
+ * Return type for useLinks composable
39
+ */
40
+ export interface UseLinksReturn {
41
+ /** Insert or edit a link at the current selection/cursor */
42
+ insertLink: () => void;
43
+ /** Check if cursor is inside a link */
44
+ isInLink: () => boolean;
45
+ }
46
+
47
+ /**
48
+ * Find the anchor element ancestor if one exists
49
+ */
50
+ function findLinkAncestor(node: Node | null, contentRef: HTMLElement): HTMLAnchorElement | null {
51
+ if (!node) return null;
52
+
53
+ let current: Node | null = node;
54
+ while (current && current !== contentRef) {
55
+ if (current.nodeType === Node.ELEMENT_NODE && (current as Element).tagName === "A") {
56
+ return current as HTMLAnchorElement;
57
+ }
58
+ current = current.parentNode;
59
+ }
60
+
61
+ return null;
62
+ }
63
+
64
+ /**
65
+ * Dispatch an input event to trigger content sync
66
+ */
67
+ function dispatchInputEvent(element: HTMLElement): void {
68
+ element.dispatchEvent(new InputEvent("input", { bubbles: true }));
69
+ }
70
+
71
+ /**
72
+ * Get the cursor position in viewport coordinates
73
+ */
74
+ function getCursorPosition(): LinkPopoverPosition {
75
+ const selection = window.getSelection();
76
+ if (!selection || !selection.rangeCount) {
77
+ return { x: window.innerWidth / 2, y: window.innerHeight / 2 };
78
+ }
79
+
80
+ const range = selection.getRangeAt(0);
81
+ const rect = range.getBoundingClientRect();
82
+
83
+ // If rect has no dimensions (collapsed cursor), use the start position
84
+ if (rect.width === 0 && rect.height === 0) {
85
+ return {
86
+ x: rect.left || window.innerWidth / 2,
87
+ y: rect.bottom || window.innerHeight / 2
88
+ };
89
+ }
90
+
91
+ // Center horizontally on the selection, position below
92
+ return {
93
+ x: rect.left + (rect.width / 2),
94
+ y: rect.bottom
95
+ };
96
+ }
97
+
98
+ /**
99
+ * Composable for link operations in markdown editor
100
+ */
101
+ export function useLinks(options: UseLinksOptions): UseLinksReturn {
102
+ const { contentRef, onContentChange, onShowLinkPopover } = options;
103
+
104
+ // Store the selection range so we can restore it after popover interaction
105
+ let savedRange: Range | null = null;
106
+
107
+ /**
108
+ * Save the current selection for later restoration
109
+ */
110
+ function saveSelection(): void {
111
+ const selection = window.getSelection();
112
+ if (selection && selection.rangeCount > 0) {
113
+ savedRange = selection.getRangeAt(0).cloneRange();
114
+ }
115
+ }
116
+
117
+ /**
118
+ * Restore the previously saved selection
119
+ */
120
+ function restoreSelection(): void {
121
+ if (savedRange) {
122
+ const selection = window.getSelection();
123
+ selection?.removeAllRanges();
124
+ selection?.addRange(savedRange);
125
+ }
126
+ }
127
+
128
+ /**
129
+ * Check if the cursor is currently inside a link
130
+ */
131
+ function isInLink(): boolean {
132
+ if (!contentRef.value) return false;
133
+
134
+ const selection = window.getSelection();
135
+ if (!selection || !selection.rangeCount) return false;
136
+
137
+ const range = selection.getRangeAt(0);
138
+ return findLinkAncestor(range.startContainer, contentRef.value) !== null;
139
+ }
140
+
141
+ /**
142
+ * Insert a new link or edit an existing one
143
+ *
144
+ * Behavior:
145
+ * - If cursor is inside an existing link: show popover to edit URL (prefilled with current href)
146
+ * - If text is selected: show popover for URL, wrap selection in <a href="url">selection</a>
147
+ * - If no selection: show popover for URL and label, insert <a href="url">label</a>
148
+ * - If user cancels, do nothing
149
+ */
150
+ function insertLink(): void {
151
+ if (!contentRef.value) return;
152
+
153
+ const selection = window.getSelection();
154
+ if (!selection || !selection.rangeCount) return;
155
+
156
+ const range = selection.getRangeAt(0);
157
+
158
+ // Check if selection is within our content area
159
+ if (!contentRef.value.contains(range.startContainer)) return;
160
+
161
+ // Save the selection before showing popover
162
+ saveSelection();
163
+
164
+ // Check if cursor is inside an existing link
165
+ const existingLink = findLinkAncestor(range.startContainer, contentRef.value);
166
+
167
+ if (existingLink) {
168
+ showEditLinkPopover(existingLink);
169
+ } else if (!range.collapsed) {
170
+ showWrapSelectionPopover(range);
171
+ } else {
172
+ showNewLinkPopover(range);
173
+ }
174
+ }
175
+
176
+ /**
177
+ * Show popover to edit an existing link
178
+ */
179
+ function showEditLinkPopover(link: HTMLAnchorElement): void {
180
+ const currentHref = link.getAttribute("href") || "";
181
+ const position = getCursorPosition();
182
+
183
+ if (onShowLinkPopover) {
184
+ onShowLinkPopover({
185
+ position,
186
+ existingUrl: currentHref,
187
+ selectedText: link.textContent || undefined,
188
+ onSubmit: (url: string) => {
189
+ restoreSelection();
190
+ completeEditLink(link, url);
191
+ },
192
+ onCancel: () => {
193
+ restoreSelection();
194
+ contentRef.value?.focus();
195
+ }
196
+ });
197
+ } else {
198
+ // Fallback to window.prompt if no popover callback provided
199
+ const newUrl = window.prompt("Edit link URL:", currentHref);
200
+ if (newUrl === null) return;
201
+ completeEditLink(link, newUrl);
202
+ }
203
+ }
204
+
205
+ /**
206
+ * Complete editing an existing link
207
+ */
208
+ function completeEditLink(link: HTMLAnchorElement, url: string): void {
209
+ if (url.trim() === "") {
210
+ unwrapLink(link);
211
+ } else {
212
+ link.setAttribute("href", url.trim());
213
+ }
214
+
215
+ dispatchInputEvent(contentRef.value!);
216
+ onContentChange();
217
+ contentRef.value?.focus();
218
+ }
219
+
220
+ /**
221
+ * Unwrap a link, keeping its text content
222
+ */
223
+ function unwrapLink(link: HTMLAnchorElement): void {
224
+ const parent = link.parentNode;
225
+ if (!parent) return;
226
+
227
+ // Move all children out of the link
228
+ while (link.firstChild) {
229
+ parent.insertBefore(link.firstChild, link);
230
+ }
231
+
232
+ // Remove the empty link element
233
+ parent.removeChild(link);
234
+ }
235
+
236
+ /**
237
+ * Show popover to wrap selected text in a link
238
+ */
239
+ function showWrapSelectionPopover(range: Range): void {
240
+ const selectedText = range.toString();
241
+ const position = getCursorPosition();
242
+
243
+ if (onShowLinkPopover) {
244
+ onShowLinkPopover({
245
+ position,
246
+ selectedText,
247
+ onSubmit: (url: string) => {
248
+ restoreSelection();
249
+ completeWrapSelection(url);
250
+ },
251
+ onCancel: () => {
252
+ restoreSelection();
253
+ contentRef.value?.focus();
254
+ }
255
+ });
256
+ } else {
257
+ // Fallback to window.prompt if no popover callback provided
258
+ const url = window.prompt("Enter link URL:");
259
+ if (!url || url.trim() === "") return;
260
+ completeWrapSelection(url);
261
+ }
262
+ }
263
+
264
+ /**
265
+ * Complete wrapping selection in a link
266
+ */
267
+ function completeWrapSelection(url: string): void {
268
+ if (!url || url.trim() === "") {
269
+ contentRef.value?.focus();
270
+ return;
271
+ }
272
+
273
+ const selection = window.getSelection();
274
+ if (!selection || !selection.rangeCount) {
275
+ contentRef.value?.focus();
276
+ return;
277
+ }
278
+
279
+ const range = selection.getRangeAt(0);
280
+
281
+ // Create the link element
282
+ const link = document.createElement("a");
283
+ link.setAttribute("href", url.trim());
284
+ link.setAttribute("target", "_blank");
285
+ link.setAttribute("rel", "noopener noreferrer");
286
+
287
+ // Extract and wrap the selection
288
+ const contents = range.extractContents();
289
+ link.appendChild(contents);
290
+ range.insertNode(link);
291
+
292
+ // Select the link contents
293
+ const newRange = document.createRange();
294
+ newRange.selectNodeContents(link);
295
+ selection.removeAllRanges();
296
+ selection.addRange(newRange);
297
+
298
+ dispatchInputEvent(contentRef.value!);
299
+ onContentChange();
300
+ contentRef.value?.focus();
301
+ }
302
+
303
+ /**
304
+ * Show popover to insert a new link (URL and label)
305
+ */
306
+ function showNewLinkPopover(range: Range): void {
307
+ const position = getCursorPosition();
308
+
309
+ if (onShowLinkPopover) {
310
+ onShowLinkPopover({
311
+ position,
312
+ onSubmit: (url: string, label?: string) => {
313
+ restoreSelection();
314
+ completeNewLink(url, label);
315
+ },
316
+ onCancel: () => {
317
+ restoreSelection();
318
+ contentRef.value?.focus();
319
+ }
320
+ });
321
+ } else {
322
+ // Fallback to window.prompt if no popover callback provided
323
+ const url = window.prompt("Enter link URL:");
324
+ if (!url || url.trim() === "") return;
325
+ completeNewLink(url);
326
+ }
327
+ }
328
+
329
+ /**
330
+ * Complete inserting a new link
331
+ */
332
+ function completeNewLink(url: string, label?: string): void {
333
+ if (!url || url.trim() === "") {
334
+ contentRef.value?.focus();
335
+ return;
336
+ }
337
+
338
+ const trimmedUrl = url.trim();
339
+ const linkText = label?.trim() || trimmedUrl;
340
+
341
+ const selection = window.getSelection();
342
+ if (!selection || !selection.rangeCount) {
343
+ contentRef.value?.focus();
344
+ return;
345
+ }
346
+
347
+ const range = selection.getRangeAt(0);
348
+
349
+ // Create the link element
350
+ const link = document.createElement("a");
351
+ link.setAttribute("href", trimmedUrl);
352
+ link.setAttribute("target", "_blank");
353
+ link.setAttribute("rel", "noopener noreferrer");
354
+ link.textContent = linkText;
355
+
356
+ // Insert at cursor position
357
+ range.insertNode(link);
358
+
359
+ // Select the link contents
360
+ const newRange = document.createRange();
361
+ newRange.selectNodeContents(link);
362
+ selection.removeAllRanges();
363
+ selection.addRange(newRange);
364
+
365
+ dispatchInputEvent(contentRef.value!);
366
+ onContentChange();
367
+ contentRef.value?.focus();
368
+ }
369
+
370
+ return {
371
+ insertLink,
372
+ isInLink
373
+ };
374
+ }