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,705 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
|
|
2
|
+
import { useInlineFormatting } from "./useInlineFormatting";
|
|
3
|
+
import { createTestEditor, TestEditorResult } from "../../../test/helpers/editorTestUtils";
|
|
4
|
+
|
|
5
|
+
describe("useInlineFormatting", () => {
|
|
6
|
+
let editor: TestEditorResult;
|
|
7
|
+
let onContentChange: ReturnType<typeof vi.fn>;
|
|
8
|
+
|
|
9
|
+
afterEach(() => {
|
|
10
|
+
if (editor) {
|
|
11
|
+
editor.destroy();
|
|
12
|
+
}
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
function createFormatting() {
|
|
16
|
+
return useInlineFormatting({
|
|
17
|
+
contentRef: editor.contentRef,
|
|
18
|
+
onContentChange
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
describe("toggleBold", () => {
|
|
23
|
+
describe("with selection", () => {
|
|
24
|
+
beforeEach(() => {
|
|
25
|
+
onContentChange = vi.fn();
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it("wraps selected text in strong tag", () => {
|
|
29
|
+
editor = createTestEditor("<p>Hello world</p>");
|
|
30
|
+
const formatting = createFormatting();
|
|
31
|
+
editor.selectInBlock(0, 0, 5); // Select "Hello"
|
|
32
|
+
|
|
33
|
+
formatting.toggleBold();
|
|
34
|
+
|
|
35
|
+
expect(editor.getHtml()).toBe("<p><strong>Hello</strong> world</p>");
|
|
36
|
+
expect(onContentChange).toHaveBeenCalled();
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("wraps middle text in strong tag", () => {
|
|
40
|
+
editor = createTestEditor("<p>Hello beautiful world</p>");
|
|
41
|
+
const formatting = createFormatting();
|
|
42
|
+
editor.selectInBlock(0, 6, 15); // Select "beautiful"
|
|
43
|
+
|
|
44
|
+
formatting.toggleBold();
|
|
45
|
+
|
|
46
|
+
expect(editor.getHtml()).toBe("<p>Hello <strong>beautiful</strong> world</p>");
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("wraps end text in strong tag", () => {
|
|
50
|
+
editor = createTestEditor("<p>Hello world</p>");
|
|
51
|
+
const formatting = createFormatting();
|
|
52
|
+
editor.selectInBlock(0, 6, 11); // Select "world"
|
|
53
|
+
|
|
54
|
+
formatting.toggleBold();
|
|
55
|
+
|
|
56
|
+
expect(editor.getHtml()).toBe("<p>Hello <strong>world</strong></p>");
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("unwraps entire bold element when fully selected", () => {
|
|
60
|
+
editor = createTestEditor("<p><strong>Hello</strong> world</p>");
|
|
61
|
+
const formatting = createFormatting();
|
|
62
|
+
editor.selectInBlock(0, 0, 5); // Select "Hello"
|
|
63
|
+
|
|
64
|
+
formatting.toggleBold();
|
|
65
|
+
|
|
66
|
+
expect(editor.getHtml()).toBe("<p>Hello world</p>");
|
|
67
|
+
expect(onContentChange).toHaveBeenCalled();
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("unwraps partial selection from beginning of bold element", () => {
|
|
71
|
+
editor = createTestEditor("<p><strong>Hello world</strong> end</p>");
|
|
72
|
+
const formatting = createFormatting();
|
|
73
|
+
editor.selectInBlock(0, 0, 5); // Select "Hello"
|
|
74
|
+
|
|
75
|
+
formatting.toggleBold();
|
|
76
|
+
|
|
77
|
+
// Should result in: "Hello" unformatted + " world" bold + " end"
|
|
78
|
+
expect(editor.getHtml()).toBe("<p>Hello<strong> world</strong> end</p>");
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it("unwraps partial selection from middle of bold element", () => {
|
|
82
|
+
editor = createTestEditor("<p><strong>Hello world</strong></p>");
|
|
83
|
+
const formatting = createFormatting();
|
|
84
|
+
editor.selectInBlock(0, 6, 11); // Select "world"
|
|
85
|
+
|
|
86
|
+
formatting.toggleBold();
|
|
87
|
+
|
|
88
|
+
// Should result in: "Hello " bold + "world" unformatted
|
|
89
|
+
expect(editor.getHtml()).toBe("<p><strong>Hello </strong>world</p>");
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it("unwraps partial selection from end of bold element", () => {
|
|
93
|
+
editor = createTestEditor("<p>start <strong>Hello world</strong></p>");
|
|
94
|
+
const formatting = createFormatting();
|
|
95
|
+
editor.selectInBlock(0, 12, 17); // Select "world"
|
|
96
|
+
|
|
97
|
+
formatting.toggleBold();
|
|
98
|
+
|
|
99
|
+
// Should result in: "start " + "Hello " bold + "world" unformatted
|
|
100
|
+
expect(editor.getHtml()).toBe("<p>start <strong>Hello </strong>world</p>");
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it("selects the wrapped content after wrapping", () => {
|
|
104
|
+
editor = createTestEditor("<p>Hello world</p>");
|
|
105
|
+
const formatting = createFormatting();
|
|
106
|
+
editor.selectInBlock(0, 0, 5); // Select "Hello"
|
|
107
|
+
|
|
108
|
+
formatting.toggleBold();
|
|
109
|
+
|
|
110
|
+
const selection = window.getSelection();
|
|
111
|
+
expect(selection?.toString()).toBe("Hello");
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
describe("without selection (cursor only)", () => {
|
|
116
|
+
beforeEach(() => {
|
|
117
|
+
onContentChange = vi.fn();
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it("inserts bold placeholder when cursor is in plain text", () => {
|
|
121
|
+
editor = createTestEditor("<p>Hello world</p>");
|
|
122
|
+
const formatting = createFormatting();
|
|
123
|
+
editor.setCursorInBlock(0, 6); // After "Hello "
|
|
124
|
+
|
|
125
|
+
formatting.toggleBold();
|
|
126
|
+
|
|
127
|
+
expect(editor.getHtml()).toContain("<strong>bold text</strong>");
|
|
128
|
+
expect(onContentChange).toHaveBeenCalled();
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it("inserts placeholder at beginning of text", () => {
|
|
132
|
+
editor = createTestEditor("<p>Hello world</p>");
|
|
133
|
+
const formatting = createFormatting();
|
|
134
|
+
editor.setCursorInBlock(0, 0); // At start
|
|
135
|
+
|
|
136
|
+
formatting.toggleBold();
|
|
137
|
+
|
|
138
|
+
expect(editor.getHtml()).toContain("<strong>bold text</strong>");
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it("placeholder text is selected after insertion", () => {
|
|
142
|
+
editor = createTestEditor("<p>Hello world</p>");
|
|
143
|
+
const formatting = createFormatting();
|
|
144
|
+
editor.setCursorInBlock(0, 6); // After "Hello "
|
|
145
|
+
|
|
146
|
+
formatting.toggleBold();
|
|
147
|
+
|
|
148
|
+
const selection = window.getSelection();
|
|
149
|
+
expect(selection?.toString()).toBe("bold text");
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it("moves cursor outside bold when cursor is inside bold text", () => {
|
|
153
|
+
editor = createTestEditor("<p><strong>Hello</strong> world</p>");
|
|
154
|
+
const formatting = createFormatting();
|
|
155
|
+
const strongEl = editor.container.querySelector("strong");
|
|
156
|
+
editor.setCursor(strongEl!.firstChild!, 5); // End of "Hello"
|
|
157
|
+
|
|
158
|
+
formatting.toggleBold();
|
|
159
|
+
|
|
160
|
+
// The HTML should still have the strong element
|
|
161
|
+
expect(editor.getHtml()).toContain("<strong>Hello</strong>");
|
|
162
|
+
// A zero-width space should be inserted for cursor positioning
|
|
163
|
+
expect(editor.getHtml()).toContain("\u200B");
|
|
164
|
+
expect(onContentChange).toHaveBeenCalled();
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it("moves cursor outside bold when cursor is at start of bold text", () => {
|
|
168
|
+
editor = createTestEditor("<p><strong>Hello</strong> world</p>");
|
|
169
|
+
const formatting = createFormatting();
|
|
170
|
+
const strongEl = editor.container.querySelector("strong");
|
|
171
|
+
editor.setCursor(strongEl!.firstChild!, 0); // Start of "Hello"
|
|
172
|
+
|
|
173
|
+
formatting.toggleBold();
|
|
174
|
+
|
|
175
|
+
expect(editor.getHtml()).toContain("<strong>Hello</strong>");
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
describe("toggleItalic", () => {
|
|
181
|
+
describe("with selection", () => {
|
|
182
|
+
beforeEach(() => {
|
|
183
|
+
onContentChange = vi.fn();
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it("wraps selected text in em tag", () => {
|
|
187
|
+
editor = createTestEditor("<p>Hello world</p>");
|
|
188
|
+
const formatting = createFormatting();
|
|
189
|
+
editor.selectInBlock(0, 0, 5); // Select "Hello"
|
|
190
|
+
|
|
191
|
+
formatting.toggleItalic();
|
|
192
|
+
|
|
193
|
+
expect(editor.getHtml()).toBe("<p><em>Hello</em> world</p>");
|
|
194
|
+
expect(onContentChange).toHaveBeenCalled();
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
it("unwraps entire italic element when fully selected", () => {
|
|
198
|
+
editor = createTestEditor("<p><em>Hello</em> world</p>");
|
|
199
|
+
const formatting = createFormatting();
|
|
200
|
+
editor.selectInBlock(0, 0, 5); // Select "Hello"
|
|
201
|
+
|
|
202
|
+
formatting.toggleItalic();
|
|
203
|
+
|
|
204
|
+
expect(editor.getHtml()).toBe("<p>Hello world</p>");
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
it("unwraps partial selection from italic element", () => {
|
|
208
|
+
editor = createTestEditor("<p><em>Hello world</em></p>");
|
|
209
|
+
const formatting = createFormatting();
|
|
210
|
+
editor.selectInBlock(0, 0, 5); // Select "Hello"
|
|
211
|
+
|
|
212
|
+
formatting.toggleItalic();
|
|
213
|
+
|
|
214
|
+
expect(editor.getHtml()).toBe("<p>Hello<em> world</em></p>");
|
|
215
|
+
});
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
describe("without selection (cursor only)", () => {
|
|
219
|
+
beforeEach(() => {
|
|
220
|
+
onContentChange = vi.fn();
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
it("inserts italic placeholder when cursor is in plain text", () => {
|
|
224
|
+
editor = createTestEditor("<p>Hello world</p>");
|
|
225
|
+
const formatting = createFormatting();
|
|
226
|
+
editor.setCursorInBlock(0, 6);
|
|
227
|
+
|
|
228
|
+
formatting.toggleItalic();
|
|
229
|
+
|
|
230
|
+
expect(editor.getHtml()).toContain("<em>italic text</em>");
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
it("placeholder text is selected after insertion", () => {
|
|
234
|
+
editor = createTestEditor("<p>Hello world</p>");
|
|
235
|
+
const formatting = createFormatting();
|
|
236
|
+
editor.setCursorInBlock(0, 6);
|
|
237
|
+
|
|
238
|
+
formatting.toggleItalic();
|
|
239
|
+
|
|
240
|
+
const selection = window.getSelection();
|
|
241
|
+
expect(selection?.toString()).toBe("italic text");
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
it("moves cursor outside italic when cursor is inside italic text", () => {
|
|
245
|
+
editor = createTestEditor("<p><em>Hello</em> world</p>");
|
|
246
|
+
const formatting = createFormatting();
|
|
247
|
+
const emEl = editor.container.querySelector("em");
|
|
248
|
+
editor.setCursor(emEl!.firstChild!, 3);
|
|
249
|
+
|
|
250
|
+
formatting.toggleItalic();
|
|
251
|
+
|
|
252
|
+
expect(editor.getHtml()).toContain("<em>Hello</em>");
|
|
253
|
+
expect(editor.getHtml()).toContain("\u200B");
|
|
254
|
+
});
|
|
255
|
+
});
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
describe("toggleStrikethrough", () => {
|
|
259
|
+
describe("with selection", () => {
|
|
260
|
+
beforeEach(() => {
|
|
261
|
+
onContentChange = vi.fn();
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
it("wraps selected text in del tag", () => {
|
|
265
|
+
editor = createTestEditor("<p>Hello world</p>");
|
|
266
|
+
const formatting = createFormatting();
|
|
267
|
+
editor.selectInBlock(0, 0, 5);
|
|
268
|
+
|
|
269
|
+
formatting.toggleStrikethrough();
|
|
270
|
+
|
|
271
|
+
expect(editor.getHtml()).toBe("<p><del>Hello</del> world</p>");
|
|
272
|
+
expect(onContentChange).toHaveBeenCalled();
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
it("unwraps entire strikethrough element when fully selected", () => {
|
|
276
|
+
editor = createTestEditor("<p><del>Hello</del> world</p>");
|
|
277
|
+
const formatting = createFormatting();
|
|
278
|
+
editor.selectInBlock(0, 0, 5);
|
|
279
|
+
|
|
280
|
+
formatting.toggleStrikethrough();
|
|
281
|
+
|
|
282
|
+
expect(editor.getHtml()).toBe("<p>Hello world</p>");
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
it("unwraps partial selection from strikethrough element", () => {
|
|
286
|
+
editor = createTestEditor("<p><del>Hello world</del></p>");
|
|
287
|
+
const formatting = createFormatting();
|
|
288
|
+
editor.selectInBlock(0, 6, 11); // Select "world"
|
|
289
|
+
|
|
290
|
+
formatting.toggleStrikethrough();
|
|
291
|
+
|
|
292
|
+
expect(editor.getHtml()).toBe("<p><del>Hello </del>world</p>");
|
|
293
|
+
});
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
describe("without selection (cursor only)", () => {
|
|
297
|
+
beforeEach(() => {
|
|
298
|
+
onContentChange = vi.fn();
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
it("inserts strikethrough placeholder when cursor is in plain text", () => {
|
|
302
|
+
editor = createTestEditor("<p>Hello world</p>");
|
|
303
|
+
const formatting = createFormatting();
|
|
304
|
+
editor.setCursorInBlock(0, 6);
|
|
305
|
+
|
|
306
|
+
formatting.toggleStrikethrough();
|
|
307
|
+
|
|
308
|
+
expect(editor.getHtml()).toContain("<del>strikethrough text</del>");
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
it("placeholder text is selected after insertion", () => {
|
|
312
|
+
editor = createTestEditor("<p>Hello world</p>");
|
|
313
|
+
const formatting = createFormatting();
|
|
314
|
+
editor.setCursorInBlock(0, 6);
|
|
315
|
+
|
|
316
|
+
formatting.toggleStrikethrough();
|
|
317
|
+
|
|
318
|
+
const selection = window.getSelection();
|
|
319
|
+
expect(selection?.toString()).toBe("strikethrough text");
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
it("moves cursor outside del when cursor is inside strikethrough text", () => {
|
|
323
|
+
editor = createTestEditor("<p><del>Hello</del> world</p>");
|
|
324
|
+
const formatting = createFormatting();
|
|
325
|
+
const delEl = editor.container.querySelector("del");
|
|
326
|
+
editor.setCursor(delEl!.firstChild!, 3);
|
|
327
|
+
|
|
328
|
+
formatting.toggleStrikethrough();
|
|
329
|
+
|
|
330
|
+
expect(editor.getHtml()).toContain("<del>Hello</del>");
|
|
331
|
+
expect(editor.getHtml()).toContain("\u200B");
|
|
332
|
+
});
|
|
333
|
+
});
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
describe("toggleInlineCode", () => {
|
|
337
|
+
describe("with selection", () => {
|
|
338
|
+
beforeEach(() => {
|
|
339
|
+
onContentChange = vi.fn();
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
it("wraps selected text in code tag", () => {
|
|
343
|
+
editor = createTestEditor("<p>Hello world</p>");
|
|
344
|
+
const formatting = createFormatting();
|
|
345
|
+
editor.selectInBlock(0, 0, 5);
|
|
346
|
+
|
|
347
|
+
formatting.toggleInlineCode();
|
|
348
|
+
|
|
349
|
+
expect(editor.getHtml()).toBe("<p><code>Hello</code> world</p>");
|
|
350
|
+
expect(onContentChange).toHaveBeenCalled();
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
it("unwraps entire code element when fully selected", () => {
|
|
354
|
+
editor = createTestEditor("<p><code>Hello</code> world</p>");
|
|
355
|
+
const formatting = createFormatting();
|
|
356
|
+
editor.selectInBlock(0, 0, 5);
|
|
357
|
+
|
|
358
|
+
formatting.toggleInlineCode();
|
|
359
|
+
|
|
360
|
+
expect(editor.getHtml()).toBe("<p>Hello world</p>");
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
it("unwraps partial selection from code element", () => {
|
|
364
|
+
editor = createTestEditor("<p><code>Hello world</code></p>");
|
|
365
|
+
const formatting = createFormatting();
|
|
366
|
+
editor.selectInBlock(0, 0, 5); // Select "Hello"
|
|
367
|
+
|
|
368
|
+
formatting.toggleInlineCode();
|
|
369
|
+
|
|
370
|
+
expect(editor.getHtml()).toBe("<p>Hello<code> world</code></p>");
|
|
371
|
+
});
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
describe("without selection (cursor only)", () => {
|
|
375
|
+
beforeEach(() => {
|
|
376
|
+
onContentChange = vi.fn();
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
it("inserts code placeholder when cursor is in plain text", () => {
|
|
380
|
+
editor = createTestEditor("<p>Hello world</p>");
|
|
381
|
+
const formatting = createFormatting();
|
|
382
|
+
editor.setCursorInBlock(0, 6);
|
|
383
|
+
|
|
384
|
+
formatting.toggleInlineCode();
|
|
385
|
+
|
|
386
|
+
expect(editor.getHtml()).toContain("<code>code</code>");
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
it("placeholder text is selected after insertion", () => {
|
|
390
|
+
editor = createTestEditor("<p>Hello world</p>");
|
|
391
|
+
const formatting = createFormatting();
|
|
392
|
+
editor.setCursorInBlock(0, 6);
|
|
393
|
+
|
|
394
|
+
formatting.toggleInlineCode();
|
|
395
|
+
|
|
396
|
+
const selection = window.getSelection();
|
|
397
|
+
expect(selection?.toString()).toBe("code");
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
it("moves cursor outside code when cursor is inside code text", () => {
|
|
401
|
+
editor = createTestEditor("<p><code>Hello</code> world</p>");
|
|
402
|
+
const formatting = createFormatting();
|
|
403
|
+
const codeEl = editor.container.querySelector("code");
|
|
404
|
+
editor.setCursor(codeEl!.firstChild!, 3);
|
|
405
|
+
|
|
406
|
+
formatting.toggleInlineCode();
|
|
407
|
+
|
|
408
|
+
expect(editor.getHtml()).toContain("<code>Hello</code>");
|
|
409
|
+
expect(editor.getHtml()).toContain("\u200B");
|
|
410
|
+
});
|
|
411
|
+
});
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
describe("edge cases", () => {
|
|
415
|
+
beforeEach(() => {
|
|
416
|
+
onContentChange = vi.fn();
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
it("does nothing when contentRef is null", () => {
|
|
420
|
+
editor = createTestEditor("<p>Hello world</p>");
|
|
421
|
+
const { toggleBold } = useInlineFormatting({
|
|
422
|
+
contentRef: { value: null },
|
|
423
|
+
onContentChange
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
toggleBold();
|
|
427
|
+
|
|
428
|
+
expect(editor.getHtml()).toBe("<p>Hello world</p>");
|
|
429
|
+
expect(onContentChange).not.toHaveBeenCalled();
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
it("does nothing when selection is outside content area", () => {
|
|
433
|
+
editor = createTestEditor("<p>Hello world</p>");
|
|
434
|
+
const formatting = createFormatting();
|
|
435
|
+
|
|
436
|
+
// Create a selection outside the editor
|
|
437
|
+
const externalDiv = document.createElement("div");
|
|
438
|
+
externalDiv.textContent = "External text";
|
|
439
|
+
document.body.appendChild(externalDiv);
|
|
440
|
+
|
|
441
|
+
const range = document.createRange();
|
|
442
|
+
range.selectNodeContents(externalDiv);
|
|
443
|
+
const sel = window.getSelection();
|
|
444
|
+
sel?.removeAllRanges();
|
|
445
|
+
sel?.addRange(range);
|
|
446
|
+
|
|
447
|
+
formatting.toggleBold();
|
|
448
|
+
|
|
449
|
+
expect(editor.getHtml()).toBe("<p>Hello world</p>");
|
|
450
|
+
expect(onContentChange).not.toHaveBeenCalled();
|
|
451
|
+
|
|
452
|
+
externalDiv.remove();
|
|
453
|
+
});
|
|
454
|
+
|
|
455
|
+
it("does nothing when no selection exists", () => {
|
|
456
|
+
editor = createTestEditor("<p>Hello world</p>");
|
|
457
|
+
const formatting = createFormatting();
|
|
458
|
+
|
|
459
|
+
// Clear any selection
|
|
460
|
+
window.getSelection()?.removeAllRanges();
|
|
461
|
+
|
|
462
|
+
formatting.toggleBold();
|
|
463
|
+
|
|
464
|
+
expect(editor.getHtml()).toBe("<p>Hello world</p>");
|
|
465
|
+
expect(onContentChange).not.toHaveBeenCalled();
|
|
466
|
+
});
|
|
467
|
+
|
|
468
|
+
it("handles fallback tag recognition (B for STRONG)", () => {
|
|
469
|
+
editor = createTestEditor("<p><b>Hello</b> world</p>");
|
|
470
|
+
const formatting = createFormatting();
|
|
471
|
+
editor.selectInBlock(0, 0, 5);
|
|
472
|
+
|
|
473
|
+
formatting.toggleBold();
|
|
474
|
+
|
|
475
|
+
// Should unwrap the B tag (recognized as bold)
|
|
476
|
+
expect(editor.getHtml()).toBe("<p>Hello world</p>");
|
|
477
|
+
});
|
|
478
|
+
|
|
479
|
+
it("handles fallback tag recognition (I for EM)", () => {
|
|
480
|
+
editor = createTestEditor("<p><i>Hello</i> world</p>");
|
|
481
|
+
const formatting = createFormatting();
|
|
482
|
+
editor.selectInBlock(0, 0, 5);
|
|
483
|
+
|
|
484
|
+
formatting.toggleItalic();
|
|
485
|
+
|
|
486
|
+
// Should unwrap the I tag (recognized as italic)
|
|
487
|
+
expect(editor.getHtml()).toBe("<p>Hello world</p>");
|
|
488
|
+
});
|
|
489
|
+
|
|
490
|
+
it("handles fallback tag recognition (S for DEL)", () => {
|
|
491
|
+
editor = createTestEditor("<p><s>Hello</s> world</p>");
|
|
492
|
+
const formatting = createFormatting();
|
|
493
|
+
editor.selectInBlock(0, 0, 5);
|
|
494
|
+
|
|
495
|
+
formatting.toggleStrikethrough();
|
|
496
|
+
|
|
497
|
+
// Should unwrap the S tag (recognized as strikethrough)
|
|
498
|
+
expect(editor.getHtml()).toBe("<p>Hello world</p>");
|
|
499
|
+
});
|
|
500
|
+
});
|
|
501
|
+
|
|
502
|
+
describe("nested formatting", () => {
|
|
503
|
+
beforeEach(() => {
|
|
504
|
+
onContentChange = vi.fn();
|
|
505
|
+
});
|
|
506
|
+
|
|
507
|
+
it("can bold text inside italic element", () => {
|
|
508
|
+
editor = createTestEditor("<p><em>Hello world</em></p>");
|
|
509
|
+
const formatting = createFormatting();
|
|
510
|
+
editor.selectInBlock(0, 0, 5); // Select "Hello"
|
|
511
|
+
|
|
512
|
+
formatting.toggleBold();
|
|
513
|
+
|
|
514
|
+
expect(editor.getHtml()).toContain("<strong>");
|
|
515
|
+
expect(editor.getHtml()).toContain("<em>");
|
|
516
|
+
});
|
|
517
|
+
|
|
518
|
+
it("can italic text inside bold element", () => {
|
|
519
|
+
editor = createTestEditor("<p><strong>Hello world</strong></p>");
|
|
520
|
+
const formatting = createFormatting();
|
|
521
|
+
editor.selectInBlock(0, 0, 5); // Select "Hello"
|
|
522
|
+
|
|
523
|
+
formatting.toggleItalic();
|
|
524
|
+
|
|
525
|
+
expect(editor.getHtml()).toContain("<em>");
|
|
526
|
+
expect(editor.getHtml()).toContain("<strong>");
|
|
527
|
+
});
|
|
528
|
+
});
|
|
529
|
+
|
|
530
|
+
describe("cursor position after operations", () => {
|
|
531
|
+
beforeEach(() => {
|
|
532
|
+
onContentChange = vi.fn();
|
|
533
|
+
});
|
|
534
|
+
|
|
535
|
+
it("selection encompasses wrapped content after wrapping", () => {
|
|
536
|
+
editor = createTestEditor("<p>Hello world</p>");
|
|
537
|
+
const formatting = createFormatting();
|
|
538
|
+
editor.selectInBlock(0, 0, 5);
|
|
539
|
+
|
|
540
|
+
formatting.toggleBold();
|
|
541
|
+
|
|
542
|
+
const selection = window.getSelection();
|
|
543
|
+
const range = selection?.getRangeAt(0);
|
|
544
|
+
const strongEl = editor.container.querySelector("strong");
|
|
545
|
+
|
|
546
|
+
// The selection should be within the strong element
|
|
547
|
+
expect(strongEl?.contains(range?.commonAncestorContainer || null)).toBe(true);
|
|
548
|
+
expect(selection?.toString()).toBe("Hello");
|
|
549
|
+
});
|
|
550
|
+
|
|
551
|
+
it("placeholder text is selected after inserting placeholder", () => {
|
|
552
|
+
editor = createTestEditor("<p>Hello world</p>");
|
|
553
|
+
const formatting = createFormatting();
|
|
554
|
+
editor.setCursorInBlock(0, 5);
|
|
555
|
+
|
|
556
|
+
formatting.toggleBold();
|
|
557
|
+
|
|
558
|
+
const selection = window.getSelection();
|
|
559
|
+
expect(selection?.toString()).toBe("bold text");
|
|
560
|
+
expect(selection?.isCollapsed).toBe(false);
|
|
561
|
+
});
|
|
562
|
+
|
|
563
|
+
it("cursor is positioned outside formatted element after exiting", () => {
|
|
564
|
+
editor = createTestEditor("<p><strong>Hello</strong> world</p>");
|
|
565
|
+
const formatting = createFormatting();
|
|
566
|
+
const strongEl = editor.container.querySelector("strong");
|
|
567
|
+
editor.setCursor(strongEl!.firstChild!, 5);
|
|
568
|
+
|
|
569
|
+
formatting.toggleBold();
|
|
570
|
+
|
|
571
|
+
const pos = editor.getCursorPosition();
|
|
572
|
+
// Cursor should be in a text node containing zero-width space
|
|
573
|
+
expect(pos.node?.textContent).toContain("\u200B");
|
|
574
|
+
});
|
|
575
|
+
});
|
|
576
|
+
|
|
577
|
+
describe("format merging (prevents nested same-type tags)", () => {
|
|
578
|
+
beforeEach(() => {
|
|
579
|
+
onContentChange = vi.fn();
|
|
580
|
+
});
|
|
581
|
+
|
|
582
|
+
it("merges highlight when selecting text containing highlighted portion", () => {
|
|
583
|
+
// User has: some ==highlighted== text
|
|
584
|
+
// User selects entire line and presses Ctrl+Shift+H
|
|
585
|
+
// Expected: <mark>some highlighted text</mark> (merged, not nested)
|
|
586
|
+
editor = createTestEditor("<p>some <mark>highlighted</mark> text</p>");
|
|
587
|
+
const formatting = createFormatting();
|
|
588
|
+
editor.selectInBlock(0, 0, 21); // Select "some highlighted text"
|
|
589
|
+
|
|
590
|
+
formatting.toggleHighlight();
|
|
591
|
+
|
|
592
|
+
const html = editor.getHtml();
|
|
593
|
+
// Should have exactly one mark tag, not nested marks
|
|
594
|
+
expect(html).toBe("<p><mark>some highlighted text</mark></p>");
|
|
595
|
+
// Verify no nested marks
|
|
596
|
+
expect(html.match(/<mark>/g)?.length).toBe(1);
|
|
597
|
+
});
|
|
598
|
+
|
|
599
|
+
it("merges bold when selecting text containing bold portion", () => {
|
|
600
|
+
editor = createTestEditor("<p>some <strong>bold</strong> text</p>");
|
|
601
|
+
const formatting = createFormatting();
|
|
602
|
+
editor.selectInBlock(0, 0, 14); // Select "some bold text"
|
|
603
|
+
|
|
604
|
+
formatting.toggleBold();
|
|
605
|
+
|
|
606
|
+
const html = editor.getHtml();
|
|
607
|
+
expect(html).toBe("<p><strong>some bold text</strong></p>");
|
|
608
|
+
expect(html.match(/<strong>/g)?.length).toBe(1);
|
|
609
|
+
});
|
|
610
|
+
|
|
611
|
+
it("merges italic when selecting text containing italic portion", () => {
|
|
612
|
+
editor = createTestEditor("<p>some <em>italic</em> text</p>");
|
|
613
|
+
const formatting = createFormatting();
|
|
614
|
+
editor.selectInBlock(0, 0, 16); // Select "some italic text"
|
|
615
|
+
|
|
616
|
+
formatting.toggleItalic();
|
|
617
|
+
|
|
618
|
+
const html = editor.getHtml();
|
|
619
|
+
expect(html).toBe("<p><em>some italic text</em></p>");
|
|
620
|
+
expect(html.match(/<em>/g)?.length).toBe(1);
|
|
621
|
+
});
|
|
622
|
+
|
|
623
|
+
it("merges strikethrough when selecting text containing strikethrough portion", () => {
|
|
624
|
+
editor = createTestEditor("<p>some <del>deleted</del> text</p>");
|
|
625
|
+
const formatting = createFormatting();
|
|
626
|
+
editor.selectInBlock(0, 0, 17); // Select "some deleted text"
|
|
627
|
+
|
|
628
|
+
formatting.toggleStrikethrough();
|
|
629
|
+
|
|
630
|
+
const html = editor.getHtml();
|
|
631
|
+
expect(html).toBe("<p><del>some deleted text</del></p>");
|
|
632
|
+
expect(html.match(/<del>/g)?.length).toBe(1);
|
|
633
|
+
});
|
|
634
|
+
|
|
635
|
+
it("merges code when selecting text containing code portion", () => {
|
|
636
|
+
editor = createTestEditor("<p>some <code>code</code> text</p>");
|
|
637
|
+
const formatting = createFormatting();
|
|
638
|
+
editor.selectInBlock(0, 0, 14); // Select "some code text"
|
|
639
|
+
|
|
640
|
+
formatting.toggleInlineCode();
|
|
641
|
+
|
|
642
|
+
const html = editor.getHtml();
|
|
643
|
+
expect(html).toBe("<p><code>some code text</code></p>");
|
|
644
|
+
expect(html.match(/<code>/g)?.length).toBe(1);
|
|
645
|
+
});
|
|
646
|
+
|
|
647
|
+
it("merges underline when selecting text containing underlined portion", () => {
|
|
648
|
+
editor = createTestEditor("<p>some <u>underlined</u> text</p>");
|
|
649
|
+
const formatting = createFormatting();
|
|
650
|
+
editor.selectInBlock(0, 0, 21); // Select "some underlined text"
|
|
651
|
+
|
|
652
|
+
formatting.toggleUnderline();
|
|
653
|
+
|
|
654
|
+
const html = editor.getHtml();
|
|
655
|
+
expect(html).toBe("<p><u>some underlined text</u></p>");
|
|
656
|
+
expect(html.match(/<u>/g)?.length).toBe(1);
|
|
657
|
+
});
|
|
658
|
+
|
|
659
|
+
it("merges multiple same-type formatted portions into one", () => {
|
|
660
|
+
// Multiple bold sections in selection - wrap the entire content
|
|
661
|
+
// The inner strong tags should be unwrapped, not nested
|
|
662
|
+
editor = createTestEditor("<p><strong>bold1</strong> middle <strong>bold2</strong></p>");
|
|
663
|
+
const formatting = createFormatting();
|
|
664
|
+
// Select all text content
|
|
665
|
+
editor.selectInBlock(0, 0, 18); // Select "bold1 middle bold2"
|
|
666
|
+
|
|
667
|
+
formatting.toggleBold();
|
|
668
|
+
|
|
669
|
+
const html = editor.getHtml();
|
|
670
|
+
// The result should have exactly one strong tag with merged content
|
|
671
|
+
// Empty boundary tags should be cleaned up
|
|
672
|
+
expect(html).toBe("<p><strong>bold1 middle bold2</strong></p>");
|
|
673
|
+
});
|
|
674
|
+
|
|
675
|
+
it("preserves different format types when merging same type", () => {
|
|
676
|
+
// Bold with italic inside, then wrap all in bold - should merge bold but keep italic
|
|
677
|
+
editor = createTestEditor("<p>text <strong>bold with <em>italic</em></strong> more</p>");
|
|
678
|
+
const formatting = createFormatting();
|
|
679
|
+
editor.selectInBlock(0, 0, 25); // Select all
|
|
680
|
+
|
|
681
|
+
formatting.toggleBold();
|
|
682
|
+
|
|
683
|
+
const html = editor.getHtml();
|
|
684
|
+
// Should have one strong tag wrapping everything, with em preserved inside
|
|
685
|
+
expect(html.match(/<strong>/g)?.length).toBe(1);
|
|
686
|
+
expect(html).toContain("<em>italic</em>");
|
|
687
|
+
});
|
|
688
|
+
});
|
|
689
|
+
|
|
690
|
+
describe("return type", () => {
|
|
691
|
+
beforeEach(() => {
|
|
692
|
+
onContentChange = vi.fn();
|
|
693
|
+
editor = createTestEditor("<p>test</p>");
|
|
694
|
+
});
|
|
695
|
+
|
|
696
|
+
it("returns all four toggle functions", () => {
|
|
697
|
+
const formatting = createFormatting();
|
|
698
|
+
|
|
699
|
+
expect(typeof formatting.toggleBold).toBe("function");
|
|
700
|
+
expect(typeof formatting.toggleItalic).toBe("function");
|
|
701
|
+
expect(typeof formatting.toggleStrikethrough).toBe("function");
|
|
702
|
+
expect(typeof formatting.toggleInlineCode).toBe("function");
|
|
703
|
+
});
|
|
704
|
+
});
|
|
705
|
+
});
|