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,369 @@
1
+ import { Quasar } from "quasar";
2
+ import { App, computed, createApp, h, nextTick, onUnmounted, ref, Ref, watch } from "vue";
3
+ import CodeViewer from "../../../components/Utility/Code/CodeViewer.vue";
4
+ import { CodeBlockState } from "./useCodeBlocks";
5
+
6
+ /**
7
+ * Options for useCodeBlockManager composable
8
+ */
9
+ export interface UseCodeBlockManagerOptions {
10
+ /** Reference to the contenteditable container */
11
+ contentRef: Ref<HTMLElement | null>;
12
+ /** Reactive map of code block states */
13
+ codeBlocks: Map<string, CodeBlockState>;
14
+ /** Callback to update code block content */
15
+ updateCodeBlockContent: (id: string, content: string) => void;
16
+ /** Callback to update code block language */
17
+ updateCodeBlockLanguage: (id: string, language: string) => void;
18
+ /** Callback when a code block requests to exit (e.g., double-Enter at end) */
19
+ onCodeBlockExit?: (id: string) => void;
20
+ /** Callback when a code block requests to be deleted (e.g., Backspace/Delete on empty) */
21
+ onCodeBlockDelete?: (id: string) => void;
22
+ /** Callback when a code block is mounted */
23
+ onCodeBlockMounted?: (id: string, wrapper: HTMLElement) => void;
24
+ }
25
+
26
+ /**
27
+ * Return type for useCodeBlockManager composable
28
+ */
29
+ export interface UseCodeBlockManagerReturn {
30
+ /** Mount CodeViewer instances for all unmounted code blocks */
31
+ mountCodeViewers: () => void;
32
+ /** Unmount a specific CodeViewer instance */
33
+ unmountCodeViewer: (id: string) => void;
34
+ /** Unmount all CodeViewer instances */
35
+ unmountAllCodeViewers: () => void;
36
+ /** Get mounted instance count */
37
+ getMountedCount: () => number;
38
+ }
39
+
40
+ /**
41
+ * Mounted CodeViewer instance tracking
42
+ */
43
+ interface MountedInstance {
44
+ app: App;
45
+ mountPoint: HTMLElement;
46
+ }
47
+
48
+ /**
49
+ * Map language aliases to CodeViewer format
50
+ */
51
+ function mapLanguageToFormat(language: string): string {
52
+ const formatMap: Record<string, string> = {
53
+ js: "javascript",
54
+ ts: "typescript",
55
+ py: "python",
56
+ rb: "ruby",
57
+ yml: "yaml",
58
+ md: "markdown",
59
+ sh: "bash",
60
+ shell: "bash"
61
+ };
62
+
63
+ return formatMap[language] || language || "text";
64
+ }
65
+
66
+ /**
67
+ * Composable for managing CodeViewer instances within the markdown editor.
68
+ * Handles mounting/unmounting Vue apps for code block "islands".
69
+ */
70
+ export function useCodeBlockManager(options: UseCodeBlockManagerOptions): UseCodeBlockManagerReturn {
71
+ const { contentRef, codeBlocks, updateCodeBlockContent, updateCodeBlockLanguage, onCodeBlockExit, onCodeBlockDelete, onCodeBlockMounted } = options;
72
+
73
+ // Track mounted instances by code block ID
74
+ const mountedInstances = new Map<string, MountedInstance>();
75
+
76
+ // Track watchers for cleanup
77
+ const mountedWatchers = new Map<string, () => void>();
78
+
79
+ // MutationObserver to watch for new code block wrappers
80
+ let observer: MutationObserver | null = null;
81
+
82
+ /**
83
+ * Create and mount a CodeViewer instance for a code block
84
+ */
85
+ function mountCodeViewer(wrapper: HTMLElement): void {
86
+ const id = wrapper.getAttribute("data-code-block-id");
87
+ if (!id) return;
88
+
89
+ // Skip if already mounted
90
+ if (mountedInstances.has(id)) return;
91
+
92
+ const mountPoint = wrapper.querySelector(".code-viewer-mount-point") as HTMLElement;
93
+ if (!mountPoint) return;
94
+
95
+ // Get state from the codeBlocks map
96
+ const state = codeBlocks.get(id);
97
+
98
+ // If no state in map, try to get from data attributes (initial load)
99
+ const initialContent = state?.content ?? mountPoint.getAttribute("data-content") ?? "";
100
+ const initialLanguage = state?.language ?? mountPoint.getAttribute("data-language") ?? "";
101
+
102
+ // Ensure state exists in the map
103
+ if (!state) {
104
+ codeBlocks.set(id, {
105
+ id,
106
+ content: initialContent,
107
+ language: initialLanguage
108
+ });
109
+ }
110
+
111
+ // Create reactive refs for the CodeViewer props
112
+ // These will be updated by watchers when the codeBlocks map changes
113
+ const reactiveContent = ref(initialContent);
114
+ const reactiveLanguage = ref(initialLanguage);
115
+
116
+ // Set up watchers to track changes to this code block's state
117
+ // We need separate watchers for content and language to ensure Vue properly
118
+ // tracks the reactive properties accessed within each getter function
119
+ const stopContentWatcher = watch(
120
+ () => codeBlocks.get(id)?.content,
121
+ (newContent) => {
122
+ if (newContent !== undefined) {
123
+ reactiveContent.value = newContent;
124
+ }
125
+ }
126
+ );
127
+
128
+ const stopLanguageWatcher = watch(
129
+ () => codeBlocks.get(id)?.language,
130
+ (newLanguage) => {
131
+ if (newLanguage !== undefined) {
132
+ reactiveLanguage.value = newLanguage;
133
+ }
134
+ }
135
+ );
136
+
137
+ // Combined cleanup function for both watchers
138
+ const stopWatcher = () => {
139
+ stopContentWatcher();
140
+ stopLanguageWatcher();
141
+ };
142
+
143
+ // Store the watcher cleanup function
144
+ mountedWatchers.set(id, stopWatcher);
145
+
146
+ // Create Vue app for CodeViewer
147
+ const app = createApp({
148
+ setup() {
149
+ // Handle model value updates
150
+ const onUpdateModelValue = (value: object | string | null) => {
151
+ const content = typeof value === "string" ? value : JSON.stringify(value);
152
+ updateCodeBlockContent(id, content);
153
+ };
154
+
155
+ // Handle format/language updates
156
+ const onUpdateFormat = (format: string) => {
157
+ updateCodeBlockLanguage(id, format);
158
+ };
159
+
160
+ // Handle exit (double-Enter at end of code block)
161
+ const onExit = () => {
162
+ if (onCodeBlockExit) {
163
+ onCodeBlockExit(id);
164
+ }
165
+ };
166
+
167
+ // Handle delete (Backspace/Delete on empty code block)
168
+ const onDelete = () => {
169
+ if (onCodeBlockDelete) {
170
+ onCodeBlockDelete(id);
171
+ }
172
+ };
173
+
174
+ // Create a computed for the format to ensure proper reactivity tracking
175
+ // The render function needs a computed that Vue can track for re-renders
176
+ const computedFormat = computed(() => mapLanguageToFormat(reactiveLanguage.value));
177
+
178
+ return () => h(CodeViewer, {
179
+ modelValue: reactiveContent.value,
180
+ format: computedFormat.value,
181
+ canEdit: true,
182
+ editable: true,
183
+ allowAnyLanguage: true,
184
+ class: "code-block-island",
185
+ "onUpdate:modelValue": onUpdateModelValue,
186
+ "onUpdate:format": onUpdateFormat,
187
+ onExit,
188
+ onDelete
189
+ });
190
+ }
191
+ });
192
+
193
+ // Install Quasar on the dynamically created app
194
+ // This is required because createApp() doesn't inherit plugins from the parent app
195
+ app.use(Quasar, { plugins: {} });
196
+
197
+ // Clear mount point content (remove data attributes div structure)
198
+ mountPoint.innerHTML = "";
199
+
200
+ // Mount the app
201
+ app.mount(mountPoint);
202
+
203
+ // Track the instance
204
+ mountedInstances.set(id, { app, mountPoint });
205
+
206
+ // Notify that the code block was mounted (after nextTick to ensure DOM is ready)
207
+ if (onCodeBlockMounted) {
208
+ nextTick(() => {
209
+ onCodeBlockMounted(id, wrapper);
210
+ });
211
+ }
212
+ }
213
+
214
+ /**
215
+ * Unmount a specific CodeViewer instance
216
+ */
217
+ function unmountCodeViewer(id: string): void {
218
+ // Clean up watcher
219
+ const stopWatcher = mountedWatchers.get(id);
220
+ if (stopWatcher) {
221
+ stopWatcher();
222
+ mountedWatchers.delete(id);
223
+ }
224
+
225
+ // Unmount Vue app
226
+ const instance = mountedInstances.get(id);
227
+ if (instance) {
228
+ instance.app.unmount();
229
+ mountedInstances.delete(id);
230
+ }
231
+ }
232
+
233
+ /**
234
+ * Unmount all CodeViewer instances
235
+ */
236
+ function unmountAllCodeViewers(): void {
237
+ // Clean up all watchers
238
+ for (const [id, stopWatcher] of mountedWatchers) {
239
+ stopWatcher();
240
+ mountedWatchers.delete(id);
241
+ }
242
+
243
+ // Unmount all Vue apps
244
+ for (const [id, instance] of mountedInstances) {
245
+ instance.app.unmount();
246
+ mountedInstances.delete(id);
247
+ }
248
+ }
249
+
250
+ /**
251
+ * Mount CodeViewer instances for all unmounted code blocks
252
+ */
253
+ function mountCodeViewers(): void {
254
+ if (!contentRef.value) return;
255
+
256
+ const wrappers = contentRef.value.querySelectorAll("[data-code-block-id]");
257
+ wrappers.forEach((wrapper) => {
258
+ mountCodeViewer(wrapper as HTMLElement);
259
+ });
260
+ }
261
+
262
+ /**
263
+ * Get mounted instance count
264
+ */
265
+ function getMountedCount(): number {
266
+ return mountedInstances.size;
267
+ }
268
+
269
+ /**
270
+ * Handle mutations to the content element
271
+ */
272
+ function handleMutations(mutations: MutationRecord[]): void {
273
+ for (const mutation of mutations) {
274
+ // Check for added nodes that are code block wrappers
275
+ for (const node of Array.from(mutation.addedNodes)) {
276
+ if (node.nodeType === Node.ELEMENT_NODE) {
277
+ const element = node as HTMLElement;
278
+
279
+ // Check if the added node is a code block wrapper
280
+ if (element.hasAttribute && element.hasAttribute("data-code-block-id")) {
281
+ nextTick(() => mountCodeViewer(element));
282
+ }
283
+
284
+ // Check children for code block wrappers
285
+ const childWrappers = element.querySelectorAll?.("[data-code-block-id]");
286
+ if (childWrappers && childWrappers.length > 0) {
287
+ childWrappers.forEach((wrapper) => {
288
+ nextTick(() => mountCodeViewer(wrapper as HTMLElement));
289
+ });
290
+ }
291
+ }
292
+ }
293
+
294
+ // Check for removed nodes that were code block wrappers
295
+ for (const node of Array.from(mutation.removedNodes)) {
296
+ if (node.nodeType === Node.ELEMENT_NODE) {
297
+ const element = node as HTMLElement;
298
+
299
+ // Check if the removed node is a code block wrapper
300
+ const id = element.getAttribute?.("data-code-block-id");
301
+ if (id) {
302
+ unmountCodeViewer(id);
303
+ }
304
+
305
+ // Check children for code block wrappers
306
+ const childWrappers = element.querySelectorAll?.("[data-code-block-id]");
307
+ if (childWrappers) {
308
+ childWrappers.forEach((wrapper) => {
309
+ const childId = wrapper.getAttribute("data-code-block-id");
310
+ if (childId) {
311
+ unmountCodeViewer(childId);
312
+ }
313
+ });
314
+ }
315
+ }
316
+ }
317
+ }
318
+ }
319
+
320
+ /**
321
+ * Start observing for DOM changes
322
+ */
323
+ function startObserver(): void {
324
+ if (!contentRef.value || observer) return;
325
+
326
+ observer = new MutationObserver(handleMutations);
327
+ observer.observe(contentRef.value, {
328
+ childList: true,
329
+ subtree: true
330
+ });
331
+ }
332
+
333
+ /**
334
+ * Stop observing DOM changes
335
+ */
336
+ function stopObserver(): void {
337
+ if (observer) {
338
+ observer.disconnect();
339
+ observer = null;
340
+ }
341
+ }
342
+
343
+ // Watch for contentRef changes to set up/tear down observer
344
+ watch(contentRef, (newRef, oldRef) => {
345
+ if (oldRef && !newRef) {
346
+ // Content ref was removed - clean up
347
+ stopObserver();
348
+ unmountAllCodeViewers();
349
+ } else if (newRef && !oldRef) {
350
+ // Content ref was added - set up
351
+ startObserver();
352
+ // Mount any existing code blocks
353
+ nextTick(() => mountCodeViewers());
354
+ }
355
+ }, { immediate: true });
356
+
357
+ // Clean up on unmount
358
+ onUnmounted(() => {
359
+ stopObserver();
360
+ unmountAllCodeViewers();
361
+ });
362
+
363
+ return {
364
+ mountCodeViewers,
365
+ unmountCodeViewer,
366
+ unmountAllCodeViewers,
367
+ getMountedCount
368
+ };
369
+ }