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,834 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
2
|
+
import { useLists } from './useLists';
|
|
3
|
+
import { useMarkdownSelection } from '../useMarkdownSelection';
|
|
4
|
+
import { createTestEditor, TestEditorResult } from '../../../test/helpers/editorTestUtils';
|
|
5
|
+
|
|
6
|
+
describe('useLists', () => {
|
|
7
|
+
let editor: TestEditorResult;
|
|
8
|
+
let onContentChange: ReturnType<typeof vi.fn>;
|
|
9
|
+
|
|
10
|
+
beforeEach(() => {
|
|
11
|
+
editor = createTestEditor('<p>Hello world</p>');
|
|
12
|
+
onContentChange = vi.fn();
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
afterEach(() => {
|
|
16
|
+
if (editor) {
|
|
17
|
+
editor.destroy();
|
|
18
|
+
}
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
function createLists() {
|
|
22
|
+
const selection = useMarkdownSelection(editor.contentRef);
|
|
23
|
+
return useLists({
|
|
24
|
+
contentRef: editor.contentRef,
|
|
25
|
+
selection,
|
|
26
|
+
onContentChange
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Helper to set cursor in a list item by finding its text node
|
|
32
|
+
*/
|
|
33
|
+
function setCursorInListItem(li: HTMLLIElement, offset: number): void {
|
|
34
|
+
const walker = document.createTreeWalker(li, NodeFilter.SHOW_TEXT);
|
|
35
|
+
let textNode = walker.nextNode() as Text | null;
|
|
36
|
+
|
|
37
|
+
// Skip text nodes inside nested lists
|
|
38
|
+
while (textNode) {
|
|
39
|
+
let parent: Node | null = textNode.parentNode;
|
|
40
|
+
let inNestedList = false;
|
|
41
|
+
while (parent && parent !== li) {
|
|
42
|
+
if (parent.nodeName === 'UL' || parent.nodeName === 'OL') {
|
|
43
|
+
inNestedList = true;
|
|
44
|
+
break;
|
|
45
|
+
}
|
|
46
|
+
parent = parent.parentNode;
|
|
47
|
+
}
|
|
48
|
+
if (!inNestedList) break;
|
|
49
|
+
textNode = walker.nextNode() as Text | null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (textNode) {
|
|
53
|
+
editor.setCursor(textNode, Math.min(offset, textNode.textContent?.length || 0));
|
|
54
|
+
} else {
|
|
55
|
+
// If no text node, set cursor at the li itself
|
|
56
|
+
const range = document.createRange();
|
|
57
|
+
range.setStart(li, 0);
|
|
58
|
+
range.collapse(true);
|
|
59
|
+
const sel = window.getSelection();
|
|
60
|
+
sel?.removeAllRanges();
|
|
61
|
+
sel?.addRange(range);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
describe('toggleUnorderedList', () => {
|
|
66
|
+
it('converts paragraph to unordered list', () => {
|
|
67
|
+
const lists = createLists();
|
|
68
|
+
editor.setCursorInBlock(0, 0);
|
|
69
|
+
|
|
70
|
+
lists.toggleUnorderedList();
|
|
71
|
+
|
|
72
|
+
expect(editor.getHtml()).toBe('<ul><li>Hello world</li></ul>');
|
|
73
|
+
expect(onContentChange).toHaveBeenCalled();
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('converts unordered list back to paragraph', () => {
|
|
77
|
+
editor = createTestEditor('<ul><li>Hello world</li></ul>');
|
|
78
|
+
const lists = createLists();
|
|
79
|
+
const li = editor.container.querySelector('li')!;
|
|
80
|
+
setCursorInListItem(li, 0);
|
|
81
|
+
|
|
82
|
+
lists.toggleUnorderedList();
|
|
83
|
+
|
|
84
|
+
expect(editor.getHtml()).toBe('<p>Hello world</p>');
|
|
85
|
+
expect(onContentChange).toHaveBeenCalled();
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('converts ordered list to unordered list', () => {
|
|
89
|
+
editor = createTestEditor('<ol><li>Item one</li></ol>');
|
|
90
|
+
const lists = createLists();
|
|
91
|
+
const li = editor.container.querySelector('li')!;
|
|
92
|
+
setCursorInListItem(li, 0);
|
|
93
|
+
|
|
94
|
+
lists.toggleUnorderedList();
|
|
95
|
+
|
|
96
|
+
expect(editor.getHtml()).toBe('<ul><li>Item one</li></ul>');
|
|
97
|
+
expect(onContentChange).toHaveBeenCalled();
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('converts multi-item ordered list to unordered list', () => {
|
|
101
|
+
editor = createTestEditor('<ol><li>First</li><li>Second</li><li>Third</li></ol>');
|
|
102
|
+
const lists = createLists();
|
|
103
|
+
const firstLi = editor.container.querySelector('li')!;
|
|
104
|
+
setCursorInListItem(firstLi, 0);
|
|
105
|
+
|
|
106
|
+
lists.toggleUnorderedList();
|
|
107
|
+
|
|
108
|
+
const html = editor.getHtml();
|
|
109
|
+
expect(html).toContain('<ul>');
|
|
110
|
+
expect(html).not.toContain('<ol>');
|
|
111
|
+
expect(editor.container.querySelectorAll('li').length).toBe(3);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it('preserves content when toggling', () => {
|
|
115
|
+
editor = createTestEditor('<p>Content with <strong>bold</strong> text</p>');
|
|
116
|
+
const lists = createLists();
|
|
117
|
+
editor.setCursorInBlock(0, 0);
|
|
118
|
+
|
|
119
|
+
lists.toggleUnorderedList();
|
|
120
|
+
|
|
121
|
+
expect(editor.container.querySelector('li strong')).not.toBeNull();
|
|
122
|
+
expect(editor.container.textContent).toContain('Content with bold text');
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
describe('toggleOrderedList', () => {
|
|
127
|
+
it('converts paragraph to ordered list', () => {
|
|
128
|
+
const lists = createLists();
|
|
129
|
+
editor.setCursorInBlock(0, 0);
|
|
130
|
+
|
|
131
|
+
lists.toggleOrderedList();
|
|
132
|
+
|
|
133
|
+
expect(editor.getHtml()).toBe('<ol><li>Hello world</li></ol>');
|
|
134
|
+
expect(onContentChange).toHaveBeenCalled();
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it('converts ordered list back to paragraph', () => {
|
|
138
|
+
editor = createTestEditor('<ol><li>Numbered item</li></ol>');
|
|
139
|
+
const lists = createLists();
|
|
140
|
+
const li = editor.container.querySelector('li')!;
|
|
141
|
+
setCursorInListItem(li, 0);
|
|
142
|
+
|
|
143
|
+
lists.toggleOrderedList();
|
|
144
|
+
|
|
145
|
+
expect(editor.getHtml()).toBe('<p>Numbered item</p>');
|
|
146
|
+
expect(onContentChange).toHaveBeenCalled();
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it('converts unordered list to ordered list', () => {
|
|
150
|
+
editor = createTestEditor('<ul><li>Bullet item</li></ul>');
|
|
151
|
+
const lists = createLists();
|
|
152
|
+
const li = editor.container.querySelector('li')!;
|
|
153
|
+
setCursorInListItem(li, 0);
|
|
154
|
+
|
|
155
|
+
lists.toggleOrderedList();
|
|
156
|
+
|
|
157
|
+
expect(editor.getHtml()).toBe('<ol><li>Bullet item</li></ol>');
|
|
158
|
+
expect(onContentChange).toHaveBeenCalled();
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it('converts multi-item unordered list to ordered list', () => {
|
|
162
|
+
editor = createTestEditor('<ul><li>One</li><li>Two</li></ul>');
|
|
163
|
+
const lists = createLists();
|
|
164
|
+
const li = editor.container.querySelector('li')!;
|
|
165
|
+
setCursorInListItem(li, 0);
|
|
166
|
+
|
|
167
|
+
lists.toggleOrderedList();
|
|
168
|
+
|
|
169
|
+
const html = editor.getHtml();
|
|
170
|
+
expect(html).toContain('<ol>');
|
|
171
|
+
expect(html).not.toContain('<ul>');
|
|
172
|
+
expect(editor.container.querySelectorAll('li').length).toBe(2);
|
|
173
|
+
});
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
describe('checkAndConvertListPattern', () => {
|
|
177
|
+
it('converts "- item" to unordered list', () => {
|
|
178
|
+
editor = createTestEditor('<p>- my item</p>');
|
|
179
|
+
const lists = createLists();
|
|
180
|
+
editor.setCursorInBlock(0, 9);
|
|
181
|
+
|
|
182
|
+
const converted = lists.checkAndConvertListPattern();
|
|
183
|
+
|
|
184
|
+
expect(converted).toBe(true);
|
|
185
|
+
expect(editor.getHtml()).toBe('<ul><li>my item</li></ul>');
|
|
186
|
+
expect(onContentChange).toHaveBeenCalled();
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it('converts "* item" to unordered list', () => {
|
|
190
|
+
editor = createTestEditor('<p>* bullet point</p>');
|
|
191
|
+
const lists = createLists();
|
|
192
|
+
editor.setCursorInBlock(0, 14);
|
|
193
|
+
|
|
194
|
+
const converted = lists.checkAndConvertListPattern();
|
|
195
|
+
|
|
196
|
+
expect(converted).toBe(true);
|
|
197
|
+
expect(editor.container.querySelector('ul')).not.toBeNull();
|
|
198
|
+
expect(editor.container.querySelector('li')?.textContent).toBe('bullet point');
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
it('converts "+ item" to unordered list', () => {
|
|
202
|
+
editor = createTestEditor('<p>+ plus item</p>');
|
|
203
|
+
const lists = createLists();
|
|
204
|
+
editor.setCursorInBlock(0, 11);
|
|
205
|
+
|
|
206
|
+
const converted = lists.checkAndConvertListPattern();
|
|
207
|
+
|
|
208
|
+
expect(converted).toBe(true);
|
|
209
|
+
expect(editor.container.querySelector('ul')).not.toBeNull();
|
|
210
|
+
expect(editor.container.querySelector('li')?.textContent).toBe('plus item');
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
it('converts "1. item" to ordered list', () => {
|
|
214
|
+
editor = createTestEditor('<p>1. first item</p>');
|
|
215
|
+
const lists = createLists();
|
|
216
|
+
editor.setCursorInBlock(0, 13);
|
|
217
|
+
|
|
218
|
+
const converted = lists.checkAndConvertListPattern();
|
|
219
|
+
|
|
220
|
+
expect(converted).toBe(true);
|
|
221
|
+
expect(editor.getHtml()).toBe('<ol><li>first item</li></ol>');
|
|
222
|
+
expect(onContentChange).toHaveBeenCalled();
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
it('converts "42. item" to ordered list', () => {
|
|
226
|
+
editor = createTestEditor('<p>42. numbered item</p>');
|
|
227
|
+
const lists = createLists();
|
|
228
|
+
editor.setCursorInBlock(0, 17);
|
|
229
|
+
|
|
230
|
+
const converted = lists.checkAndConvertListPattern();
|
|
231
|
+
|
|
232
|
+
expect(converted).toBe(true);
|
|
233
|
+
expect(editor.container.querySelector('ol')).not.toBeNull();
|
|
234
|
+
expect(editor.container.querySelector('li')?.textContent).toBe('numbered item');
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
it('does not convert text without list pattern', () => {
|
|
238
|
+
editor = createTestEditor('<p>Normal paragraph</p>');
|
|
239
|
+
const lists = createLists();
|
|
240
|
+
editor.setCursorInBlock(0, 0);
|
|
241
|
+
|
|
242
|
+
const converted = lists.checkAndConvertListPattern();
|
|
243
|
+
|
|
244
|
+
expect(converted).toBe(false);
|
|
245
|
+
expect(editor.getHtml()).toBe('<p>Normal paragraph</p>');
|
|
246
|
+
expect(onContentChange).not.toHaveBeenCalled();
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
it('does not convert existing list items', () => {
|
|
250
|
+
editor = createTestEditor('<ul><li>- nested dash</li></ul>');
|
|
251
|
+
const lists = createLists();
|
|
252
|
+
const li = editor.container.querySelector('li')!;
|
|
253
|
+
setCursorInListItem(li, 0);
|
|
254
|
+
|
|
255
|
+
const converted = lists.checkAndConvertListPattern();
|
|
256
|
+
|
|
257
|
+
expect(converted).toBe(false);
|
|
258
|
+
// Should still be a single list
|
|
259
|
+
expect(editor.container.querySelectorAll('ul').length).toBe(1);
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
it('handles empty content after pattern', () => {
|
|
263
|
+
editor = createTestEditor('<p>- </p>');
|
|
264
|
+
const lists = createLists();
|
|
265
|
+
editor.setCursorInBlock(0, 2);
|
|
266
|
+
|
|
267
|
+
const converted = lists.checkAndConvertListPattern();
|
|
268
|
+
|
|
269
|
+
expect(converted).toBe(true);
|
|
270
|
+
expect(editor.container.querySelector('ul')).not.toBeNull();
|
|
271
|
+
expect(editor.container.querySelector('li')?.textContent).toBe('');
|
|
272
|
+
});
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
describe('handleListEnter', () => {
|
|
276
|
+
it('creates new list item when current item has content', () => {
|
|
277
|
+
editor = createTestEditor('<ul><li>Item 1</li></ul>');
|
|
278
|
+
const lists = createLists();
|
|
279
|
+
const li = editor.container.querySelector('li')!;
|
|
280
|
+
setCursorInListItem(li, 6); // End of "Item 1"
|
|
281
|
+
|
|
282
|
+
const handled = lists.handleListEnter();
|
|
283
|
+
|
|
284
|
+
expect(handled).toBe(true);
|
|
285
|
+
expect(editor.container.querySelectorAll('li').length).toBe(2);
|
|
286
|
+
expect(onContentChange).toHaveBeenCalled();
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
it('splits content when cursor is in middle', () => {
|
|
290
|
+
editor = createTestEditor('<ul><li>Hello World</li></ul>');
|
|
291
|
+
const lists = createLists();
|
|
292
|
+
const li = editor.container.querySelector('li')!;
|
|
293
|
+
setCursorInListItem(li, 5); // After "Hello"
|
|
294
|
+
|
|
295
|
+
lists.handleListEnter();
|
|
296
|
+
|
|
297
|
+
const items = editor.container.querySelectorAll('li');
|
|
298
|
+
expect(items.length).toBe(2);
|
|
299
|
+
expect(items[0].textContent).toBe('Hello');
|
|
300
|
+
expect(items[1].textContent).toBe(' World');
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
it('exits list when item is empty at top level', () => {
|
|
304
|
+
editor = createTestEditor('<ul><li>Item 1</li><li></li></ul>');
|
|
305
|
+
const lists = createLists();
|
|
306
|
+
const emptyLi = editor.container.querySelectorAll('li')[1];
|
|
307
|
+
setCursorInListItem(emptyLi, 0);
|
|
308
|
+
|
|
309
|
+
lists.handleListEnter();
|
|
310
|
+
|
|
311
|
+
// Should convert empty li to paragraph
|
|
312
|
+
expect(editor.container.querySelector('p')).not.toBeNull();
|
|
313
|
+
expect(onContentChange).toHaveBeenCalled();
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
it('exits list and removes list when only empty item remains', () => {
|
|
317
|
+
editor = createTestEditor('<ul><li></li></ul>');
|
|
318
|
+
const lists = createLists();
|
|
319
|
+
const li = editor.container.querySelector('li')!;
|
|
320
|
+
setCursorInListItem(li, 0);
|
|
321
|
+
|
|
322
|
+
lists.handleListEnter();
|
|
323
|
+
|
|
324
|
+
// List should be gone, replaced with paragraph
|
|
325
|
+
expect(editor.container.querySelector('ul')).toBeNull();
|
|
326
|
+
expect(editor.container.querySelector('p')).not.toBeNull();
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
it('outdents when empty nested item', () => {
|
|
330
|
+
editor = createTestEditor('<ul><li>Parent<ul><li></li></ul></li></ul>');
|
|
331
|
+
const lists = createLists();
|
|
332
|
+
const nestedLi = editor.container.querySelector('ul ul li')!;
|
|
333
|
+
setCursorInListItem(nestedLi as HTMLLIElement, 0);
|
|
334
|
+
|
|
335
|
+
lists.handleListEnter();
|
|
336
|
+
|
|
337
|
+
// Should outdent the empty nested item
|
|
338
|
+
expect(onContentChange).toHaveBeenCalled();
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
it('returns false when not in a list', () => {
|
|
342
|
+
editor = createTestEditor('<p>Not a list</p>');
|
|
343
|
+
const lists = createLists();
|
|
344
|
+
editor.setCursorInBlock(0, 0);
|
|
345
|
+
|
|
346
|
+
const handled = lists.handleListEnter();
|
|
347
|
+
|
|
348
|
+
expect(handled).toBe(false);
|
|
349
|
+
expect(onContentChange).not.toHaveBeenCalled();
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
it('works with ordered lists', () => {
|
|
353
|
+
editor = createTestEditor('<ol><li>First</li></ol>');
|
|
354
|
+
const lists = createLists();
|
|
355
|
+
const li = editor.container.querySelector('li')!;
|
|
356
|
+
setCursorInListItem(li, 5);
|
|
357
|
+
|
|
358
|
+
lists.handleListEnter();
|
|
359
|
+
|
|
360
|
+
expect(editor.container.querySelectorAll('ol li').length).toBe(2);
|
|
361
|
+
});
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
describe('indentListItem', () => {
|
|
365
|
+
it('indents second item under first', () => {
|
|
366
|
+
editor = createTestEditor('<ul><li>First</li><li>Second</li></ul>');
|
|
367
|
+
const lists = createLists();
|
|
368
|
+
const secondLi = editor.container.querySelectorAll('li')[1];
|
|
369
|
+
setCursorInListItem(secondLi as HTMLLIElement, 3);
|
|
370
|
+
|
|
371
|
+
const handled = lists.indentListItem();
|
|
372
|
+
|
|
373
|
+
expect(handled).toBe(true);
|
|
374
|
+
expect(editor.getHtml()).toContain('<li>First<ul><li>Second</li></ul></li>');
|
|
375
|
+
expect(onContentChange).toHaveBeenCalled();
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
it('cannot indent first item (no previous sibling)', () => {
|
|
379
|
+
editor = createTestEditor('<ul><li>First</li><li>Second</li></ul>');
|
|
380
|
+
const lists = createLists();
|
|
381
|
+
const firstLi = editor.container.querySelector('li')!;
|
|
382
|
+
setCursorInListItem(firstLi, 0);
|
|
383
|
+
|
|
384
|
+
const handled = lists.indentListItem();
|
|
385
|
+
|
|
386
|
+
expect(handled).toBe(false);
|
|
387
|
+
expect(onContentChange).not.toHaveBeenCalled();
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
it('preserves cursor position after indent', () => {
|
|
391
|
+
editor = createTestEditor('<ul><li>First</li><li>Second</li></ul>');
|
|
392
|
+
const lists = createLists();
|
|
393
|
+
const secondLi = editor.container.querySelectorAll('li')[1];
|
|
394
|
+
setCursorInListItem(secondLi as HTMLLIElement, 3); // In middle of "Second"
|
|
395
|
+
|
|
396
|
+
lists.indentListItem();
|
|
397
|
+
|
|
398
|
+
// Cursor should still be within the text
|
|
399
|
+
const sel = window.getSelection();
|
|
400
|
+
expect(sel?.rangeCount).toBeGreaterThan(0);
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
it('appends to existing nested list', () => {
|
|
404
|
+
editor = createTestEditor('<ul><li>First<ul><li>Nested</li></ul></li><li>Third</li></ul>');
|
|
405
|
+
const lists = createLists();
|
|
406
|
+
const thirdLi = editor.container.querySelectorAll(':scope > ul > li')[1] as HTMLLIElement;
|
|
407
|
+
setCursorInListItem(thirdLi, 0);
|
|
408
|
+
|
|
409
|
+
lists.indentListItem();
|
|
410
|
+
|
|
411
|
+
// Third should now be in the nested list
|
|
412
|
+
const nestedItems = editor.container.querySelectorAll('ul ul li');
|
|
413
|
+
expect(nestedItems.length).toBe(2);
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
it('preserves list type when indenting', () => {
|
|
417
|
+
editor = createTestEditor('<ol><li>First</li><li>Second</li></ol>');
|
|
418
|
+
const lists = createLists();
|
|
419
|
+
const secondLi = editor.container.querySelectorAll('li')[1];
|
|
420
|
+
setCursorInListItem(secondLi as HTMLLIElement, 0);
|
|
421
|
+
|
|
422
|
+
lists.indentListItem();
|
|
423
|
+
|
|
424
|
+
// Nested list should also be ordered
|
|
425
|
+
expect(editor.container.querySelector('ol ol')).not.toBeNull();
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
it('returns false when not in a list', () => {
|
|
429
|
+
editor = createTestEditor('<p>Paragraph</p>');
|
|
430
|
+
const lists = createLists();
|
|
431
|
+
editor.setCursorInBlock(0, 0);
|
|
432
|
+
|
|
433
|
+
const handled = lists.indentListItem();
|
|
434
|
+
|
|
435
|
+
expect(handled).toBe(false);
|
|
436
|
+
});
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
describe('outdentListItem', () => {
|
|
440
|
+
it('outdents nested item to parent level', () => {
|
|
441
|
+
editor = createTestEditor('<ul><li>Parent<ul><li>Nested</li></ul></li></ul>');
|
|
442
|
+
const lists = createLists();
|
|
443
|
+
const nestedLi = editor.container.querySelector('ul ul li')!;
|
|
444
|
+
setCursorInListItem(nestedLi as HTMLLIElement, 0);
|
|
445
|
+
|
|
446
|
+
const handled = lists.outdentListItem();
|
|
447
|
+
|
|
448
|
+
expect(handled).toBe(true);
|
|
449
|
+
// Nested item should now be at top level
|
|
450
|
+
const topLevelItems = editor.container.querySelectorAll(':scope > ul > li');
|
|
451
|
+
expect(topLevelItems.length).toBe(2);
|
|
452
|
+
expect(onContentChange).toHaveBeenCalled();
|
|
453
|
+
});
|
|
454
|
+
|
|
455
|
+
it('converts top-level item to paragraph', () => {
|
|
456
|
+
editor = createTestEditor('<ul><li>Item</li></ul>');
|
|
457
|
+
const lists = createLists();
|
|
458
|
+
const li = editor.container.querySelector('li')!;
|
|
459
|
+
setCursorInListItem(li, 0);
|
|
460
|
+
|
|
461
|
+
const handled = lists.outdentListItem();
|
|
462
|
+
|
|
463
|
+
expect(handled).toBe(true);
|
|
464
|
+
expect(editor.container.querySelector('ul')).toBeNull();
|
|
465
|
+
expect(editor.container.querySelector('p')).not.toBeNull();
|
|
466
|
+
expect(editor.container.querySelector('p')?.textContent).toBe('Item');
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
it('preserves cursor position after outdent', () => {
|
|
470
|
+
editor = createTestEditor('<ul><li>Parent<ul><li>Nested</li></ul></li></ul>');
|
|
471
|
+
const lists = createLists();
|
|
472
|
+
const nestedLi = editor.container.querySelector('ul ul li')!;
|
|
473
|
+
setCursorInListItem(nestedLi as HTMLLIElement, 3);
|
|
474
|
+
|
|
475
|
+
lists.outdentListItem();
|
|
476
|
+
|
|
477
|
+
// Cursor should still be within text
|
|
478
|
+
const sel = window.getSelection();
|
|
479
|
+
expect(sel?.rangeCount).toBeGreaterThan(0);
|
|
480
|
+
});
|
|
481
|
+
|
|
482
|
+
it('moves following siblings to nested list in current item', () => {
|
|
483
|
+
editor = createTestEditor('<ul><li>Parent<ul><li>First</li><li>Second</li><li>Third</li></ul></li></ul>');
|
|
484
|
+
const lists = createLists();
|
|
485
|
+
// Outdent "Second" - "Third" should become nested under it
|
|
486
|
+
const secondLi = editor.container.querySelectorAll('ul ul li')[1];
|
|
487
|
+
setCursorInListItem(secondLi as HTMLLIElement, 0);
|
|
488
|
+
|
|
489
|
+
lists.outdentListItem();
|
|
490
|
+
|
|
491
|
+
// After outdent, "Second" should be at parent level with "Third" nested under it
|
|
492
|
+
const secondNowTop = editor.container.querySelectorAll(':scope > ul > li')[1];
|
|
493
|
+
expect(secondNowTop?.textContent).toContain('Second');
|
|
494
|
+
expect(secondNowTop?.textContent).toContain('Third');
|
|
495
|
+
});
|
|
496
|
+
|
|
497
|
+
it('cleans up empty parent list after outdent', () => {
|
|
498
|
+
editor = createTestEditor('<ul><li>Parent<ul><li>Only child</li></ul></li></ul>');
|
|
499
|
+
const lists = createLists();
|
|
500
|
+
const nestedLi = editor.container.querySelector('ul ul li')!;
|
|
501
|
+
setCursorInListItem(nestedLi as HTMLLIElement, 0);
|
|
502
|
+
|
|
503
|
+
lists.outdentListItem();
|
|
504
|
+
|
|
505
|
+
// The nested ul should be removed since it's empty
|
|
506
|
+
const nestedLists = editor.container.querySelectorAll('ul ul');
|
|
507
|
+
expect(nestedLists.length).toBe(0);
|
|
508
|
+
});
|
|
509
|
+
|
|
510
|
+
it('returns false when not in a list', () => {
|
|
511
|
+
editor = createTestEditor('<p>Paragraph</p>');
|
|
512
|
+
const lists = createLists();
|
|
513
|
+
editor.setCursorInBlock(0, 0);
|
|
514
|
+
|
|
515
|
+
const handled = lists.outdentListItem();
|
|
516
|
+
|
|
517
|
+
expect(handled).toBe(false);
|
|
518
|
+
});
|
|
519
|
+
});
|
|
520
|
+
|
|
521
|
+
describe('getCurrentListType', () => {
|
|
522
|
+
it('returns "ul" when in unordered list', () => {
|
|
523
|
+
editor = createTestEditor('<ul><li>Bullet item</li></ul>');
|
|
524
|
+
const lists = createLists();
|
|
525
|
+
const li = editor.container.querySelector('li')!;
|
|
526
|
+
setCursorInListItem(li, 0);
|
|
527
|
+
|
|
528
|
+
expect(lists.getCurrentListType()).toBe('ul');
|
|
529
|
+
});
|
|
530
|
+
|
|
531
|
+
it('returns "ol" when in ordered list', () => {
|
|
532
|
+
editor = createTestEditor('<ol><li>Numbered item</li></ol>');
|
|
533
|
+
const lists = createLists();
|
|
534
|
+
const li = editor.container.querySelector('li')!;
|
|
535
|
+
setCursorInListItem(li, 0);
|
|
536
|
+
|
|
537
|
+
expect(lists.getCurrentListType()).toBe('ol');
|
|
538
|
+
});
|
|
539
|
+
|
|
540
|
+
it('returns null when not in list', () => {
|
|
541
|
+
editor = createTestEditor('<p>Paragraph</p>');
|
|
542
|
+
const lists = createLists();
|
|
543
|
+
editor.setCursorInBlock(0, 0);
|
|
544
|
+
|
|
545
|
+
expect(lists.getCurrentListType()).toBeNull();
|
|
546
|
+
});
|
|
547
|
+
|
|
548
|
+
it('returns null when in heading', () => {
|
|
549
|
+
editor = createTestEditor('<h1>Heading</h1>');
|
|
550
|
+
const lists = createLists();
|
|
551
|
+
editor.setCursorInBlock(0, 0);
|
|
552
|
+
|
|
553
|
+
expect(lists.getCurrentListType()).toBeNull();
|
|
554
|
+
});
|
|
555
|
+
|
|
556
|
+
it('returns correct type for nested list', () => {
|
|
557
|
+
editor = createTestEditor('<ul><li>Parent<ol><li>Nested ordered</li></ol></li></ul>');
|
|
558
|
+
const lists = createLists();
|
|
559
|
+
const nestedLi = editor.container.querySelector('ol li')!;
|
|
560
|
+
setCursorInListItem(nestedLi as HTMLLIElement, 0);
|
|
561
|
+
|
|
562
|
+
// Should return the immediate parent list type
|
|
563
|
+
expect(lists.getCurrentListType()).toBe('ol');
|
|
564
|
+
});
|
|
565
|
+
});
|
|
566
|
+
|
|
567
|
+
describe('list splitting behavior', () => {
|
|
568
|
+
it('splits list when converting middle item to paragraph', () => {
|
|
569
|
+
editor = createTestEditor('<ul><li>First</li><li>Second</li><li>Third</li></ul>');
|
|
570
|
+
const lists = createLists();
|
|
571
|
+
const secondLi = editor.container.querySelectorAll('li')[1];
|
|
572
|
+
setCursorInListItem(secondLi as HTMLLIElement, 0);
|
|
573
|
+
|
|
574
|
+
lists.toggleUnorderedList();
|
|
575
|
+
|
|
576
|
+
// Should have: ul with First, p with Second, ul with Third
|
|
577
|
+
const paragraphs = editor.container.querySelectorAll('p');
|
|
578
|
+
expect(paragraphs.length).toBe(1);
|
|
579
|
+
expect(paragraphs[0].textContent).toBe('Second');
|
|
580
|
+
|
|
581
|
+
const lists2 = editor.container.querySelectorAll('ul');
|
|
582
|
+
expect(lists2.length).toBe(2);
|
|
583
|
+
});
|
|
584
|
+
|
|
585
|
+
it('handles converting first item of multi-item list', () => {
|
|
586
|
+
editor = createTestEditor('<ul><li>First</li><li>Second</li></ul>');
|
|
587
|
+
const lists = createLists();
|
|
588
|
+
const firstLi = editor.container.querySelector('li')!;
|
|
589
|
+
setCursorInListItem(firstLi, 0);
|
|
590
|
+
|
|
591
|
+
lists.toggleUnorderedList();
|
|
592
|
+
|
|
593
|
+
// Should have: p with First, ul with Second
|
|
594
|
+
expect(editor.container.querySelector('p')?.textContent).toBe('First');
|
|
595
|
+
expect(editor.container.querySelectorAll('li').length).toBe(1);
|
|
596
|
+
});
|
|
597
|
+
|
|
598
|
+
it('handles converting last item of multi-item list', () => {
|
|
599
|
+
editor = createTestEditor('<ul><li>First</li><li>Last</li></ul>');
|
|
600
|
+
const lists = createLists();
|
|
601
|
+
const lastLi = editor.container.querySelectorAll('li')[1];
|
|
602
|
+
setCursorInListItem(lastLi as HTMLLIElement, 0);
|
|
603
|
+
|
|
604
|
+
lists.toggleUnorderedList();
|
|
605
|
+
|
|
606
|
+
// Should have: ul with First, p with Last
|
|
607
|
+
expect(editor.container.querySelectorAll('li').length).toBe(1);
|
|
608
|
+
expect(editor.container.querySelector('li')?.textContent).toBe('First');
|
|
609
|
+
expect(editor.container.querySelector('p')?.textContent).toBe('Last');
|
|
610
|
+
});
|
|
611
|
+
});
|
|
612
|
+
|
|
613
|
+
describe('heading to list conversion', () => {
|
|
614
|
+
it('converts H1 to unordered list', () => {
|
|
615
|
+
editor = createTestEditor('<h1>Heading One</h1>');
|
|
616
|
+
const lists = createLists();
|
|
617
|
+
editor.setCursorInBlock(0, 0);
|
|
618
|
+
|
|
619
|
+
lists.toggleUnorderedList();
|
|
620
|
+
|
|
621
|
+
expect(editor.getHtml()).toBe('<ul><li>Heading One</li></ul>');
|
|
622
|
+
expect(onContentChange).toHaveBeenCalled();
|
|
623
|
+
});
|
|
624
|
+
|
|
625
|
+
it('converts H2 to ordered list', () => {
|
|
626
|
+
editor = createTestEditor('<h2>Heading Two</h2>');
|
|
627
|
+
const lists = createLists();
|
|
628
|
+
editor.setCursorInBlock(0, 0);
|
|
629
|
+
|
|
630
|
+
lists.toggleOrderedList();
|
|
631
|
+
|
|
632
|
+
expect(editor.getHtml()).toBe('<ol><li>Heading Two</li></ol>');
|
|
633
|
+
expect(onContentChange).toHaveBeenCalled();
|
|
634
|
+
});
|
|
635
|
+
|
|
636
|
+
it('converts H3 to unordered list via hotkey', () => {
|
|
637
|
+
editor = createTestEditor('<h3>Heading Three</h3>');
|
|
638
|
+
const lists = createLists();
|
|
639
|
+
editor.setCursorInBlock(0, 0);
|
|
640
|
+
|
|
641
|
+
lists.toggleUnorderedList();
|
|
642
|
+
|
|
643
|
+
expect(editor.container.querySelector('ul')).not.toBeNull();
|
|
644
|
+
expect(editor.container.querySelector('li')?.textContent).toBe('Heading Three');
|
|
645
|
+
});
|
|
646
|
+
|
|
647
|
+
it('converts H4 to ordered list via hotkey', () => {
|
|
648
|
+
editor = createTestEditor('<h4>Heading Four</h4>');
|
|
649
|
+
const lists = createLists();
|
|
650
|
+
editor.setCursorInBlock(0, 0);
|
|
651
|
+
|
|
652
|
+
lists.toggleOrderedList();
|
|
653
|
+
|
|
654
|
+
expect(editor.container.querySelector('ol')).not.toBeNull();
|
|
655
|
+
expect(editor.container.querySelector('li')?.textContent).toBe('Heading Four');
|
|
656
|
+
});
|
|
657
|
+
|
|
658
|
+
it('converts H5 heading to list via "- " pattern', () => {
|
|
659
|
+
editor = createTestEditor('<h5>- item from heading</h5>');
|
|
660
|
+
const lists = createLists();
|
|
661
|
+
editor.setCursorInBlock(0, 20);
|
|
662
|
+
|
|
663
|
+
const converted = lists.checkAndConvertListPattern();
|
|
664
|
+
|
|
665
|
+
expect(converted).toBe(true);
|
|
666
|
+
expect(editor.container.querySelector('ul')).not.toBeNull();
|
|
667
|
+
expect(editor.container.querySelector('li')?.textContent).toBe('item from heading');
|
|
668
|
+
});
|
|
669
|
+
|
|
670
|
+
it('converts H6 heading to list via "1. " pattern', () => {
|
|
671
|
+
editor = createTestEditor('<h6>1. numbered from heading</h6>');
|
|
672
|
+
const lists = createLists();
|
|
673
|
+
editor.setCursorInBlock(0, 25);
|
|
674
|
+
|
|
675
|
+
const converted = lists.checkAndConvertListPattern();
|
|
676
|
+
|
|
677
|
+
expect(converted).toBe(true);
|
|
678
|
+
expect(editor.container.querySelector('ol')).not.toBeNull();
|
|
679
|
+
expect(editor.container.querySelector('li')?.textContent).toBe('numbered from heading');
|
|
680
|
+
});
|
|
681
|
+
|
|
682
|
+
it('converts list back to paragraph, not heading', () => {
|
|
683
|
+
// Start with a heading, convert to list, then back
|
|
684
|
+
editor = createTestEditor('<h1>Original Heading</h1>');
|
|
685
|
+
const lists = createLists();
|
|
686
|
+
editor.setCursorInBlock(0, 0);
|
|
687
|
+
|
|
688
|
+
// Convert heading to list
|
|
689
|
+
lists.toggleUnorderedList();
|
|
690
|
+
expect(editor.container.querySelector('ul')).not.toBeNull();
|
|
691
|
+
|
|
692
|
+
// Now convert back - should become a paragraph, not a heading
|
|
693
|
+
const li = editor.container.querySelector('li')!;
|
|
694
|
+
setCursorInListItem(li, 0);
|
|
695
|
+
lists.toggleUnorderedList();
|
|
696
|
+
|
|
697
|
+
// Should be a paragraph now
|
|
698
|
+
expect(editor.container.querySelector('p')).not.toBeNull();
|
|
699
|
+
expect(editor.container.querySelector('h1')).toBeNull();
|
|
700
|
+
expect(editor.container.querySelector('p')?.textContent).toBe('Original Heading');
|
|
701
|
+
});
|
|
702
|
+
|
|
703
|
+
it('preserves heading content with formatting when converting to list', () => {
|
|
704
|
+
editor = createTestEditor('<h2>Heading with <strong>bold</strong> text</h2>');
|
|
705
|
+
const lists = createLists();
|
|
706
|
+
editor.setCursorInBlock(0, 0);
|
|
707
|
+
|
|
708
|
+
lists.toggleUnorderedList();
|
|
709
|
+
|
|
710
|
+
expect(editor.container.querySelector('li strong')).not.toBeNull();
|
|
711
|
+
expect(editor.container.textContent).toContain('Heading with bold text');
|
|
712
|
+
});
|
|
713
|
+
});
|
|
714
|
+
|
|
715
|
+
describe('convertCurrentListItemToParagraph', () => {
|
|
716
|
+
it('converts unordered list item to paragraph', () => {
|
|
717
|
+
editor = createTestEditor('<ul><li>List item</li></ul>');
|
|
718
|
+
const lists = createLists();
|
|
719
|
+
const li = editor.container.querySelector('li')!;
|
|
720
|
+
setCursorInListItem(li, 0);
|
|
721
|
+
|
|
722
|
+
const result = lists.convertCurrentListItemToParagraph();
|
|
723
|
+
|
|
724
|
+
expect(result).not.toBeNull();
|
|
725
|
+
expect(editor.getHtml()).toBe('<p>List item</p>');
|
|
726
|
+
expect(onContentChange).toHaveBeenCalled();
|
|
727
|
+
});
|
|
728
|
+
|
|
729
|
+
it('converts ordered list item to paragraph', () => {
|
|
730
|
+
editor = createTestEditor('<ol><li>Numbered item</li></ol>');
|
|
731
|
+
const lists = createLists();
|
|
732
|
+
const li = editor.container.querySelector('li')!;
|
|
733
|
+
setCursorInListItem(li, 0);
|
|
734
|
+
|
|
735
|
+
const result = lists.convertCurrentListItemToParagraph();
|
|
736
|
+
|
|
737
|
+
expect(result).not.toBeNull();
|
|
738
|
+
expect(editor.getHtml()).toBe('<p>Numbered item</p>');
|
|
739
|
+
expect(onContentChange).toHaveBeenCalled();
|
|
740
|
+
});
|
|
741
|
+
|
|
742
|
+
it('returns null when not in a list', () => {
|
|
743
|
+
editor = createTestEditor('<p>Paragraph</p>');
|
|
744
|
+
const lists = createLists();
|
|
745
|
+
editor.setCursorInBlock(0, 0);
|
|
746
|
+
|
|
747
|
+
const result = lists.convertCurrentListItemToParagraph();
|
|
748
|
+
|
|
749
|
+
expect(result).toBeNull();
|
|
750
|
+
expect(onContentChange).not.toHaveBeenCalled();
|
|
751
|
+
});
|
|
752
|
+
|
|
753
|
+
it('preserves content with formatting', () => {
|
|
754
|
+
editor = createTestEditor('<ul><li>Item with <strong>bold</strong> text</li></ul>');
|
|
755
|
+
const lists = createLists();
|
|
756
|
+
const li = editor.container.querySelector('li')!;
|
|
757
|
+
setCursorInListItem(li, 0);
|
|
758
|
+
|
|
759
|
+
lists.convertCurrentListItemToParagraph();
|
|
760
|
+
|
|
761
|
+
expect(editor.container.querySelector('p strong')).not.toBeNull();
|
|
762
|
+
expect(editor.container.textContent).toContain('Item with bold text');
|
|
763
|
+
});
|
|
764
|
+
|
|
765
|
+
it('handles multi-item list - converts only current item', () => {
|
|
766
|
+
editor = createTestEditor('<ul><li>First</li><li>Second</li><li>Third</li></ul>');
|
|
767
|
+
const lists = createLists();
|
|
768
|
+
const secondLi = editor.container.querySelectorAll('li')[1];
|
|
769
|
+
setCursorInListItem(secondLi as HTMLLIElement, 0);
|
|
770
|
+
|
|
771
|
+
lists.convertCurrentListItemToParagraph();
|
|
772
|
+
|
|
773
|
+
// Should have two lists with paragraph in between
|
|
774
|
+
const paragraphs = editor.container.querySelectorAll('p');
|
|
775
|
+
expect(paragraphs.length).toBe(1);
|
|
776
|
+
expect(paragraphs[0].textContent).toBe('Second');
|
|
777
|
+
});
|
|
778
|
+
});
|
|
779
|
+
|
|
780
|
+
describe('edge cases', () => {
|
|
781
|
+
it('handles empty contentRef gracefully', () => {
|
|
782
|
+
const lists = createLists();
|
|
783
|
+
editor.contentRef.value = null;
|
|
784
|
+
|
|
785
|
+
// Should not throw
|
|
786
|
+
expect(() => lists.toggleUnorderedList()).not.toThrow();
|
|
787
|
+
expect(() => lists.toggleOrderedList()).not.toThrow();
|
|
788
|
+
expect(() => lists.checkAndConvertListPattern()).not.toThrow();
|
|
789
|
+
expect(() => lists.handleListEnter()).not.toThrow();
|
|
790
|
+
expect(() => lists.indentListItem()).not.toThrow();
|
|
791
|
+
expect(() => lists.outdentListItem()).not.toThrow();
|
|
792
|
+
expect(lists.getCurrentListType()).toBeNull();
|
|
793
|
+
});
|
|
794
|
+
|
|
795
|
+
it('handles list item with only whitespace as empty', () => {
|
|
796
|
+
editor = createTestEditor('<ul><li> </li></ul>');
|
|
797
|
+
const lists = createLists();
|
|
798
|
+
const li = editor.container.querySelector('li')!;
|
|
799
|
+
setCursorInListItem(li, 0);
|
|
800
|
+
|
|
801
|
+
// Whitespace-only item should be treated as empty for Enter behavior
|
|
802
|
+
lists.handleListEnter();
|
|
803
|
+
|
|
804
|
+
// Should exit the list
|
|
805
|
+
expect(editor.container.querySelector('p')).not.toBeNull();
|
|
806
|
+
});
|
|
807
|
+
|
|
808
|
+
it('handles deeply nested lists', () => {
|
|
809
|
+
editor = createTestEditor('<ul><li>L1<ul><li>L2<ul><li>L3</li></ul></li></ul></li></ul>');
|
|
810
|
+
const lists = createLists();
|
|
811
|
+
const deepLi = editor.container.querySelector('ul ul ul li')!;
|
|
812
|
+
setCursorInListItem(deepLi as HTMLLIElement, 0);
|
|
813
|
+
|
|
814
|
+
// Should be able to outdent from deep nesting
|
|
815
|
+
lists.outdentListItem();
|
|
816
|
+
|
|
817
|
+
// L3 should now be at L2 level
|
|
818
|
+
const l2Items = editor.container.querySelectorAll('ul ul li');
|
|
819
|
+
expect(l2Items.length).toBe(2);
|
|
820
|
+
});
|
|
821
|
+
|
|
822
|
+
it('preserves nested list content when converting parent to paragraph', () => {
|
|
823
|
+
editor = createTestEditor('<ul><li>Parent<ul><li>Nested</li></ul></li></ul>');
|
|
824
|
+
const lists = createLists();
|
|
825
|
+
const parentLi = editor.container.querySelector('li')!;
|
|
826
|
+
setCursorInListItem(parentLi, 0);
|
|
827
|
+
|
|
828
|
+
lists.toggleUnorderedList();
|
|
829
|
+
|
|
830
|
+
// Parent should become paragraph, nested list is removed (per implementation)
|
|
831
|
+
expect(editor.container.querySelector('p')?.textContent).toBe('Parent');
|
|
832
|
+
});
|
|
833
|
+
});
|
|
834
|
+
});
|