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,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
|
+
}
|