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,1068 @@
|
|
|
1
|
+
import { computed, nextTick, Ref, ref } from "vue";
|
|
2
|
+
import { HotkeyDefinition, useMarkdownHotkeys } from "./useMarkdownHotkeys";
|
|
3
|
+
import { useMarkdownSelection } from "./useMarkdownSelection";
|
|
4
|
+
import { useMarkdownSync } from "./useMarkdownSync";
|
|
5
|
+
import { useBlockquotes } from "./features/useBlockquotes";
|
|
6
|
+
import { useCodeBlocks } from "./features/useCodeBlocks";
|
|
7
|
+
import { useCodeBlockManager } from "./features/useCodeBlockManager";
|
|
8
|
+
import { useHeadings } from "./features/useHeadings";
|
|
9
|
+
import { useInlineFormatting } from "./features/useInlineFormatting";
|
|
10
|
+
import { ShowLinkPopoverOptions, useLinks } from "./features/useLinks";
|
|
11
|
+
import { useLists } from "./features/useLists";
|
|
12
|
+
import { ShowTablePopoverOptions, useTables } from "./features/useTables";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Options for useMarkdownEditor composable
|
|
16
|
+
*/
|
|
17
|
+
export interface UseMarkdownEditorOptions {
|
|
18
|
+
contentRef: Ref<HTMLElement | null>;
|
|
19
|
+
initialValue: string;
|
|
20
|
+
onEmitValue: (markdown: string) => void;
|
|
21
|
+
/** Callback to show the link popover UI */
|
|
22
|
+
onShowLinkPopover?: (options: ShowLinkPopoverOptions) => void;
|
|
23
|
+
/** Callback to show the table popover UI */
|
|
24
|
+
onShowTablePopover?: (options: ShowTablePopoverOptions) => void;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Return type for useMarkdownEditor composable
|
|
29
|
+
*/
|
|
30
|
+
export interface UseMarkdownEditorReturn {
|
|
31
|
+
// From sync
|
|
32
|
+
renderedHtml: Ref<string>;
|
|
33
|
+
isInternalUpdate: Ref<boolean>;
|
|
34
|
+
|
|
35
|
+
// State
|
|
36
|
+
isShowingHotkeyHelp: Ref<boolean>;
|
|
37
|
+
charCount: Ref<number>;
|
|
38
|
+
|
|
39
|
+
// Event handlers
|
|
40
|
+
onInput: () => void;
|
|
41
|
+
onKeyDown: (event: KeyboardEvent) => void;
|
|
42
|
+
onBlur: () => void;
|
|
43
|
+
|
|
44
|
+
// For external value updates
|
|
45
|
+
setMarkdown: (markdown: string) => void;
|
|
46
|
+
|
|
47
|
+
// Formatting actions
|
|
48
|
+
insertHorizontalRule: () => void;
|
|
49
|
+
|
|
50
|
+
// Hotkey help
|
|
51
|
+
showHotkeyHelp: () => void;
|
|
52
|
+
hideHotkeyHelp: () => void;
|
|
53
|
+
hotkeyDefinitions: Ref<HotkeyDefinition[]>;
|
|
54
|
+
|
|
55
|
+
// Feature access (for custom hotkey registration)
|
|
56
|
+
headings: ReturnType<typeof useHeadings>;
|
|
57
|
+
inlineFormatting: ReturnType<typeof useInlineFormatting>;
|
|
58
|
+
links: ReturnType<typeof useLinks>;
|
|
59
|
+
lists: ReturnType<typeof useLists>;
|
|
60
|
+
codeBlocks: ReturnType<typeof useCodeBlocks>;
|
|
61
|
+
codeBlockManager: ReturnType<typeof useCodeBlockManager>;
|
|
62
|
+
blockquotes: ReturnType<typeof useBlockquotes>;
|
|
63
|
+
tables: ReturnType<typeof useTables>;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Main orchestrator composable for markdown editor
|
|
68
|
+
* Composes selection, sync, hotkeys, and feature composables
|
|
69
|
+
*/
|
|
70
|
+
export function useMarkdownEditor(options: UseMarkdownEditorOptions): UseMarkdownEditorReturn {
|
|
71
|
+
const { contentRef, initialValue, onEmitValue, onShowLinkPopover, onShowTablePopover } = options;
|
|
72
|
+
|
|
73
|
+
// State
|
|
74
|
+
const isShowingHotkeyHelp = ref(false);
|
|
75
|
+
|
|
76
|
+
// Initialize selection management
|
|
77
|
+
const selection = useMarkdownSelection(contentRef);
|
|
78
|
+
|
|
79
|
+
// Initialize code blocks feature first (sync needs access to getCodeBlockById)
|
|
80
|
+
// Note: onContentChange will be set after sync is created
|
|
81
|
+
let syncCallback: (() => void) | null = null;
|
|
82
|
+
const codeBlocks = useCodeBlocks({
|
|
83
|
+
contentRef,
|
|
84
|
+
selection,
|
|
85
|
+
onContentChange: () => {
|
|
86
|
+
if (syncCallback) syncCallback();
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
// Initialize sync with code block lookup for HTML-to-markdown conversion
|
|
91
|
+
// and registration for markdown-to-HTML conversion (initial render)
|
|
92
|
+
const sync = useMarkdownSync({
|
|
93
|
+
contentRef,
|
|
94
|
+
onEmitValue,
|
|
95
|
+
debounceMs: 300,
|
|
96
|
+
getCodeBlockById: codeBlocks.getCodeBlockById,
|
|
97
|
+
registerCodeBlock: codeBlocks.registerCodeBlock
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
// Now set the sync callback for code blocks
|
|
101
|
+
syncCallback = () => sync.debouncedSyncFromHtml();
|
|
102
|
+
|
|
103
|
+
// Initialize hotkeys
|
|
104
|
+
const hotkeys = useMarkdownHotkeys({
|
|
105
|
+
contentRef,
|
|
106
|
+
onShowHotkeyHelp: () => {
|
|
107
|
+
isShowingHotkeyHelp.value = true;
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
// Initialize headings feature
|
|
112
|
+
const headings = useHeadings({
|
|
113
|
+
contentRef,
|
|
114
|
+
selection,
|
|
115
|
+
onContentChange: () => {
|
|
116
|
+
sync.debouncedSyncFromHtml();
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
// Initialize inline formatting feature
|
|
121
|
+
const inlineFormatting = useInlineFormatting({
|
|
122
|
+
contentRef,
|
|
123
|
+
onContentChange: () => {
|
|
124
|
+
sync.debouncedSyncFromHtml();
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
// Initialize lists feature
|
|
129
|
+
const lists = useLists({
|
|
130
|
+
contentRef,
|
|
131
|
+
selection,
|
|
132
|
+
onContentChange: () => {
|
|
133
|
+
sync.debouncedSyncFromHtml();
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
// Initialize blockquotes feature
|
|
138
|
+
const blockquotes = useBlockquotes({
|
|
139
|
+
contentRef,
|
|
140
|
+
onContentChange: () => {
|
|
141
|
+
sync.debouncedSyncFromHtml();
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
// Initialize links feature
|
|
146
|
+
const links = useLinks({
|
|
147
|
+
contentRef,
|
|
148
|
+
onContentChange: () => {
|
|
149
|
+
sync.debouncedSyncFromHtml();
|
|
150
|
+
},
|
|
151
|
+
onShowLinkPopover
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
// Initialize tables feature
|
|
155
|
+
const tables = useTables({
|
|
156
|
+
contentRef,
|
|
157
|
+
onContentChange: () => {
|
|
158
|
+
sync.debouncedSyncFromHtml();
|
|
159
|
+
},
|
|
160
|
+
onShowTablePopover
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Handle code block exit (double-Enter at end of code block)
|
|
165
|
+
* Creates a new paragraph after the code block and positions cursor there
|
|
166
|
+
*/
|
|
167
|
+
function handleCodeBlockExit(id: string): void {
|
|
168
|
+
if (!contentRef.value) return;
|
|
169
|
+
|
|
170
|
+
const wrapper = contentRef.value.querySelector(`[data-code-block-id="${id}"]`);
|
|
171
|
+
if (!wrapper) return;
|
|
172
|
+
|
|
173
|
+
// Create new paragraph after the code block
|
|
174
|
+
const p = document.createElement("p");
|
|
175
|
+
p.appendChild(document.createElement("br"));
|
|
176
|
+
wrapper.parentNode?.insertBefore(p, wrapper.nextSibling);
|
|
177
|
+
|
|
178
|
+
// Position cursor in the new paragraph
|
|
179
|
+
nextTick(() => {
|
|
180
|
+
const range = document.createRange();
|
|
181
|
+
range.selectNodeContents(p);
|
|
182
|
+
range.collapse(true);
|
|
183
|
+
const sel = window.getSelection();
|
|
184
|
+
sel?.removeAllRanges();
|
|
185
|
+
sel?.addRange(range);
|
|
186
|
+
p.focus();
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
sync.debouncedSyncFromHtml();
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Handle code block delete (Backspace/Delete on empty code block)
|
|
194
|
+
* Removes the code block and positions cursor in the previous or next element
|
|
195
|
+
*/
|
|
196
|
+
function handleCodeBlockDelete(id: string): void {
|
|
197
|
+
if (!contentRef.value) return;
|
|
198
|
+
|
|
199
|
+
const wrapper = contentRef.value.querySelector(`[data-code-block-id="${id}"]`);
|
|
200
|
+
if (!wrapper) return;
|
|
201
|
+
|
|
202
|
+
// Find previous or next sibling to position cursor
|
|
203
|
+
const previousSibling = wrapper.previousElementSibling;
|
|
204
|
+
const nextSibling = wrapper.nextElementSibling;
|
|
205
|
+
|
|
206
|
+
// Remove the code block from state (this will trigger MutationObserver cleanup)
|
|
207
|
+
codeBlocks.codeBlocks.delete(id);
|
|
208
|
+
|
|
209
|
+
// Remove the wrapper from DOM (MutationObserver will handle unmounting)
|
|
210
|
+
wrapper.remove();
|
|
211
|
+
|
|
212
|
+
// Position cursor in the appropriate element
|
|
213
|
+
nextTick(() => {
|
|
214
|
+
let targetElement: Element | null = null;
|
|
215
|
+
|
|
216
|
+
if (previousSibling) {
|
|
217
|
+
targetElement = previousSibling;
|
|
218
|
+
} else if (nextSibling) {
|
|
219
|
+
targetElement = nextSibling;
|
|
220
|
+
} else {
|
|
221
|
+
// No siblings - create a new paragraph
|
|
222
|
+
const p = document.createElement("p");
|
|
223
|
+
p.appendChild(document.createElement("br"));
|
|
224
|
+
contentRef.value?.appendChild(p);
|
|
225
|
+
targetElement = p;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
if (targetElement) {
|
|
229
|
+
const range = document.createRange();
|
|
230
|
+
range.selectNodeContents(targetElement);
|
|
231
|
+
range.collapse(previousSibling ? false : true); // End if previous, start if next
|
|
232
|
+
const sel = window.getSelection();
|
|
233
|
+
sel?.removeAllRanges();
|
|
234
|
+
sel?.addRange(range);
|
|
235
|
+
}
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
sync.debouncedSyncFromHtml();
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// Initialize code block manager for mounting CodeViewer instances
|
|
242
|
+
const codeBlockManager = useCodeBlockManager({
|
|
243
|
+
contentRef,
|
|
244
|
+
codeBlocks: codeBlocks.codeBlocks,
|
|
245
|
+
updateCodeBlockContent: codeBlocks.updateCodeBlockContent,
|
|
246
|
+
updateCodeBlockLanguage: codeBlocks.updateCodeBlockLanguage,
|
|
247
|
+
onCodeBlockExit: handleCodeBlockExit,
|
|
248
|
+
onCodeBlockDelete: handleCodeBlockDelete,
|
|
249
|
+
onCodeBlockMounted: codeBlocks.handleCodeBlockMounted
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
// Register default hotkeys
|
|
253
|
+
registerDefaultHotkeys();
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Set heading level, handling list items by converting to paragraph first.
|
|
257
|
+
* This wrapper ensures Ctrl+0-6 hotkeys work even when cursor is in a list.
|
|
258
|
+
*/
|
|
259
|
+
function setHeadingLevelWithListHandling(level: 0 | 1 | 2 | 3 | 4 | 5 | 6): void {
|
|
260
|
+
// Check if currently in a list
|
|
261
|
+
const listType = lists.getCurrentListType();
|
|
262
|
+
if (listType) {
|
|
263
|
+
// Convert list item to paragraph first
|
|
264
|
+
lists.convertCurrentListItemToParagraph();
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// Now apply heading level (skip if level is 0 and we just converted from list,
|
|
268
|
+
// because convertCurrentListItemToParagraph already creates a paragraph)
|
|
269
|
+
if (level > 0 || !listType) {
|
|
270
|
+
headings.setHeadingLevel(level);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Increase heading level, handling list items by converting to paragraph first.
|
|
276
|
+
* This wrapper ensures Ctrl+> hotkey works even when cursor is in a list.
|
|
277
|
+
*/
|
|
278
|
+
function increaseHeadingLevelWithListHandling(): void {
|
|
279
|
+
const listType = lists.getCurrentListType();
|
|
280
|
+
if (listType) {
|
|
281
|
+
// Convert list item to paragraph first, then apply H6 (since P -> H6 is first step)
|
|
282
|
+
lists.convertCurrentListItemToParagraph();
|
|
283
|
+
headings.setHeadingLevel(6);
|
|
284
|
+
} else {
|
|
285
|
+
headings.increaseHeadingLevel();
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Decrease heading level, handling list items by converting to paragraph first.
|
|
291
|
+
* This wrapper ensures Ctrl+< hotkey works even when cursor is in a list.
|
|
292
|
+
* For list items, just converts to paragraph (since that's the lowest level).
|
|
293
|
+
*/
|
|
294
|
+
function decreaseHeadingLevelWithListHandling(): void {
|
|
295
|
+
const listType = lists.getCurrentListType();
|
|
296
|
+
if (listType) {
|
|
297
|
+
// Convert list item to paragraph (already at paragraph level after conversion)
|
|
298
|
+
lists.convertCurrentListItemToParagraph();
|
|
299
|
+
} else {
|
|
300
|
+
headings.decreaseHeadingLevel();
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Toggle code block, handling list items by converting to paragraph first.
|
|
306
|
+
* This wrapper ensures Ctrl+Shift+K hotkey works even when cursor is in a list.
|
|
307
|
+
*/
|
|
308
|
+
function toggleCodeBlockWithListHandling(): void {
|
|
309
|
+
// If already in a code block, just toggle off
|
|
310
|
+
if (codeBlocks.isInCodeBlock()) {
|
|
311
|
+
codeBlocks.toggleCodeBlock();
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// Check if currently in a list
|
|
316
|
+
const listType = lists.getCurrentListType();
|
|
317
|
+
if (listType) {
|
|
318
|
+
// Convert list item to paragraph first
|
|
319
|
+
lists.convertCurrentListItemToParagraph();
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// Now toggle to code block
|
|
323
|
+
codeBlocks.toggleCodeBlock();
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// Computed character count
|
|
327
|
+
const charCount = computed(() => {
|
|
328
|
+
return contentRef.value?.textContent?.length || 0;
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
// Reactive hotkey definitions for UI
|
|
332
|
+
const hotkeyDefinitions = computed(() => {
|
|
333
|
+
return hotkeys.getHotkeyDefinitions();
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* Register default hotkeys for all features
|
|
338
|
+
*/
|
|
339
|
+
function registerDefaultHotkeys(): void {
|
|
340
|
+
// === Inline Formatting Hotkeys ===
|
|
341
|
+
hotkeys.registerHotkey({
|
|
342
|
+
key: "ctrl+b",
|
|
343
|
+
action: () => inlineFormatting.toggleBold(),
|
|
344
|
+
description: "Bold",
|
|
345
|
+
group: "formatting"
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
hotkeys.registerHotkey({
|
|
349
|
+
key: "ctrl+i",
|
|
350
|
+
action: () => inlineFormatting.toggleItalic(),
|
|
351
|
+
description: "Italic",
|
|
352
|
+
group: "formatting"
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
hotkeys.registerHotkey({
|
|
356
|
+
key: "ctrl+e",
|
|
357
|
+
action: () => inlineFormatting.toggleInlineCode(),
|
|
358
|
+
description: "Inline code",
|
|
359
|
+
group: "formatting"
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
hotkeys.registerHotkey({
|
|
363
|
+
key: "ctrl+shift+s",
|
|
364
|
+
action: () => inlineFormatting.toggleStrikethrough(),
|
|
365
|
+
description: "Strikethrough",
|
|
366
|
+
group: "formatting"
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
hotkeys.registerHotkey({
|
|
370
|
+
key: "ctrl+shift+h",
|
|
371
|
+
action: () => inlineFormatting.toggleHighlight(),
|
|
372
|
+
description: "Highlight",
|
|
373
|
+
group: "formatting"
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
hotkeys.registerHotkey({
|
|
377
|
+
key: "ctrl+u",
|
|
378
|
+
action: () => inlineFormatting.toggleUnderline(),
|
|
379
|
+
description: "Underline",
|
|
380
|
+
group: "formatting"
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
// === Heading Hotkeys (Ctrl+0 through Ctrl+6) ===
|
|
384
|
+
// These use wrapper functions that handle list items by converting to paragraph first
|
|
385
|
+
hotkeys.registerHotkey({
|
|
386
|
+
key: "ctrl+0",
|
|
387
|
+
action: () => setHeadingLevelWithListHandling(0),
|
|
388
|
+
description: "Convert to paragraph",
|
|
389
|
+
group: "headings"
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
hotkeys.registerHotkey({
|
|
393
|
+
key: "ctrl+1",
|
|
394
|
+
action: () => setHeadingLevelWithListHandling(1),
|
|
395
|
+
description: "Convert to Heading 1",
|
|
396
|
+
group: "headings"
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
hotkeys.registerHotkey({
|
|
400
|
+
key: "ctrl+2",
|
|
401
|
+
action: () => setHeadingLevelWithListHandling(2),
|
|
402
|
+
description: "Convert to Heading 2",
|
|
403
|
+
group: "headings"
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
hotkeys.registerHotkey({
|
|
407
|
+
key: "ctrl+3",
|
|
408
|
+
action: () => setHeadingLevelWithListHandling(3),
|
|
409
|
+
description: "Convert to Heading 3",
|
|
410
|
+
group: "headings"
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
hotkeys.registerHotkey({
|
|
414
|
+
key: "ctrl+4",
|
|
415
|
+
action: () => setHeadingLevelWithListHandling(4),
|
|
416
|
+
description: "Convert to Heading 4",
|
|
417
|
+
group: "headings"
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
hotkeys.registerHotkey({
|
|
421
|
+
key: "ctrl+5",
|
|
422
|
+
action: () => setHeadingLevelWithListHandling(5),
|
|
423
|
+
description: "Convert to Heading 5",
|
|
424
|
+
group: "headings"
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
hotkeys.registerHotkey({
|
|
428
|
+
key: "ctrl+6",
|
|
429
|
+
action: () => setHeadingLevelWithListHandling(6),
|
|
430
|
+
description: "Convert to Heading 6",
|
|
431
|
+
group: "headings"
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
// Heading level cycling hotkeys (also handle list items)
|
|
435
|
+
// Ctrl+< decreases heading (H1 -> H2 -> ... -> H6 -> P)
|
|
436
|
+
hotkeys.registerHotkey({
|
|
437
|
+
key: "ctrl+<",
|
|
438
|
+
action: () => decreaseHeadingLevelWithListHandling(),
|
|
439
|
+
description: "Decrease heading level",
|
|
440
|
+
group: "headings"
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
// Ctrl+> increases heading (P -> H6 -> H5 -> ... -> H1)
|
|
444
|
+
hotkeys.registerHotkey({
|
|
445
|
+
key: "ctrl+>",
|
|
446
|
+
action: () => increaseHeadingLevelWithListHandling(),
|
|
447
|
+
description: "Increase heading level",
|
|
448
|
+
group: "headings"
|
|
449
|
+
});
|
|
450
|
+
|
|
451
|
+
// === List Hotkeys ===
|
|
452
|
+
hotkeys.registerHotkey({
|
|
453
|
+
key: "ctrl+shift+[",
|
|
454
|
+
action: () => lists.toggleUnorderedList(),
|
|
455
|
+
description: "Toggle bullet list",
|
|
456
|
+
group: "lists"
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
hotkeys.registerHotkey({
|
|
460
|
+
key: "ctrl+shift+]",
|
|
461
|
+
action: () => lists.toggleOrderedList(),
|
|
462
|
+
description: "Toggle numbered list",
|
|
463
|
+
group: "lists"
|
|
464
|
+
});
|
|
465
|
+
|
|
466
|
+
// Tab/Shift+Tab for list indentation (registered for help display - actual handling is in onKeyDown)
|
|
467
|
+
hotkeys.registerHotkey({
|
|
468
|
+
key: "tab",
|
|
469
|
+
action: () => {}, // Handled in onKeyDown
|
|
470
|
+
description: "Indent list item",
|
|
471
|
+
group: "lists"
|
|
472
|
+
});
|
|
473
|
+
|
|
474
|
+
hotkeys.registerHotkey({
|
|
475
|
+
key: "shift+tab",
|
|
476
|
+
action: () => {}, // Handled in onKeyDown
|
|
477
|
+
description: "Outdent list item",
|
|
478
|
+
group: "lists"
|
|
479
|
+
});
|
|
480
|
+
|
|
481
|
+
// === Code Block Hotkeys ===
|
|
482
|
+
hotkeys.registerHotkey({
|
|
483
|
+
key: "ctrl+shift+k",
|
|
484
|
+
action: () => toggleCodeBlockWithListHandling(),
|
|
485
|
+
description: "Toggle code block",
|
|
486
|
+
group: "blocks"
|
|
487
|
+
});
|
|
488
|
+
|
|
489
|
+
// Exit code block (registered for help display - actual handling is in CodeViewer)
|
|
490
|
+
hotkeys.registerHotkey({
|
|
491
|
+
key: "ctrl+enter",
|
|
492
|
+
action: () => {}, // Handled by CodeViewer's onKeyDown
|
|
493
|
+
description: "Exit code block",
|
|
494
|
+
group: "blocks"
|
|
495
|
+
});
|
|
496
|
+
|
|
497
|
+
// Language cycling for code blocks (registered for help display - actual handling is in onKeyDown)
|
|
498
|
+
hotkeys.registerHotkey({
|
|
499
|
+
key: "ctrl+alt+l",
|
|
500
|
+
action: () => {}, // Handled in onKeyDown when in code block
|
|
501
|
+
description: "Cycle language (in code block)",
|
|
502
|
+
group: "blocks"
|
|
503
|
+
});
|
|
504
|
+
|
|
505
|
+
hotkeys.registerHotkey({
|
|
506
|
+
key: "ctrl+alt+shift+l",
|
|
507
|
+
action: () => {}, // Handled by CodeViewer
|
|
508
|
+
description: "Search language (in code block)",
|
|
509
|
+
group: "blocks"
|
|
510
|
+
});
|
|
511
|
+
|
|
512
|
+
// === Blockquote Hotkeys ===
|
|
513
|
+
hotkeys.registerHotkey({
|
|
514
|
+
key: "ctrl+shift+q",
|
|
515
|
+
action: () => blockquotes.toggleBlockquote(),
|
|
516
|
+
description: "Toggle blockquote",
|
|
517
|
+
group: "blocks"
|
|
518
|
+
});
|
|
519
|
+
|
|
520
|
+
// === Horizontal Rule Hotkey ===
|
|
521
|
+
hotkeys.registerHotkey({
|
|
522
|
+
key: "ctrl+shift+enter",
|
|
523
|
+
action: () => insertHorizontalRule(),
|
|
524
|
+
description: "Insert horizontal rule",
|
|
525
|
+
group: "blocks"
|
|
526
|
+
});
|
|
527
|
+
|
|
528
|
+
// === Link Hotkeys ===
|
|
529
|
+
hotkeys.registerHotkey({
|
|
530
|
+
key: "ctrl+k",
|
|
531
|
+
action: () => links.insertLink(),
|
|
532
|
+
description: "Insert/edit link",
|
|
533
|
+
group: "formatting"
|
|
534
|
+
});
|
|
535
|
+
|
|
536
|
+
// === Table Hotkeys ===
|
|
537
|
+
hotkeys.registerHotkey({
|
|
538
|
+
key: "ctrl+alt+shift+t",
|
|
539
|
+
action: () => tables.insertTable(),
|
|
540
|
+
description: "Insert table",
|
|
541
|
+
group: "tables"
|
|
542
|
+
});
|
|
543
|
+
|
|
544
|
+
// Table insert operations (only work when in table)
|
|
545
|
+
hotkeys.registerHotkey({
|
|
546
|
+
key: "ctrl+alt+shift+up",
|
|
547
|
+
action: () => { if (tables.isInTable()) tables.insertRowAbove(); },
|
|
548
|
+
description: "Insert row above",
|
|
549
|
+
group: "tables"
|
|
550
|
+
});
|
|
551
|
+
|
|
552
|
+
hotkeys.registerHotkey({
|
|
553
|
+
key: "ctrl+alt+shift+down",
|
|
554
|
+
action: () => { if (tables.isInTable()) tables.insertRowBelow(); },
|
|
555
|
+
description: "Insert row below",
|
|
556
|
+
group: "tables"
|
|
557
|
+
});
|
|
558
|
+
|
|
559
|
+
hotkeys.registerHotkey({
|
|
560
|
+
key: "ctrl+alt+shift+left",
|
|
561
|
+
action: () => { if (tables.isInTable()) tables.insertColumnLeft(); },
|
|
562
|
+
description: "Insert column left",
|
|
563
|
+
group: "tables"
|
|
564
|
+
});
|
|
565
|
+
|
|
566
|
+
hotkeys.registerHotkey({
|
|
567
|
+
key: "ctrl+alt+shift+right",
|
|
568
|
+
action: () => { if (tables.isInTable()) tables.insertColumnRight(); },
|
|
569
|
+
description: "Insert column right",
|
|
570
|
+
group: "tables"
|
|
571
|
+
});
|
|
572
|
+
|
|
573
|
+
// Table delete operations (only work when in table)
|
|
574
|
+
hotkeys.registerHotkey({
|
|
575
|
+
key: "ctrl+alt+backspace",
|
|
576
|
+
action: () => { if (tables.isInTable()) tables.deleteCurrentRow(); },
|
|
577
|
+
description: "Delete row",
|
|
578
|
+
group: "tables"
|
|
579
|
+
});
|
|
580
|
+
|
|
581
|
+
hotkeys.registerHotkey({
|
|
582
|
+
key: "ctrl+shift+backspace",
|
|
583
|
+
action: () => { if (tables.isInTable()) tables.deleteCurrentColumn(); },
|
|
584
|
+
description: "Delete column",
|
|
585
|
+
group: "tables"
|
|
586
|
+
});
|
|
587
|
+
|
|
588
|
+
hotkeys.registerHotkey({
|
|
589
|
+
key: "ctrl+alt+shift+backspace",
|
|
590
|
+
action: () => { if (tables.isInTable()) tables.deleteTable(); },
|
|
591
|
+
description: "Delete table",
|
|
592
|
+
group: "tables"
|
|
593
|
+
});
|
|
594
|
+
|
|
595
|
+
// Table alignment hotkeys (only work when in table)
|
|
596
|
+
hotkeys.registerHotkey({
|
|
597
|
+
key: "ctrl+alt+l",
|
|
598
|
+
action: () => { if (tables.isInTable()) tables.setColumnAlignmentLeft(); },
|
|
599
|
+
description: "Align column left",
|
|
600
|
+
group: "tables"
|
|
601
|
+
});
|
|
602
|
+
|
|
603
|
+
hotkeys.registerHotkey({
|
|
604
|
+
key: "ctrl+alt+c",
|
|
605
|
+
action: () => { if (tables.isInTable()) tables.setColumnAlignmentCenter(); },
|
|
606
|
+
description: "Align column center",
|
|
607
|
+
group: "tables"
|
|
608
|
+
});
|
|
609
|
+
|
|
610
|
+
hotkeys.registerHotkey({
|
|
611
|
+
key: "ctrl+alt+r",
|
|
612
|
+
action: () => { if (tables.isInTable()) tables.setColumnAlignmentRight(); },
|
|
613
|
+
description: "Align column right",
|
|
614
|
+
group: "tables"
|
|
615
|
+
});
|
|
616
|
+
|
|
617
|
+
// Help hotkey (Ctrl+? is handled specially in handleKeyDown)
|
|
618
|
+
// This registration is for the help display list
|
|
619
|
+
hotkeys.registerHotkey({
|
|
620
|
+
key: "ctrl+?",
|
|
621
|
+
action: () => {
|
|
622
|
+
isShowingHotkeyHelp.value = true;
|
|
623
|
+
},
|
|
624
|
+
description: "Show keyboard shortcuts",
|
|
625
|
+
group: "other"
|
|
626
|
+
});
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
/**
|
|
630
|
+
* Insert a horizontal rule after the current block element
|
|
631
|
+
* Creates an <hr> element followed by a new paragraph for continued editing
|
|
632
|
+
*/
|
|
633
|
+
function insertHorizontalRule(): void {
|
|
634
|
+
if (!contentRef.value) return;
|
|
635
|
+
|
|
636
|
+
const sel = window.getSelection();
|
|
637
|
+
if (!sel || sel.rangeCount === 0) return;
|
|
638
|
+
|
|
639
|
+
// Find the current block element containing the cursor
|
|
640
|
+
let node: Node | null = sel.getRangeAt(0).startContainer;
|
|
641
|
+
let blockElement: HTMLElement | null = null;
|
|
642
|
+
|
|
643
|
+
// Walk up to find a block-level element
|
|
644
|
+
while (node && node !== contentRef.value) {
|
|
645
|
+
const element = node as HTMLElement;
|
|
646
|
+
const tagName = element.tagName?.toUpperCase();
|
|
647
|
+
|
|
648
|
+
// Check if this is a block element (p, h1-h6, li, blockquote, etc.)
|
|
649
|
+
if (tagName === "P" || /^H[1-6]$/.test(tagName) || tagName === "LI" || tagName === "BLOCKQUOTE") {
|
|
650
|
+
blockElement = element;
|
|
651
|
+
break;
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
// Also check for code block wrapper
|
|
655
|
+
if (element.hasAttribute?.("data-code-block-id")) {
|
|
656
|
+
blockElement = element;
|
|
657
|
+
break;
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
node = element.parentElement;
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
// If no block element found, use the contentRef itself as reference
|
|
664
|
+
const insertAfter = blockElement || contentRef.value.lastElementChild;
|
|
665
|
+
|
|
666
|
+
if (!insertAfter) {
|
|
667
|
+
// Empty editor - just add hr and paragraph
|
|
668
|
+
const hr = document.createElement("hr");
|
|
669
|
+
const p = document.createElement("p");
|
|
670
|
+
p.appendChild(document.createElement("br"));
|
|
671
|
+
contentRef.value.appendChild(hr);
|
|
672
|
+
contentRef.value.appendChild(p);
|
|
673
|
+
} else {
|
|
674
|
+
// Insert hr after the current block
|
|
675
|
+
const hr = document.createElement("hr");
|
|
676
|
+
const p = document.createElement("p");
|
|
677
|
+
p.appendChild(document.createElement("br"));
|
|
678
|
+
|
|
679
|
+
// Insert after the block element (or its parent list if in a list item)
|
|
680
|
+
let insertionPoint: Element = insertAfter;
|
|
681
|
+
if (insertAfter.tagName?.toUpperCase() === "LI") {
|
|
682
|
+
// If in a list item, insert after the entire list
|
|
683
|
+
const parentList = insertAfter.closest("ul, ol");
|
|
684
|
+
if (parentList) {
|
|
685
|
+
insertionPoint = parentList;
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
insertionPoint.parentNode?.insertBefore(hr, insertionPoint.nextSibling);
|
|
690
|
+
hr.parentNode?.insertBefore(p, hr.nextSibling);
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
// Position cursor in the new paragraph
|
|
694
|
+
nextTick(() => {
|
|
695
|
+
const newParagraph = contentRef.value?.querySelector("hr + p");
|
|
696
|
+
if (newParagraph) {
|
|
697
|
+
const range = document.createRange();
|
|
698
|
+
range.selectNodeContents(newParagraph);
|
|
699
|
+
range.collapse(true);
|
|
700
|
+
const newSel = window.getSelection();
|
|
701
|
+
newSel?.removeAllRanges();
|
|
702
|
+
newSel?.addRange(range);
|
|
703
|
+
}
|
|
704
|
+
});
|
|
705
|
+
|
|
706
|
+
sync.debouncedSyncFromHtml();
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
/**
|
|
710
|
+
* Insert a tab character at the current cursor position
|
|
711
|
+
*/
|
|
712
|
+
function insertTabCharacter(): void {
|
|
713
|
+
if (!contentRef.value) return;
|
|
714
|
+
|
|
715
|
+
const sel = window.getSelection();
|
|
716
|
+
if (!sel || sel.rangeCount === 0) return;
|
|
717
|
+
|
|
718
|
+
const range = sel.getRangeAt(0);
|
|
719
|
+
range.deleteContents();
|
|
720
|
+
|
|
721
|
+
const tabNode = document.createTextNode("\t");
|
|
722
|
+
range.insertNode(tabNode);
|
|
723
|
+
|
|
724
|
+
// Position cursor AFTER the tab node
|
|
725
|
+
range.setStartAfter(tabNode);
|
|
726
|
+
range.setEndAfter(tabNode);
|
|
727
|
+
|
|
728
|
+
sel.removeAllRanges();
|
|
729
|
+
sel.addRange(range);
|
|
730
|
+
|
|
731
|
+
// Trigger content sync AFTER cursor is positioned
|
|
732
|
+
sync.debouncedSyncFromHtml();
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
/**
|
|
736
|
+
* Handle input events from contenteditable
|
|
737
|
+
* Checks for markdown patterns (e.g., "# " for headings, "- " for lists) and converts immediately
|
|
738
|
+
* Note: Code fence patterns (```) are only converted on Enter key press, not on input
|
|
739
|
+
*/
|
|
740
|
+
function onInput(): void {
|
|
741
|
+
// Check for heading pattern (e.g., "# " -> H1)
|
|
742
|
+
let converted = headings.checkAndConvertHeadingPattern();
|
|
743
|
+
|
|
744
|
+
// Check for list pattern (e.g., "- " -> ul, "1. " -> ol)
|
|
745
|
+
if (!converted) {
|
|
746
|
+
converted = lists.checkAndConvertListPattern();
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
// If a pattern was converted, the content change callback already triggers sync
|
|
750
|
+
// Otherwise, sync as normal
|
|
751
|
+
if (!converted) {
|
|
752
|
+
sync.debouncedSyncFromHtml();
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
/**
|
|
757
|
+
* Check if cursor is at the start of a block element (paragraph, heading, etc.)
|
|
758
|
+
* Returns the block element if cursor is at position 0, null otherwise
|
|
759
|
+
*/
|
|
760
|
+
function getCursorBlockAtStart(): HTMLElement | null {
|
|
761
|
+
const sel = window.getSelection();
|
|
762
|
+
if (!sel || sel.rangeCount === 0) return null;
|
|
763
|
+
|
|
764
|
+
const range = sel.getRangeAt(0);
|
|
765
|
+
|
|
766
|
+
// Check if range is collapsed (no selection, just cursor)
|
|
767
|
+
if (!range.collapsed) return null;
|
|
768
|
+
|
|
769
|
+
// Get the cursor's offset - must be at position 0
|
|
770
|
+
if (range.startOffset !== 0) return null;
|
|
771
|
+
|
|
772
|
+
// Find the containing block element
|
|
773
|
+
let node: Node | null = range.startContainer;
|
|
774
|
+
|
|
775
|
+
// If we're in a text node, check if we're at the very beginning
|
|
776
|
+
if (node.nodeType === Node.TEXT_NODE) {
|
|
777
|
+
// Must be at position 0 of the text node
|
|
778
|
+
if (range.startOffset !== 0) return null;
|
|
779
|
+
|
|
780
|
+
// Check if this text node is the first content in its parent block
|
|
781
|
+
const parent = node.parentElement;
|
|
782
|
+
if (!parent) return null;
|
|
783
|
+
|
|
784
|
+
// Walk up to find a block element
|
|
785
|
+
node = parent;
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
// Find the block-level element (p, h1-h6, etc.)
|
|
789
|
+
while (node && node !== contentRef.value) {
|
|
790
|
+
const element = node as HTMLElement;
|
|
791
|
+
const tagName = element.tagName?.toUpperCase();
|
|
792
|
+
|
|
793
|
+
// Check if this is a block element
|
|
794
|
+
if (tagName === "P" || /^H[1-6]$/.test(tagName)) {
|
|
795
|
+
// Verify cursor is truly at the start by checking the range position
|
|
796
|
+
const blockRange = document.createRange();
|
|
797
|
+
blockRange.selectNodeContents(element);
|
|
798
|
+
blockRange.collapse(true);
|
|
799
|
+
|
|
800
|
+
// Compare the cursor position with the block start
|
|
801
|
+
if (range.compareBoundaryPoints(Range.START_TO_START, blockRange) === 0) {
|
|
802
|
+
return element;
|
|
803
|
+
}
|
|
804
|
+
return null;
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
node = element.parentElement;
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
return null;
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
/**
|
|
814
|
+
* Handle Backspace at the start of a paragraph after a code block
|
|
815
|
+
* Moves cursor into the code block instead of deleting
|
|
816
|
+
*/
|
|
817
|
+
function handleBackspaceIntoCodeBlock(): boolean {
|
|
818
|
+
const block = getCursorBlockAtStart();
|
|
819
|
+
if (!block) return false;
|
|
820
|
+
|
|
821
|
+
// Check if the previous sibling is a code block wrapper
|
|
822
|
+
const previousSibling = block.previousElementSibling;
|
|
823
|
+
if (!previousSibling?.hasAttribute("data-code-block-id")) return false;
|
|
824
|
+
|
|
825
|
+
// Check if the current block is empty (only contains <br> or whitespace)
|
|
826
|
+
const isEmpty = !block.textContent?.trim();
|
|
827
|
+
|
|
828
|
+
// Find the CodeViewer's contenteditable pre element inside the wrapper
|
|
829
|
+
const codeViewerPre = previousSibling.querySelector("pre[contenteditable='true']");
|
|
830
|
+
if (!codeViewerPre) return false;
|
|
831
|
+
|
|
832
|
+
// If the paragraph is empty, remove it
|
|
833
|
+
if (isEmpty) {
|
|
834
|
+
block.remove();
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
// Focus the CodeViewer and position cursor at the end
|
|
838
|
+
nextTick(() => {
|
|
839
|
+
(codeViewerPre as HTMLElement).focus();
|
|
840
|
+
|
|
841
|
+
// Position cursor at the end of the code content
|
|
842
|
+
const selection = window.getSelection();
|
|
843
|
+
if (selection) {
|
|
844
|
+
const range = document.createRange();
|
|
845
|
+
range.selectNodeContents(codeViewerPre);
|
|
846
|
+
range.collapse(false); // Collapse to end
|
|
847
|
+
selection.removeAllRanges();
|
|
848
|
+
selection.addRange(range);
|
|
849
|
+
}
|
|
850
|
+
});
|
|
851
|
+
|
|
852
|
+
sync.debouncedSyncFromHtml();
|
|
853
|
+
return true;
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
/**
|
|
857
|
+
* Handle keydown events
|
|
858
|
+
* Handles Enter for code block continuation/exit and list continuation, Tab/Shift+Tab for indentation
|
|
859
|
+
*/
|
|
860
|
+
function onKeyDown(event: KeyboardEvent): void {
|
|
861
|
+
// Don't handle events that originate from inside code block wrappers
|
|
862
|
+
// (CodeViewer handles its own keyboard events like Ctrl+A for select-all)
|
|
863
|
+
const target = event.target as HTMLElement;
|
|
864
|
+
const isInCodeBlock = target.closest("[data-code-block-id]");
|
|
865
|
+
|
|
866
|
+
// SPECIAL CASE: Handle Ctrl+Alt+L for code block language cycling
|
|
867
|
+
// This is a fallback in case the CodeViewer's handler doesn't receive the event
|
|
868
|
+
// (Can happen due to event propagation issues in nested contenteditable elements)
|
|
869
|
+
const isCtrlAltL = (event.ctrlKey || event.metaKey) && event.altKey && event.key.toLowerCase() === "l";
|
|
870
|
+
|
|
871
|
+
if (isInCodeBlock && isCtrlAltL) {
|
|
872
|
+
event.preventDefault();
|
|
873
|
+
event.stopPropagation();
|
|
874
|
+
|
|
875
|
+
// Find the code block ID and cycle its language
|
|
876
|
+
const wrapper = target.closest("[data-code-block-id]");
|
|
877
|
+
const codeBlockId = wrapper?.getAttribute("data-code-block-id");
|
|
878
|
+
|
|
879
|
+
if (codeBlockId) {
|
|
880
|
+
const state = codeBlocks.codeBlocks.get(codeBlockId);
|
|
881
|
+
|
|
882
|
+
if (state) {
|
|
883
|
+
// Cycle through available formats based on current language
|
|
884
|
+
const currentLang = state.language || "yaml";
|
|
885
|
+
let nextLang: string;
|
|
886
|
+
|
|
887
|
+
if (currentLang === "json" || currentLang === "yaml") {
|
|
888
|
+
// Cycle: yaml -> json -> yaml (YAML/JSON only)
|
|
889
|
+
nextLang = currentLang === "yaml" ? "json" : "yaml";
|
|
890
|
+
} else if (currentLang === "text" || currentLang === "markdown") {
|
|
891
|
+
// Cycle: text -> markdown -> text
|
|
892
|
+
nextLang = currentLang === "text" ? "markdown" : "text";
|
|
893
|
+
} else {
|
|
894
|
+
// For other languages, don't cycle
|
|
895
|
+
nextLang = currentLang;
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
if (nextLang !== currentLang) {
|
|
899
|
+
codeBlocks.updateCodeBlockLanguage(codeBlockId, nextLang);
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
return;
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
if (isInCodeBlock) {
|
|
907
|
+
return;
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
// Handle Backspace at the start of a paragraph after a code block
|
|
911
|
+
if (event.key === "Backspace" && !event.shiftKey && !event.ctrlKey && !event.altKey && !event.metaKey) {
|
|
912
|
+
if (handleBackspaceIntoCodeBlock()) {
|
|
913
|
+
event.preventDefault();
|
|
914
|
+
return;
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
// Handle arrow keys in tables (without modifiers for simple navigation)
|
|
919
|
+
if ((event.key === "ArrowUp" || event.key === "ArrowDown") &&
|
|
920
|
+
!event.ctrlKey && !event.altKey && !event.shiftKey && !event.metaKey) {
|
|
921
|
+
if (tables.isInTable()) {
|
|
922
|
+
// Always navigate to the same column in the adjacent row
|
|
923
|
+
const handled = event.key === "ArrowUp"
|
|
924
|
+
? tables.navigateToCellAbove()
|
|
925
|
+
: tables.navigateToCellBelow();
|
|
926
|
+
if (handled) {
|
|
927
|
+
event.preventDefault();
|
|
928
|
+
return;
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
// Handle Enter key for code block, table, and list continuation
|
|
934
|
+
if (event.key === "Enter" && !event.shiftKey && !event.ctrlKey && !event.altKey && !event.metaKey) {
|
|
935
|
+
// Check for code fence pattern first (e.g., "```javascript" -> code block)
|
|
936
|
+
// This triggers only on Enter, allowing user to type full language before conversion
|
|
937
|
+
const convertedToCodeBlock = codeBlocks.checkAndConvertCodeBlockPattern();
|
|
938
|
+
if (convertedToCodeBlock) {
|
|
939
|
+
event.preventDefault();
|
|
940
|
+
return;
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
// Check existing code block - Enter inserts newline, or exits after double-Enter at end
|
|
944
|
+
const handledByCodeBlock = codeBlocks.handleCodeBlockEnter();
|
|
945
|
+
if (handledByCodeBlock) {
|
|
946
|
+
event.preventDefault();
|
|
947
|
+
return;
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
// Check if in a table - Enter moves to cell below or creates new row
|
|
951
|
+
if (tables.isInTable()) {
|
|
952
|
+
event.preventDefault();
|
|
953
|
+
tables.handleTableEnter();
|
|
954
|
+
return;
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
// Then check lists
|
|
958
|
+
const handled = lists.handleListEnter();
|
|
959
|
+
if (handled) {
|
|
960
|
+
event.preventDefault();
|
|
961
|
+
return;
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
// Handle Tab key - always prevent default to keep focus in editor
|
|
966
|
+
if (event.key === "Tab" && !event.ctrlKey && !event.altKey && !event.metaKey) {
|
|
967
|
+
event.preventDefault();
|
|
968
|
+
|
|
969
|
+
// Check if in a table first - Tab navigates between cells
|
|
970
|
+
if (tables.isInTable()) {
|
|
971
|
+
tables.handleTableTab(event.shiftKey);
|
|
972
|
+
return;
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
if (event.shiftKey) {
|
|
976
|
+
// Shift+Tab - outdent if in list, otherwise do nothing
|
|
977
|
+
lists.outdentListItem();
|
|
978
|
+
} else {
|
|
979
|
+
// Tab - indent if in list, otherwise insert tab character
|
|
980
|
+
const handled = lists.indentListItem();
|
|
981
|
+
if (!handled) {
|
|
982
|
+
// Not in a list - insert a tab character at cursor position
|
|
983
|
+
insertTabCharacter();
|
|
984
|
+
}
|
|
985
|
+
}
|
|
986
|
+
return;
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
// Let hotkeys handle other keys - if not handled, default browser behavior occurs
|
|
990
|
+
hotkeys.handleKeyDown(event);
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
/**
|
|
994
|
+
* Handle blur events - sync immediately
|
|
995
|
+
*/
|
|
996
|
+
function onBlur(): void {
|
|
997
|
+
sync.syncFromHtml();
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
/**
|
|
1001
|
+
* Set markdown content from external source
|
|
1002
|
+
*/
|
|
1003
|
+
function setMarkdown(markdown: string): void {
|
|
1004
|
+
sync.syncFromMarkdown(markdown);
|
|
1005
|
+
|
|
1006
|
+
// Update the contenteditable element
|
|
1007
|
+
nextTick(() => {
|
|
1008
|
+
if (contentRef.value) {
|
|
1009
|
+
contentRef.value.innerHTML = sync.renderedHtml.value;
|
|
1010
|
+
}
|
|
1011
|
+
});
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
/**
|
|
1015
|
+
* Show hotkey help popover
|
|
1016
|
+
*/
|
|
1017
|
+
function showHotkeyHelp(): void {
|
|
1018
|
+
isShowingHotkeyHelp.value = true;
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
/**
|
|
1022
|
+
* Hide hotkey help popover
|
|
1023
|
+
*/
|
|
1024
|
+
function hideHotkeyHelp(): void {
|
|
1025
|
+
isShowingHotkeyHelp.value = false;
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
// Initialize with initial value
|
|
1029
|
+
if (initialValue) {
|
|
1030
|
+
sync.syncFromMarkdown(initialValue);
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
return {
|
|
1034
|
+
// From sync
|
|
1035
|
+
renderedHtml: sync.renderedHtml,
|
|
1036
|
+
isInternalUpdate: sync.isInternalUpdate,
|
|
1037
|
+
|
|
1038
|
+
// State
|
|
1039
|
+
isShowingHotkeyHelp,
|
|
1040
|
+
charCount,
|
|
1041
|
+
|
|
1042
|
+
// Event handlers
|
|
1043
|
+
onInput,
|
|
1044
|
+
onKeyDown,
|
|
1045
|
+
onBlur,
|
|
1046
|
+
|
|
1047
|
+
// External value updates
|
|
1048
|
+
setMarkdown,
|
|
1049
|
+
|
|
1050
|
+
// Formatting actions
|
|
1051
|
+
insertHorizontalRule,
|
|
1052
|
+
|
|
1053
|
+
// Hotkey help
|
|
1054
|
+
showHotkeyHelp,
|
|
1055
|
+
hideHotkeyHelp,
|
|
1056
|
+
hotkeyDefinitions,
|
|
1057
|
+
|
|
1058
|
+
// Feature access
|
|
1059
|
+
headings,
|
|
1060
|
+
inlineFormatting,
|
|
1061
|
+
links,
|
|
1062
|
+
lists,
|
|
1063
|
+
codeBlocks,
|
|
1064
|
+
codeBlockManager,
|
|
1065
|
+
blockquotes,
|
|
1066
|
+
tables
|
|
1067
|
+
};
|
|
1068
|
+
}
|