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,791 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
2
|
+
import { ref } from "vue";
|
|
3
|
+
import {
|
|
4
|
+
parseKeyCombo,
|
|
5
|
+
matchesKeyCombo,
|
|
6
|
+
useMarkdownHotkeys,
|
|
7
|
+
ParsedKey,
|
|
8
|
+
HotkeyDefinition
|
|
9
|
+
} from "./useMarkdownHotkeys";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Helper to create a mock KeyboardEvent with specified properties
|
|
13
|
+
*/
|
|
14
|
+
function createKeyboardEvent(options: {
|
|
15
|
+
key: string;
|
|
16
|
+
code?: string;
|
|
17
|
+
ctrlKey?: boolean;
|
|
18
|
+
shiftKey?: boolean;
|
|
19
|
+
altKey?: boolean;
|
|
20
|
+
metaKey?: boolean;
|
|
21
|
+
}): KeyboardEvent {
|
|
22
|
+
return {
|
|
23
|
+
key: options.key,
|
|
24
|
+
code: options.code || "",
|
|
25
|
+
ctrlKey: options.ctrlKey || false,
|
|
26
|
+
shiftKey: options.shiftKey || false,
|
|
27
|
+
altKey: options.altKey || false,
|
|
28
|
+
metaKey: options.metaKey || false,
|
|
29
|
+
preventDefault: vi.fn()
|
|
30
|
+
} as unknown as KeyboardEvent;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
describe("useMarkdownHotkeys", () => {
|
|
34
|
+
describe("parseKeyCombo", () => {
|
|
35
|
+
it("parses simple key without modifiers", () => {
|
|
36
|
+
const result = parseKeyCombo("a");
|
|
37
|
+
expect(result).toEqual({
|
|
38
|
+
key: "a",
|
|
39
|
+
ctrl: false,
|
|
40
|
+
shift: false,
|
|
41
|
+
alt: false,
|
|
42
|
+
meta: false
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("parses ctrl+key combination", () => {
|
|
47
|
+
const result = parseKeyCombo("ctrl+b");
|
|
48
|
+
expect(result).toEqual({
|
|
49
|
+
key: "b",
|
|
50
|
+
ctrl: true,
|
|
51
|
+
shift: false,
|
|
52
|
+
alt: false,
|
|
53
|
+
meta: false
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("parses control as alias for ctrl", () => {
|
|
58
|
+
const result = parseKeyCombo("control+b");
|
|
59
|
+
expect(result).toEqual({
|
|
60
|
+
key: "b",
|
|
61
|
+
ctrl: true,
|
|
62
|
+
shift: false,
|
|
63
|
+
alt: false,
|
|
64
|
+
meta: false
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("parses shift+key combination", () => {
|
|
69
|
+
const result = parseKeyCombo("shift+a");
|
|
70
|
+
expect(result).toEqual({
|
|
71
|
+
key: "a",
|
|
72
|
+
ctrl: false,
|
|
73
|
+
shift: true,
|
|
74
|
+
alt: false,
|
|
75
|
+
meta: false
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("parses alt+key combination", () => {
|
|
80
|
+
const result = parseKeyCombo("alt+x");
|
|
81
|
+
expect(result).toEqual({
|
|
82
|
+
key: "x",
|
|
83
|
+
ctrl: false,
|
|
84
|
+
shift: false,
|
|
85
|
+
alt: true,
|
|
86
|
+
meta: false
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it("parses option as alias for alt (Mac)", () => {
|
|
91
|
+
const result = parseKeyCombo("option+x");
|
|
92
|
+
expect(result).toEqual({
|
|
93
|
+
key: "x",
|
|
94
|
+
ctrl: false,
|
|
95
|
+
shift: false,
|
|
96
|
+
alt: true,
|
|
97
|
+
meta: false
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it("parses meta+key combination", () => {
|
|
102
|
+
const result = parseKeyCombo("meta+s");
|
|
103
|
+
expect(result).toEqual({
|
|
104
|
+
key: "s",
|
|
105
|
+
ctrl: false,
|
|
106
|
+
shift: false,
|
|
107
|
+
alt: false,
|
|
108
|
+
meta: true
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it("parses cmd as alias for meta (Mac)", () => {
|
|
113
|
+
const result = parseKeyCombo("cmd+s");
|
|
114
|
+
expect(result).toEqual({
|
|
115
|
+
key: "s",
|
|
116
|
+
ctrl: false,
|
|
117
|
+
shift: false,
|
|
118
|
+
alt: false,
|
|
119
|
+
meta: true
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it("parses command as alias for meta (Mac)", () => {
|
|
124
|
+
const result = parseKeyCombo("command+s");
|
|
125
|
+
expect(result).toEqual({
|
|
126
|
+
key: "s",
|
|
127
|
+
ctrl: false,
|
|
128
|
+
shift: false,
|
|
129
|
+
alt: false,
|
|
130
|
+
meta: true
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it("parses win as alias for meta (Windows)", () => {
|
|
135
|
+
const result = parseKeyCombo("win+e");
|
|
136
|
+
expect(result).toEqual({
|
|
137
|
+
key: "e",
|
|
138
|
+
ctrl: false,
|
|
139
|
+
shift: false,
|
|
140
|
+
alt: false,
|
|
141
|
+
meta: true
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it("parses windows as alias for meta (Windows)", () => {
|
|
146
|
+
const result = parseKeyCombo("windows+e");
|
|
147
|
+
expect(result).toEqual({
|
|
148
|
+
key: "e",
|
|
149
|
+
ctrl: false,
|
|
150
|
+
shift: false,
|
|
151
|
+
alt: false,
|
|
152
|
+
meta: true
|
|
153
|
+
});
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it("parses ctrl+shift+key combination", () => {
|
|
157
|
+
const result = parseKeyCombo("ctrl+shift+b");
|
|
158
|
+
expect(result).toEqual({
|
|
159
|
+
key: "b",
|
|
160
|
+
ctrl: true,
|
|
161
|
+
shift: true,
|
|
162
|
+
alt: false,
|
|
163
|
+
meta: false
|
|
164
|
+
});
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it("parses ctrl+alt+key combination", () => {
|
|
168
|
+
const result = parseKeyCombo("ctrl+alt+d");
|
|
169
|
+
expect(result).toEqual({
|
|
170
|
+
key: "d",
|
|
171
|
+
ctrl: true,
|
|
172
|
+
shift: false,
|
|
173
|
+
alt: true,
|
|
174
|
+
meta: false
|
|
175
|
+
});
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it("parses all modifiers together", () => {
|
|
179
|
+
const result = parseKeyCombo("ctrl+shift+alt+meta+x");
|
|
180
|
+
expect(result).toEqual({
|
|
181
|
+
key: "x",
|
|
182
|
+
ctrl: true,
|
|
183
|
+
shift: true,
|
|
184
|
+
alt: true,
|
|
185
|
+
meta: true
|
|
186
|
+
});
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it("handles uppercase input (normalizes to lowercase)", () => {
|
|
190
|
+
const result = parseKeyCombo("CTRL+SHIFT+B");
|
|
191
|
+
expect(result).toEqual({
|
|
192
|
+
key: "b",
|
|
193
|
+
ctrl: true,
|
|
194
|
+
shift: true,
|
|
195
|
+
alt: false,
|
|
196
|
+
meta: false
|
|
197
|
+
});
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it("parses number keys", () => {
|
|
201
|
+
const result = parseKeyCombo("ctrl+1");
|
|
202
|
+
expect(result).toEqual({
|
|
203
|
+
key: "1",
|
|
204
|
+
ctrl: true,
|
|
205
|
+
shift: false,
|
|
206
|
+
alt: false,
|
|
207
|
+
meta: false
|
|
208
|
+
});
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
it("parses bracket key [", () => {
|
|
212
|
+
const result = parseKeyCombo("ctrl+shift+[");
|
|
213
|
+
expect(result).toEqual({
|
|
214
|
+
key: "[",
|
|
215
|
+
ctrl: true,
|
|
216
|
+
shift: true,
|
|
217
|
+
alt: false,
|
|
218
|
+
meta: false
|
|
219
|
+
});
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
it("parses bracket key ]", () => {
|
|
223
|
+
const result = parseKeyCombo("ctrl+shift+]");
|
|
224
|
+
expect(result).toEqual({
|
|
225
|
+
key: "]",
|
|
226
|
+
ctrl: true,
|
|
227
|
+
shift: true,
|
|
228
|
+
alt: false,
|
|
229
|
+
meta: false
|
|
230
|
+
});
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
it("parses special characters", () => {
|
|
234
|
+
const result = parseKeyCombo("ctrl+/");
|
|
235
|
+
expect(result).toEqual({
|
|
236
|
+
key: "/",
|
|
237
|
+
ctrl: true,
|
|
238
|
+
shift: false,
|
|
239
|
+
alt: false,
|
|
240
|
+
meta: false
|
|
241
|
+
});
|
|
242
|
+
});
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
describe("matchesKeyCombo", () => {
|
|
246
|
+
describe("basic key matching", () => {
|
|
247
|
+
it("matches simple key press", () => {
|
|
248
|
+
const event = createKeyboardEvent({ key: "a" });
|
|
249
|
+
const parsed = parseKeyCombo("a");
|
|
250
|
+
expect(matchesKeyCombo(event, parsed)).toBe(true);
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
it("does not match wrong key", () => {
|
|
254
|
+
const event = createKeyboardEvent({ key: "b" });
|
|
255
|
+
const parsed = parseKeyCombo("a");
|
|
256
|
+
expect(matchesKeyCombo(event, parsed)).toBe(false);
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
it("matches case-insensitively", () => {
|
|
260
|
+
const event = createKeyboardEvent({ key: "A" });
|
|
261
|
+
const parsed = parseKeyCombo("a");
|
|
262
|
+
expect(matchesKeyCombo(event, parsed)).toBe(true);
|
|
263
|
+
});
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
describe("modifier key matching", () => {
|
|
267
|
+
it("matches ctrl+key on Windows/Linux", () => {
|
|
268
|
+
const event = createKeyboardEvent({ key: "b", ctrlKey: true });
|
|
269
|
+
const parsed = parseKeyCombo("ctrl+b");
|
|
270
|
+
expect(matchesKeyCombo(event, parsed)).toBe(true);
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
it("does not match when ctrl is expected but not pressed", () => {
|
|
274
|
+
const event = createKeyboardEvent({ key: "b", ctrlKey: false });
|
|
275
|
+
const parsed = parseKeyCombo("ctrl+b");
|
|
276
|
+
expect(matchesKeyCombo(event, parsed)).toBe(false);
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
it("does not match when extra modifiers are pressed", () => {
|
|
280
|
+
const event = createKeyboardEvent({ key: "b", ctrlKey: true, shiftKey: true });
|
|
281
|
+
const parsed = parseKeyCombo("ctrl+b");
|
|
282
|
+
expect(matchesKeyCombo(event, parsed)).toBe(false);
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
it("matches shift+key", () => {
|
|
286
|
+
const event = createKeyboardEvent({ key: "a", shiftKey: true });
|
|
287
|
+
const parsed = parseKeyCombo("shift+a");
|
|
288
|
+
expect(matchesKeyCombo(event, parsed)).toBe(true);
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
it("matches alt+key", () => {
|
|
292
|
+
const event = createKeyboardEvent({ key: "x", altKey: true });
|
|
293
|
+
const parsed = parseKeyCombo("alt+x");
|
|
294
|
+
expect(matchesKeyCombo(event, parsed)).toBe(true);
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
it("matches ctrl+shift+key", () => {
|
|
298
|
+
const event = createKeyboardEvent({ key: "b", ctrlKey: true, shiftKey: true });
|
|
299
|
+
const parsed = parseKeyCombo("ctrl+shift+b");
|
|
300
|
+
expect(matchesKeyCombo(event, parsed)).toBe(true);
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
it("matches meta+key (for explicit meta hotkeys)", () => {
|
|
304
|
+
const event = createKeyboardEvent({ key: "s", metaKey: true });
|
|
305
|
+
const parsed = parseKeyCombo("meta+s");
|
|
306
|
+
expect(matchesKeyCombo(event, parsed)).toBe(true);
|
|
307
|
+
});
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
describe("number key matching", () => {
|
|
311
|
+
it("matches ctrl+1", () => {
|
|
312
|
+
const event = createKeyboardEvent({ key: "1", code: "Digit1", ctrlKey: true });
|
|
313
|
+
const parsed = parseKeyCombo("ctrl+1");
|
|
314
|
+
expect(matchesKeyCombo(event, parsed)).toBe(true);
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
it("matches ctrl+2", () => {
|
|
318
|
+
const event = createKeyboardEvent({ key: "2", code: "Digit2", ctrlKey: true });
|
|
319
|
+
const parsed = parseKeyCombo("ctrl+2");
|
|
320
|
+
expect(matchesKeyCombo(event, parsed)).toBe(true);
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
it("matches ctrl+3", () => {
|
|
324
|
+
const event = createKeyboardEvent({ key: "3", code: "Digit3", ctrlKey: true });
|
|
325
|
+
const parsed = parseKeyCombo("ctrl+3");
|
|
326
|
+
expect(matchesKeyCombo(event, parsed)).toBe(true);
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
it("matches ctrl+4", () => {
|
|
330
|
+
const event = createKeyboardEvent({ key: "4", code: "Digit4", ctrlKey: true });
|
|
331
|
+
const parsed = parseKeyCombo("ctrl+4");
|
|
332
|
+
expect(matchesKeyCombo(event, parsed)).toBe(true);
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
it("matches ctrl+5", () => {
|
|
336
|
+
const event = createKeyboardEvent({ key: "5", code: "Digit5", ctrlKey: true });
|
|
337
|
+
const parsed = parseKeyCombo("ctrl+5");
|
|
338
|
+
expect(matchesKeyCombo(event, parsed)).toBe(true);
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
it("matches ctrl+6", () => {
|
|
342
|
+
const event = createKeyboardEvent({ key: "6", code: "Digit6", ctrlKey: true });
|
|
343
|
+
const parsed = parseKeyCombo("ctrl+6");
|
|
344
|
+
expect(matchesKeyCombo(event, parsed)).toBe(true);
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
it("matches numpad keys", () => {
|
|
348
|
+
const event = createKeyboardEvent({ key: "1", code: "Numpad1", ctrlKey: true });
|
|
349
|
+
const parsed = parseKeyCombo("ctrl+1");
|
|
350
|
+
expect(matchesKeyCombo(event, parsed)).toBe(true);
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
it("does not match wrong number", () => {
|
|
354
|
+
const event = createKeyboardEvent({ key: "2", code: "Digit2", ctrlKey: true });
|
|
355
|
+
const parsed = parseKeyCombo("ctrl+1");
|
|
356
|
+
expect(matchesKeyCombo(event, parsed)).toBe(false);
|
|
357
|
+
});
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
describe("shifted key handling", () => {
|
|
361
|
+
it("matches > when browser reports > directly", () => {
|
|
362
|
+
// Browser reports the shifted character
|
|
363
|
+
const event = createKeyboardEvent({ key: ">", shiftKey: true, ctrlKey: true });
|
|
364
|
+
const parsed = parseKeyCombo("ctrl+>");
|
|
365
|
+
expect(matchesKeyCombo(event, parsed)).toBe(true);
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
it("matches < when browser reports < directly", () => {
|
|
369
|
+
const event = createKeyboardEvent({ key: "<", shiftKey: true, ctrlKey: true });
|
|
370
|
+
const parsed = parseKeyCombo("ctrl+<");
|
|
371
|
+
expect(matchesKeyCombo(event, parsed)).toBe(true);
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
it("matches ? when browser reports ? directly", () => {
|
|
375
|
+
const event = createKeyboardEvent({ key: "?", shiftKey: true, ctrlKey: true });
|
|
376
|
+
const parsed = parseKeyCombo("ctrl+?");
|
|
377
|
+
expect(matchesKeyCombo(event, parsed)).toBe(true);
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
it("matches > via shift+. combination", () => {
|
|
381
|
+
// Browser reports the base key with shift
|
|
382
|
+
const event = createKeyboardEvent({ key: ".", shiftKey: true, ctrlKey: true });
|
|
383
|
+
const parsed = parseKeyCombo("ctrl+>");
|
|
384
|
+
expect(matchesKeyCombo(event, parsed)).toBe(true);
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
it("matches < via shift+, combination", () => {
|
|
388
|
+
const event = createKeyboardEvent({ key: ",", shiftKey: true, ctrlKey: true });
|
|
389
|
+
const parsed = parseKeyCombo("ctrl+<");
|
|
390
|
+
expect(matchesKeyCombo(event, parsed)).toBe(true);
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
it("matches ? via shift+/ combination", () => {
|
|
394
|
+
const event = createKeyboardEvent({ key: "/", shiftKey: true, ctrlKey: true });
|
|
395
|
+
const parsed = parseKeyCombo("ctrl+?");
|
|
396
|
+
expect(matchesKeyCombo(event, parsed)).toBe(true);
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
it("matches ! when browser reports !", () => {
|
|
400
|
+
const event = createKeyboardEvent({ key: "!", shiftKey: true, ctrlKey: true });
|
|
401
|
+
const parsed = parseKeyCombo("ctrl+!");
|
|
402
|
+
expect(matchesKeyCombo(event, parsed)).toBe(true);
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
it("matches @ when browser reports @", () => {
|
|
406
|
+
const event = createKeyboardEvent({ key: "@", shiftKey: true, ctrlKey: true });
|
|
407
|
+
const parsed = parseKeyCombo("ctrl+@");
|
|
408
|
+
expect(matchesKeyCombo(event, parsed)).toBe(true);
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
it("matches # when browser reports #", () => {
|
|
412
|
+
const event = createKeyboardEvent({ key: "#", shiftKey: true, ctrlKey: true });
|
|
413
|
+
const parsed = parseKeyCombo("ctrl+#");
|
|
414
|
+
expect(matchesKeyCombo(event, parsed)).toBe(true);
|
|
415
|
+
});
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
describe("bracket hotkeys (ctrl+shift+[ and ctrl+shift+])", () => {
|
|
419
|
+
it("matches ctrl+shift+[ when browser reports { (shifted character)", () => {
|
|
420
|
+
// When pressing Ctrl+Shift+[, browser reports key as "{"
|
|
421
|
+
const event = createKeyboardEvent({ key: "{", shiftKey: true, ctrlKey: true });
|
|
422
|
+
const parsed = parseKeyCombo("ctrl+shift+[");
|
|
423
|
+
expect(matchesKeyCombo(event, parsed)).toBe(true);
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
it("matches ctrl+shift+] when browser reports } (shifted character)", () => {
|
|
427
|
+
// When pressing Ctrl+Shift+], browser reports key as "}"
|
|
428
|
+
const event = createKeyboardEvent({ key: "}", shiftKey: true, ctrlKey: true });
|
|
429
|
+
const parsed = parseKeyCombo("ctrl+shift+]");
|
|
430
|
+
expect(matchesKeyCombo(event, parsed)).toBe(true);
|
|
431
|
+
});
|
|
432
|
+
|
|
433
|
+
it("matches ctrl+shift+[ when browser reports [ with shift", () => {
|
|
434
|
+
// Alternative: browser might report base key with shift modifier
|
|
435
|
+
const event = createKeyboardEvent({ key: "[", shiftKey: true, ctrlKey: true });
|
|
436
|
+
const parsed = parseKeyCombo("ctrl+shift+[");
|
|
437
|
+
expect(matchesKeyCombo(event, parsed)).toBe(true);
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
it("matches ctrl+shift+] when browser reports ] with shift", () => {
|
|
441
|
+
const event = createKeyboardEvent({ key: "]", shiftKey: true, ctrlKey: true });
|
|
442
|
+
const parsed = parseKeyCombo("ctrl+shift+]");
|
|
443
|
+
expect(matchesKeyCombo(event, parsed)).toBe(true);
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
it("does not match ctrl+[ without shift", () => {
|
|
447
|
+
const event = createKeyboardEvent({ key: "[", shiftKey: false, ctrlKey: true });
|
|
448
|
+
const parsed = parseKeyCombo("ctrl+shift+[");
|
|
449
|
+
expect(matchesKeyCombo(event, parsed)).toBe(false);
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
it("does not match ctrl+] without shift", () => {
|
|
453
|
+
const event = createKeyboardEvent({ key: "]", shiftKey: false, ctrlKey: true });
|
|
454
|
+
const parsed = parseKeyCombo("ctrl+shift+]");
|
|
455
|
+
expect(matchesKeyCombo(event, parsed)).toBe(false);
|
|
456
|
+
});
|
|
457
|
+
|
|
458
|
+
it("does not match shift+[ without ctrl", () => {
|
|
459
|
+
const event = createKeyboardEvent({ key: "{", shiftKey: true, ctrlKey: false });
|
|
460
|
+
const parsed = parseKeyCombo("ctrl+shift+[");
|
|
461
|
+
expect(matchesKeyCombo(event, parsed)).toBe(false);
|
|
462
|
+
});
|
|
463
|
+
});
|
|
464
|
+
|
|
465
|
+
describe("arrow key normalization", () => {
|
|
466
|
+
it("matches ArrowUp to ctrl+alt+shift+up", () => {
|
|
467
|
+
const event = createKeyboardEvent({
|
|
468
|
+
key: "ArrowUp",
|
|
469
|
+
ctrlKey: true,
|
|
470
|
+
altKey: true,
|
|
471
|
+
shiftKey: true
|
|
472
|
+
});
|
|
473
|
+
const parsed = parseKeyCombo("ctrl+alt+shift+up");
|
|
474
|
+
expect(matchesKeyCombo(event, parsed)).toBe(true);
|
|
475
|
+
});
|
|
476
|
+
|
|
477
|
+
it("matches ArrowDown to ctrl+alt+shift+down", () => {
|
|
478
|
+
const event = createKeyboardEvent({
|
|
479
|
+
key: "ArrowDown",
|
|
480
|
+
ctrlKey: true,
|
|
481
|
+
altKey: true,
|
|
482
|
+
shiftKey: true
|
|
483
|
+
});
|
|
484
|
+
const parsed = parseKeyCombo("ctrl+alt+shift+down");
|
|
485
|
+
expect(matchesKeyCombo(event, parsed)).toBe(true);
|
|
486
|
+
});
|
|
487
|
+
|
|
488
|
+
it("matches ArrowLeft to ctrl+alt+shift+left", () => {
|
|
489
|
+
const event = createKeyboardEvent({
|
|
490
|
+
key: "ArrowLeft",
|
|
491
|
+
ctrlKey: true,
|
|
492
|
+
altKey: true,
|
|
493
|
+
shiftKey: true
|
|
494
|
+
});
|
|
495
|
+
const parsed = parseKeyCombo("ctrl+alt+shift+left");
|
|
496
|
+
expect(matchesKeyCombo(event, parsed)).toBe(true);
|
|
497
|
+
});
|
|
498
|
+
|
|
499
|
+
it("matches ArrowRight to ctrl+alt+shift+right", () => {
|
|
500
|
+
const event = createKeyboardEvent({
|
|
501
|
+
key: "ArrowRight",
|
|
502
|
+
ctrlKey: true,
|
|
503
|
+
altKey: true,
|
|
504
|
+
shiftKey: true
|
|
505
|
+
});
|
|
506
|
+
const parsed = parseKeyCombo("ctrl+alt+shift+right");
|
|
507
|
+
expect(matchesKeyCombo(event, parsed)).toBe(true);
|
|
508
|
+
});
|
|
509
|
+
|
|
510
|
+
it("matches ArrowUp with only ctrl modifier", () => {
|
|
511
|
+
const event = createKeyboardEvent({
|
|
512
|
+
key: "ArrowUp",
|
|
513
|
+
ctrlKey: true
|
|
514
|
+
});
|
|
515
|
+
const parsed = parseKeyCombo("ctrl+up");
|
|
516
|
+
expect(matchesKeyCombo(event, parsed)).toBe(true);
|
|
517
|
+
});
|
|
518
|
+
|
|
519
|
+
it("matches ArrowDown with only alt modifier", () => {
|
|
520
|
+
const event = createKeyboardEvent({
|
|
521
|
+
key: "ArrowDown",
|
|
522
|
+
altKey: true
|
|
523
|
+
});
|
|
524
|
+
const parsed = parseKeyCombo("alt+down");
|
|
525
|
+
expect(matchesKeyCombo(event, parsed)).toBe(true);
|
|
526
|
+
});
|
|
527
|
+
|
|
528
|
+
it("does not match ArrowUp when expecting ArrowDown", () => {
|
|
529
|
+
const event = createKeyboardEvent({
|
|
530
|
+
key: "ArrowUp",
|
|
531
|
+
ctrlKey: true
|
|
532
|
+
});
|
|
533
|
+
const parsed = parseKeyCombo("ctrl+down");
|
|
534
|
+
expect(matchesKeyCombo(event, parsed)).toBe(false);
|
|
535
|
+
});
|
|
536
|
+
|
|
537
|
+
it("does not match when modifiers are different", () => {
|
|
538
|
+
const event = createKeyboardEvent({
|
|
539
|
+
key: "ArrowUp",
|
|
540
|
+
ctrlKey: true,
|
|
541
|
+
shiftKey: false
|
|
542
|
+
});
|
|
543
|
+
const parsed = parseKeyCombo("ctrl+shift+up");
|
|
544
|
+
expect(matchesKeyCombo(event, parsed)).toBe(false);
|
|
545
|
+
});
|
|
546
|
+
});
|
|
547
|
+
|
|
548
|
+
describe("cross-platform modifier handling", () => {
|
|
549
|
+
// Note: These tests check the function behavior; actual Mac detection
|
|
550
|
+
// depends on navigator.platform which is mocked by jsdom
|
|
551
|
+
|
|
552
|
+
it("matches ctrl+key when only ctrl is pressed (non-Mac)", () => {
|
|
553
|
+
const event = createKeyboardEvent({ key: "c", ctrlKey: true, metaKey: false });
|
|
554
|
+
const parsed = parseKeyCombo("ctrl+c");
|
|
555
|
+
expect(matchesKeyCombo(event, parsed)).toBe(true);
|
|
556
|
+
});
|
|
557
|
+
|
|
558
|
+
it("does not match key press when ctrl is required but neither ctrl nor meta pressed", () => {
|
|
559
|
+
const event = createKeyboardEvent({ key: "c", ctrlKey: false, metaKey: false });
|
|
560
|
+
const parsed = parseKeyCombo("ctrl+c");
|
|
561
|
+
expect(matchesKeyCombo(event, parsed)).toBe(false);
|
|
562
|
+
});
|
|
563
|
+
});
|
|
564
|
+
});
|
|
565
|
+
|
|
566
|
+
describe("useMarkdownHotkeys composable", () => {
|
|
567
|
+
let contentRef: ReturnType<typeof ref<HTMLElement | null>>;
|
|
568
|
+
let onShowHotkeyHelp: ReturnType<typeof vi.fn>;
|
|
569
|
+
|
|
570
|
+
beforeEach(() => {
|
|
571
|
+
contentRef = ref(document.createElement("div"));
|
|
572
|
+
onShowHotkeyHelp = vi.fn();
|
|
573
|
+
});
|
|
574
|
+
|
|
575
|
+
it("registers and retrieves hotkey definitions", () => {
|
|
576
|
+
const { registerHotkey, getHotkeyDefinitions } = useMarkdownHotkeys({
|
|
577
|
+
contentRef,
|
|
578
|
+
onShowHotkeyHelp
|
|
579
|
+
});
|
|
580
|
+
|
|
581
|
+
const def: HotkeyDefinition = {
|
|
582
|
+
key: "ctrl+b",
|
|
583
|
+
action: vi.fn(),
|
|
584
|
+
description: "Bold",
|
|
585
|
+
group: "formatting"
|
|
586
|
+
};
|
|
587
|
+
|
|
588
|
+
registerHotkey(def);
|
|
589
|
+
|
|
590
|
+
const definitions = getHotkeyDefinitions();
|
|
591
|
+
expect(definitions).toHaveLength(1);
|
|
592
|
+
expect(definitions[0].key).toBe("ctrl+b");
|
|
593
|
+
expect(definitions[0].description).toBe("Bold");
|
|
594
|
+
});
|
|
595
|
+
|
|
596
|
+
it("unregisters hotkeys", () => {
|
|
597
|
+
const { registerHotkey, unregisterHotkey, getHotkeyDefinitions } = useMarkdownHotkeys({
|
|
598
|
+
contentRef,
|
|
599
|
+
onShowHotkeyHelp
|
|
600
|
+
});
|
|
601
|
+
|
|
602
|
+
registerHotkey({
|
|
603
|
+
key: "ctrl+b",
|
|
604
|
+
action: vi.fn(),
|
|
605
|
+
description: "Bold",
|
|
606
|
+
group: "formatting"
|
|
607
|
+
});
|
|
608
|
+
|
|
609
|
+
expect(getHotkeyDefinitions()).toHaveLength(1);
|
|
610
|
+
|
|
611
|
+
unregisterHotkey("ctrl+b");
|
|
612
|
+
|
|
613
|
+
expect(getHotkeyDefinitions()).toHaveLength(0);
|
|
614
|
+
});
|
|
615
|
+
|
|
616
|
+
it("handles keydown and executes registered action", () => {
|
|
617
|
+
const { registerHotkey, handleKeyDown } = useMarkdownHotkeys({
|
|
618
|
+
contentRef,
|
|
619
|
+
onShowHotkeyHelp
|
|
620
|
+
});
|
|
621
|
+
|
|
622
|
+
const action = vi.fn();
|
|
623
|
+
registerHotkey({
|
|
624
|
+
key: "ctrl+b",
|
|
625
|
+
action,
|
|
626
|
+
description: "Bold",
|
|
627
|
+
group: "formatting"
|
|
628
|
+
});
|
|
629
|
+
|
|
630
|
+
const event = createKeyboardEvent({ key: "b", ctrlKey: true });
|
|
631
|
+
const handled = handleKeyDown(event);
|
|
632
|
+
|
|
633
|
+
expect(handled).toBe(true);
|
|
634
|
+
expect(action).toHaveBeenCalledTimes(1);
|
|
635
|
+
expect(event.preventDefault).toHaveBeenCalled();
|
|
636
|
+
});
|
|
637
|
+
|
|
638
|
+
it("returns false when no hotkey matches", () => {
|
|
639
|
+
const { handleKeyDown } = useMarkdownHotkeys({
|
|
640
|
+
contentRef,
|
|
641
|
+
onShowHotkeyHelp
|
|
642
|
+
});
|
|
643
|
+
|
|
644
|
+
const event = createKeyboardEvent({ key: "x" });
|
|
645
|
+
const handled = handleKeyDown(event);
|
|
646
|
+
|
|
647
|
+
expect(handled).toBe(false);
|
|
648
|
+
expect(event.preventDefault).not.toHaveBeenCalled();
|
|
649
|
+
});
|
|
650
|
+
|
|
651
|
+
it("triggers help on ctrl+/", () => {
|
|
652
|
+
const { handleKeyDown } = useMarkdownHotkeys({
|
|
653
|
+
contentRef,
|
|
654
|
+
onShowHotkeyHelp
|
|
655
|
+
});
|
|
656
|
+
|
|
657
|
+
const event = createKeyboardEvent({ key: "/", ctrlKey: true });
|
|
658
|
+
const handled = handleKeyDown(event);
|
|
659
|
+
|
|
660
|
+
expect(handled).toBe(true);
|
|
661
|
+
expect(onShowHotkeyHelp).toHaveBeenCalledTimes(1);
|
|
662
|
+
});
|
|
663
|
+
|
|
664
|
+
it("triggers help on ctrl+?", () => {
|
|
665
|
+
const { handleKeyDown } = useMarkdownHotkeys({
|
|
666
|
+
contentRef,
|
|
667
|
+
onShowHotkeyHelp
|
|
668
|
+
});
|
|
669
|
+
|
|
670
|
+
const event = createKeyboardEvent({ key: "?", ctrlKey: true });
|
|
671
|
+
const handled = handleKeyDown(event);
|
|
672
|
+
|
|
673
|
+
expect(handled).toBe(true);
|
|
674
|
+
expect(onShowHotkeyHelp).toHaveBeenCalledTimes(1);
|
|
675
|
+
});
|
|
676
|
+
|
|
677
|
+
it("triggers help on meta+/ (Mac)", () => {
|
|
678
|
+
const { handleKeyDown } = useMarkdownHotkeys({
|
|
679
|
+
contentRef,
|
|
680
|
+
onShowHotkeyHelp
|
|
681
|
+
});
|
|
682
|
+
|
|
683
|
+
const event = createKeyboardEvent({ key: "/", metaKey: true });
|
|
684
|
+
const handled = handleKeyDown(event);
|
|
685
|
+
|
|
686
|
+
expect(handled).toBe(true);
|
|
687
|
+
expect(onShowHotkeyHelp).toHaveBeenCalledTimes(1);
|
|
688
|
+
});
|
|
689
|
+
|
|
690
|
+
it("handles multiple registered hotkeys", () => {
|
|
691
|
+
const { registerHotkey, handleKeyDown } = useMarkdownHotkeys({
|
|
692
|
+
contentRef,
|
|
693
|
+
onShowHotkeyHelp
|
|
694
|
+
});
|
|
695
|
+
|
|
696
|
+
const boldAction = vi.fn();
|
|
697
|
+
const italicAction = vi.fn();
|
|
698
|
+
|
|
699
|
+
registerHotkey({
|
|
700
|
+
key: "ctrl+b",
|
|
701
|
+
action: boldAction,
|
|
702
|
+
description: "Bold",
|
|
703
|
+
group: "formatting"
|
|
704
|
+
});
|
|
705
|
+
|
|
706
|
+
registerHotkey({
|
|
707
|
+
key: "ctrl+i",
|
|
708
|
+
action: italicAction,
|
|
709
|
+
description: "Italic",
|
|
710
|
+
group: "formatting"
|
|
711
|
+
});
|
|
712
|
+
|
|
713
|
+
const boldEvent = createKeyboardEvent({ key: "b", ctrlKey: true });
|
|
714
|
+
handleKeyDown(boldEvent);
|
|
715
|
+
expect(boldAction).toHaveBeenCalledTimes(1);
|
|
716
|
+
expect(italicAction).not.toHaveBeenCalled();
|
|
717
|
+
|
|
718
|
+
const italicEvent = createKeyboardEvent({ key: "i", ctrlKey: true });
|
|
719
|
+
handleKeyDown(italicEvent);
|
|
720
|
+
expect(italicAction).toHaveBeenCalledTimes(1);
|
|
721
|
+
});
|
|
722
|
+
|
|
723
|
+
it("normalizes hotkey keys to lowercase", () => {
|
|
724
|
+
const { registerHotkey, handleKeyDown } = useMarkdownHotkeys({
|
|
725
|
+
contentRef,
|
|
726
|
+
onShowHotkeyHelp
|
|
727
|
+
});
|
|
728
|
+
|
|
729
|
+
const action = vi.fn();
|
|
730
|
+
registerHotkey({
|
|
731
|
+
key: "CTRL+B", // Uppercase input
|
|
732
|
+
action,
|
|
733
|
+
description: "Bold",
|
|
734
|
+
group: "formatting"
|
|
735
|
+
});
|
|
736
|
+
|
|
737
|
+
// Event with lowercase key
|
|
738
|
+
const event = createKeyboardEvent({ key: "b", ctrlKey: true });
|
|
739
|
+
const handled = handleKeyDown(event);
|
|
740
|
+
|
|
741
|
+
expect(handled).toBe(true);
|
|
742
|
+
expect(action).toHaveBeenCalled();
|
|
743
|
+
});
|
|
744
|
+
|
|
745
|
+
describe("bracket hotkey integration", () => {
|
|
746
|
+
it("handles ctrl+shift+[ hotkey registration and execution", () => {
|
|
747
|
+
const { registerHotkey, handleKeyDown } = useMarkdownHotkeys({
|
|
748
|
+
contentRef,
|
|
749
|
+
onShowHotkeyHelp
|
|
750
|
+
});
|
|
751
|
+
|
|
752
|
+
const increaseHeadingAction = vi.fn();
|
|
753
|
+
registerHotkey({
|
|
754
|
+
key: "ctrl+shift+[",
|
|
755
|
+
action: increaseHeadingAction,
|
|
756
|
+
description: "Increase heading level",
|
|
757
|
+
group: "headings"
|
|
758
|
+
});
|
|
759
|
+
|
|
760
|
+
// Browser reports "{" when Ctrl+Shift+[ is pressed
|
|
761
|
+
const event = createKeyboardEvent({ key: "{", ctrlKey: true, shiftKey: true });
|
|
762
|
+
const handled = handleKeyDown(event);
|
|
763
|
+
|
|
764
|
+
expect(handled).toBe(true);
|
|
765
|
+
expect(increaseHeadingAction).toHaveBeenCalledTimes(1);
|
|
766
|
+
});
|
|
767
|
+
|
|
768
|
+
it("handles ctrl+shift+] hotkey registration and execution", () => {
|
|
769
|
+
const { registerHotkey, handleKeyDown } = useMarkdownHotkeys({
|
|
770
|
+
contentRef,
|
|
771
|
+
onShowHotkeyHelp
|
|
772
|
+
});
|
|
773
|
+
|
|
774
|
+
const decreaseHeadingAction = vi.fn();
|
|
775
|
+
registerHotkey({
|
|
776
|
+
key: "ctrl+shift+]",
|
|
777
|
+
action: decreaseHeadingAction,
|
|
778
|
+
description: "Decrease heading level",
|
|
779
|
+
group: "headings"
|
|
780
|
+
});
|
|
781
|
+
|
|
782
|
+
// Browser reports "}" when Ctrl+Shift+] is pressed
|
|
783
|
+
const event = createKeyboardEvent({ key: "}", ctrlKey: true, shiftKey: true });
|
|
784
|
+
const handled = handleKeyDown(event);
|
|
785
|
+
|
|
786
|
+
expect(handled).toBe(true);
|
|
787
|
+
expect(decreaseHeadingAction).toHaveBeenCalledTimes(1);
|
|
788
|
+
});
|
|
789
|
+
});
|
|
790
|
+
});
|
|
791
|
+
});
|