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,266 @@
|
|
|
1
|
+
import { Ref, ref } from "vue";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Hotkey group categories for organization
|
|
5
|
+
*/
|
|
6
|
+
export type HotkeyGroup = "headings" | "formatting" | "lists" | "blocks" | "tables" | "other";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Definition for a registered hotkey
|
|
10
|
+
*/
|
|
11
|
+
export interface HotkeyDefinition {
|
|
12
|
+
/** Key combination string, e.g., 'ctrl+1', 'ctrl+shift+b' */
|
|
13
|
+
key: string;
|
|
14
|
+
/** Action to execute when hotkey is triggered */
|
|
15
|
+
action: () => void;
|
|
16
|
+
/** Human-readable description for help display */
|
|
17
|
+
description: string;
|
|
18
|
+
/** Category group for help organization */
|
|
19
|
+
group: HotkeyGroup;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Options for useMarkdownHotkeys composable
|
|
24
|
+
*/
|
|
25
|
+
export interface UseMarkdownHotkeysOptions {
|
|
26
|
+
contentRef: Ref<HTMLElement | null>;
|
|
27
|
+
onShowHotkeyHelp: () => void;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Return type for useMarkdownHotkeys composable
|
|
32
|
+
*/
|
|
33
|
+
export interface UseMarkdownHotkeysReturn {
|
|
34
|
+
registerHotkey: (def: HotkeyDefinition) => void;
|
|
35
|
+
unregisterHotkey: (key: string) => void;
|
|
36
|
+
handleKeyDown: (event: KeyboardEvent) => boolean;
|
|
37
|
+
getHotkeyDefinitions: () => HotkeyDefinition[];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Parsed key combination for matching
|
|
42
|
+
*/
|
|
43
|
+
export interface ParsedKey {
|
|
44
|
+
key: string;
|
|
45
|
+
ctrl: boolean;
|
|
46
|
+
shift: boolean;
|
|
47
|
+
alt: boolean;
|
|
48
|
+
meta: boolean;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Parse a key combination string into its components
|
|
53
|
+
* Supports: ctrl, shift, alt, meta/cmd
|
|
54
|
+
*
|
|
55
|
+
* Examples:
|
|
56
|
+
* - 'ctrl+1' -> { key: '1', ctrl: true, ... }
|
|
57
|
+
* - 'ctrl+shift+b' -> { key: 'b', ctrl: true, shift: true, ... }
|
|
58
|
+
* - 'cmd+s' -> { key: 's', meta: true, ... } (Mac)
|
|
59
|
+
*/
|
|
60
|
+
export function parseKeyCombo(combo: string): ParsedKey {
|
|
61
|
+
const parts = combo.toLowerCase().split("+");
|
|
62
|
+
const result: ParsedKey = {
|
|
63
|
+
key: "",
|
|
64
|
+
ctrl: false,
|
|
65
|
+
shift: false,
|
|
66
|
+
alt: false,
|
|
67
|
+
meta: false
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
for (const part of parts) {
|
|
71
|
+
switch (part) {
|
|
72
|
+
case "ctrl":
|
|
73
|
+
case "control":
|
|
74
|
+
result.ctrl = true;
|
|
75
|
+
break;
|
|
76
|
+
case "shift":
|
|
77
|
+
result.shift = true;
|
|
78
|
+
break;
|
|
79
|
+
case "alt":
|
|
80
|
+
case "option":
|
|
81
|
+
result.alt = true;
|
|
82
|
+
break;
|
|
83
|
+
case "meta":
|
|
84
|
+
case "cmd":
|
|
85
|
+
case "command":
|
|
86
|
+
case "win":
|
|
87
|
+
case "windows":
|
|
88
|
+
result.meta = true;
|
|
89
|
+
break;
|
|
90
|
+
default:
|
|
91
|
+
// This is the actual key
|
|
92
|
+
result.key = part;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return result;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Check if a keyboard event matches a parsed key combination
|
|
101
|
+
* Handles cross-platform modifier differences (Ctrl on Windows/Linux, Cmd on Mac)
|
|
102
|
+
*/
|
|
103
|
+
export function matchesKeyCombo(event: KeyboardEvent, parsed: ParsedKey): boolean {
|
|
104
|
+
// Normalize the event key
|
|
105
|
+
let eventKey = event.key.toLowerCase();
|
|
106
|
+
|
|
107
|
+
// Normalize arrow keys: ArrowUp -> up, ArrowDown -> down, etc.
|
|
108
|
+
if (eventKey.startsWith("arrow")) {
|
|
109
|
+
eventKey = eventKey.replace("arrow", "");
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Special handling for shifted keys
|
|
113
|
+
// When shift is held, some keys produce different characters
|
|
114
|
+
const shiftedKeys: Record<string, string> = {
|
|
115
|
+
">": ".",
|
|
116
|
+
"<": ",",
|
|
117
|
+
"?": "/",
|
|
118
|
+
"!": "1",
|
|
119
|
+
"@": "2",
|
|
120
|
+
"#": "3",
|
|
121
|
+
"$": "4",
|
|
122
|
+
"%": "5",
|
|
123
|
+
"^": "6",
|
|
124
|
+
"&": "7",
|
|
125
|
+
"*": "8",
|
|
126
|
+
"(": "9",
|
|
127
|
+
")": "0",
|
|
128
|
+
"{": "[",
|
|
129
|
+
"}": "]"
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
// If the parsed key expects a shifted character, check if we have the right combination
|
|
133
|
+
if (shiftedKeys[parsed.key]) {
|
|
134
|
+
// User wants to match '>' which is shift+.
|
|
135
|
+
if (eventKey === parsed.key) {
|
|
136
|
+
// Browser reports the shifted character directly
|
|
137
|
+
return matchesModifiers(event, { ...parsed, shift: true });
|
|
138
|
+
}
|
|
139
|
+
// Or check if shift+base key matches
|
|
140
|
+
if (event.shiftKey && eventKey === shiftedKeys[parsed.key]) {
|
|
141
|
+
return matchesModifiers(event, { ...parsed, shift: true });
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Handle case where browser reports shifted character but hotkey expects base key with shift
|
|
146
|
+
// e.g., hotkey "ctrl+shift+[" but browser reports "{" when Ctrl+Shift+[ is pressed
|
|
147
|
+
if (shiftedKeys[eventKey] && shiftedKeys[eventKey] === parsed.key) {
|
|
148
|
+
// The event key is a shifted character (e.g., "{") that maps to the parsed key (e.g., "[")
|
|
149
|
+
return matchesModifiers(event, parsed);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Handle number keys (both main keyboard and numpad)
|
|
153
|
+
if (/^[0-6]$/.test(parsed.key)) {
|
|
154
|
+
if (eventKey !== parsed.key && event.code !== `Digit${parsed.key}` && event.code !== `Numpad${parsed.key}`) {
|
|
155
|
+
return false;
|
|
156
|
+
}
|
|
157
|
+
} else if (eventKey !== parsed.key) {
|
|
158
|
+
return false;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return matchesModifiers(event, parsed);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Check if modifier keys match
|
|
166
|
+
* On Mac, treat Cmd (metaKey) as equivalent to Ctrl for cross-platform support
|
|
167
|
+
*/
|
|
168
|
+
function matchesModifiers(event: KeyboardEvent, parsed: ParsedKey): boolean {
|
|
169
|
+
const isMac = navigator.platform.toLowerCase().includes("mac");
|
|
170
|
+
|
|
171
|
+
// For cross-platform support:
|
|
172
|
+
// - If hotkey specifies 'ctrl', match either ctrlKey or metaKey (on Mac)
|
|
173
|
+
// - If hotkey specifies 'meta', match metaKey only
|
|
174
|
+
let ctrlMatch: boolean;
|
|
175
|
+
if (parsed.ctrl) {
|
|
176
|
+
if (isMac) {
|
|
177
|
+
// On Mac, ctrl+key can be either Ctrl+key or Cmd+key
|
|
178
|
+
ctrlMatch = event.ctrlKey || event.metaKey;
|
|
179
|
+
} else {
|
|
180
|
+
ctrlMatch = event.ctrlKey;
|
|
181
|
+
}
|
|
182
|
+
} else if (parsed.meta) {
|
|
183
|
+
ctrlMatch = event.metaKey;
|
|
184
|
+
} else {
|
|
185
|
+
// No modifier required, ensure neither is pressed
|
|
186
|
+
ctrlMatch = !event.ctrlKey && !event.metaKey;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Check other modifiers exactly
|
|
190
|
+
const shiftMatch = parsed.shift === event.shiftKey;
|
|
191
|
+
const altMatch = parsed.alt === event.altKey;
|
|
192
|
+
|
|
193
|
+
return ctrlMatch && shiftMatch && altMatch;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Composable for hotkey registration and dispatch in markdown editor
|
|
198
|
+
*/
|
|
199
|
+
export function useMarkdownHotkeys(options: UseMarkdownHotkeysOptions): UseMarkdownHotkeysReturn {
|
|
200
|
+
const { onShowHotkeyHelp } = options;
|
|
201
|
+
|
|
202
|
+
// Registry of all hotkeys
|
|
203
|
+
const hotkeys = ref<Map<string, HotkeyDefinition>>(new Map());
|
|
204
|
+
|
|
205
|
+
// Pre-parsed key combinations for performance
|
|
206
|
+
const parsedKeys = new Map<string, ParsedKey>();
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Register a new hotkey
|
|
210
|
+
*/
|
|
211
|
+
function registerHotkey(def: HotkeyDefinition): void {
|
|
212
|
+
const normalizedKey = def.key.toLowerCase();
|
|
213
|
+
hotkeys.value.set(normalizedKey, def);
|
|
214
|
+
parsedKeys.set(normalizedKey, parseKeyCombo(normalizedKey));
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Unregister a hotkey
|
|
219
|
+
*/
|
|
220
|
+
function unregisterHotkey(key: string): void {
|
|
221
|
+
const normalizedKey = key.toLowerCase();
|
|
222
|
+
hotkeys.value.delete(normalizedKey);
|
|
223
|
+
parsedKeys.delete(normalizedKey);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Get all registered hotkey definitions for help display
|
|
228
|
+
*/
|
|
229
|
+
function getHotkeyDefinitions(): HotkeyDefinition[] {
|
|
230
|
+
return Array.from(hotkeys.value.values());
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Handle a keydown event and dispatch to registered hotkey
|
|
235
|
+
* Returns true if a hotkey was matched and handled
|
|
236
|
+
*/
|
|
237
|
+
function handleKeyDown(event: KeyboardEvent): boolean {
|
|
238
|
+
// Check for help shortcut first (Ctrl/Cmd + / or Ctrl/Cmd + ?)
|
|
239
|
+
// Supports both Ctrl+/ (without shift) and Ctrl+? (which is Ctrl+Shift+/)
|
|
240
|
+
if ((event.ctrlKey || event.metaKey) && (event.key === "?" || event.key === "/")) {
|
|
241
|
+
event.preventDefault();
|
|
242
|
+
onShowHotkeyHelp();
|
|
243
|
+
return true;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// Check all registered hotkeys
|
|
247
|
+
for (const [key, def] of hotkeys.value) {
|
|
248
|
+
const parsed = parsedKeys.get(key);
|
|
249
|
+
|
|
250
|
+
if (parsed && matchesKeyCombo(event, parsed)) {
|
|
251
|
+
event.preventDefault();
|
|
252
|
+
def.action();
|
|
253
|
+
return true;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
return false;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
return {
|
|
261
|
+
registerHotkey,
|
|
262
|
+
unregisterHotkey,
|
|
263
|
+
handleKeyDown,
|
|
264
|
+
getHotkeyDefinitions
|
|
265
|
+
};
|
|
266
|
+
}
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
import { Ref } from "vue";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Cursor position tracking for markdown editor
|
|
5
|
+
*/
|
|
6
|
+
export interface CursorPosition {
|
|
7
|
+
/** Index of the block element containing the cursor */
|
|
8
|
+
blockIndex: number;
|
|
9
|
+
/** Character offset within the block */
|
|
10
|
+
charOffset: number;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Return type for useMarkdownSelection composable
|
|
15
|
+
*/
|
|
16
|
+
export interface UseMarkdownSelectionReturn {
|
|
17
|
+
saveCursorPosition: () => CursorPosition | null;
|
|
18
|
+
restoreCursorPosition: (position: CursorPosition) => void;
|
|
19
|
+
getCurrentBlock: () => Element | null;
|
|
20
|
+
getBlockIndex: () => number;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Get cursor offset in plain text within a contenteditable element
|
|
25
|
+
* Adapted from useCodeViewerEditor.ts
|
|
26
|
+
*/
|
|
27
|
+
function getCursorOffset(element: HTMLElement | null): number {
|
|
28
|
+
const selection = window.getSelection();
|
|
29
|
+
if (!selection || !selection.rangeCount || !element) return 0;
|
|
30
|
+
|
|
31
|
+
const range = selection.getRangeAt(0);
|
|
32
|
+
const preCaretRange = document.createRange();
|
|
33
|
+
preCaretRange.selectNodeContents(element);
|
|
34
|
+
preCaretRange.setEnd(range.startContainer, range.startOffset);
|
|
35
|
+
|
|
36
|
+
// Count characters by walking text nodes
|
|
37
|
+
let offset = 0;
|
|
38
|
+
const walker = document.createTreeWalker(preCaretRange.commonAncestorContainer, NodeFilter.SHOW_TEXT);
|
|
39
|
+
let node = walker.nextNode();
|
|
40
|
+
while (node) {
|
|
41
|
+
if (preCaretRange.intersectsNode(node)) {
|
|
42
|
+
const nodeRange = document.createRange();
|
|
43
|
+
nodeRange.selectNodeContents(node);
|
|
44
|
+
if (preCaretRange.compareBoundaryPoints(Range.END_TO_END, nodeRange) >= 0) {
|
|
45
|
+
offset += node.textContent?.length || 0;
|
|
46
|
+
} else {
|
|
47
|
+
// Partial node - cursor is in this node
|
|
48
|
+
offset += range.startOffset;
|
|
49
|
+
break;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
node = walker.nextNode();
|
|
53
|
+
}
|
|
54
|
+
return offset;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Set cursor to offset in plain text within a contenteditable element
|
|
59
|
+
* Adapted from useCodeViewerEditor.ts
|
|
60
|
+
*/
|
|
61
|
+
function setCursorOffset(element: HTMLElement | null, targetOffset: number): void {
|
|
62
|
+
if (!element) return;
|
|
63
|
+
|
|
64
|
+
const selection = window.getSelection();
|
|
65
|
+
if (!selection) return;
|
|
66
|
+
|
|
67
|
+
let currentOffset = 0;
|
|
68
|
+
const walker = document.createTreeWalker(element, NodeFilter.SHOW_TEXT);
|
|
69
|
+
let node = walker.nextNode();
|
|
70
|
+
|
|
71
|
+
while (node) {
|
|
72
|
+
const nodeLength = node.textContent?.length || 0;
|
|
73
|
+
if (currentOffset + nodeLength >= targetOffset) {
|
|
74
|
+
const range = document.createRange();
|
|
75
|
+
range.setStart(node, targetOffset - currentOffset);
|
|
76
|
+
range.collapse(true);
|
|
77
|
+
selection.removeAllRanges();
|
|
78
|
+
selection.addRange(range);
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
currentOffset += nodeLength;
|
|
82
|
+
node = walker.nextNode();
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// If offset not found, place at end
|
|
86
|
+
const range = document.createRange();
|
|
87
|
+
range.selectNodeContents(element);
|
|
88
|
+
range.collapse(false);
|
|
89
|
+
selection.removeAllRanges();
|
|
90
|
+
selection.addRange(range);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Get the block-level parent element (p, h1-h6, li, blockquote, etc.) containing the cursor
|
|
95
|
+
*/
|
|
96
|
+
function findBlockParent(node: Node | null, contentRef: HTMLElement): Element | null {
|
|
97
|
+
if (!node) return null;
|
|
98
|
+
|
|
99
|
+
const blockTags = ["P", "H1", "H2", "H3", "H4", "H5", "H6", "LI", "BLOCKQUOTE", "PRE", "DIV"];
|
|
100
|
+
|
|
101
|
+
let current: Node | null = node;
|
|
102
|
+
while (current && current !== contentRef) {
|
|
103
|
+
if (current.nodeType === Node.ELEMENT_NODE) {
|
|
104
|
+
const element = current as Element;
|
|
105
|
+
if (blockTags.includes(element.tagName)) {
|
|
106
|
+
return element;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
current = current.parentNode;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Get all direct block children of the content element
|
|
117
|
+
*/
|
|
118
|
+
function getBlockElements(contentRef: HTMLElement): Element[] {
|
|
119
|
+
const blocks: Element[] = [];
|
|
120
|
+
const blockTags = ["P", "H1", "H2", "H3", "H4", "H5", "H6", "UL", "OL", "BLOCKQUOTE", "PRE", "DIV", "TABLE", "HR"];
|
|
121
|
+
|
|
122
|
+
for (const child of Array.from(contentRef.children)) {
|
|
123
|
+
if (blockTags.includes(child.tagName)) {
|
|
124
|
+
blocks.push(child);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return blocks;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Composable for cursor and selection management in markdown editor
|
|
133
|
+
*/
|
|
134
|
+
export function useMarkdownSelection(contentRef: Ref<HTMLElement | null>): UseMarkdownSelectionReturn {
|
|
135
|
+
/**
|
|
136
|
+
* Get the current block element containing the cursor
|
|
137
|
+
*/
|
|
138
|
+
function getCurrentBlock(): Element | null {
|
|
139
|
+
if (!contentRef.value) return null;
|
|
140
|
+
|
|
141
|
+
const selection = window.getSelection();
|
|
142
|
+
if (!selection || !selection.rangeCount) return null;
|
|
143
|
+
|
|
144
|
+
const range = selection.getRangeAt(0);
|
|
145
|
+
return findBlockParent(range.startContainer, contentRef.value);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Get the index of the current block within the content element
|
|
150
|
+
*/
|
|
151
|
+
function getBlockIndex(): number {
|
|
152
|
+
if (!contentRef.value) return -1;
|
|
153
|
+
|
|
154
|
+
const currentBlock = getCurrentBlock();
|
|
155
|
+
if (!currentBlock) return -1;
|
|
156
|
+
|
|
157
|
+
const blocks = getBlockElements(contentRef.value);
|
|
158
|
+
return blocks.indexOf(currentBlock);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Save current cursor position as block index + character offset
|
|
163
|
+
*/
|
|
164
|
+
function saveCursorPosition(): CursorPosition | null {
|
|
165
|
+
if (!contentRef.value) return null;
|
|
166
|
+
|
|
167
|
+
const selection = window.getSelection();
|
|
168
|
+
if (!selection || !selection.rangeCount) return null;
|
|
169
|
+
|
|
170
|
+
const currentBlock = getCurrentBlock();
|
|
171
|
+
if (!currentBlock) {
|
|
172
|
+
// Cursor is not in a block, save position relative to content root
|
|
173
|
+
return {
|
|
174
|
+
blockIndex: -1,
|
|
175
|
+
charOffset: getCursorOffset(contentRef.value)
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const blocks = getBlockElements(contentRef.value);
|
|
180
|
+
const blockIndex = blocks.indexOf(currentBlock);
|
|
181
|
+
const charOffset = getCursorOffset(currentBlock as HTMLElement);
|
|
182
|
+
|
|
183
|
+
return { blockIndex, charOffset };
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Restore cursor position from saved state
|
|
188
|
+
*/
|
|
189
|
+
function restoreCursorPosition(position: CursorPosition): void {
|
|
190
|
+
if (!contentRef.value) return;
|
|
191
|
+
|
|
192
|
+
if (position.blockIndex === -1) {
|
|
193
|
+
// Restore to content root level
|
|
194
|
+
setCursorOffset(contentRef.value, position.charOffset);
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const blocks = getBlockElements(contentRef.value);
|
|
199
|
+
if (position.blockIndex >= 0 && position.blockIndex < blocks.length) {
|
|
200
|
+
const block = blocks[position.blockIndex] as HTMLElement;
|
|
201
|
+
setCursorOffset(block, position.charOffset);
|
|
202
|
+
} else {
|
|
203
|
+
// Block no longer exists, place cursor at end
|
|
204
|
+
const range = document.createRange();
|
|
205
|
+
range.selectNodeContents(contentRef.value);
|
|
206
|
+
range.collapse(false);
|
|
207
|
+
const selection = window.getSelection();
|
|
208
|
+
selection?.removeAllRanges();
|
|
209
|
+
selection?.addRange(range);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
return {
|
|
214
|
+
saveCursorPosition,
|
|
215
|
+
restoreCursorPosition,
|
|
216
|
+
getCurrentBlock,
|
|
217
|
+
getBlockIndex
|
|
218
|
+
};
|
|
219
|
+
}
|