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,1601 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
|
|
2
|
+
import { useTables } from "./useTables";
|
|
3
|
+
import { createTestEditor, TestEditorResult } from "../../../test/helpers/editorTestUtils";
|
|
4
|
+
|
|
5
|
+
describe("useTables", () => {
|
|
6
|
+
let editor: TestEditorResult;
|
|
7
|
+
let onContentChange: ReturnType<typeof vi.fn>;
|
|
8
|
+
|
|
9
|
+
beforeEach(() => {
|
|
10
|
+
onContentChange = vi.fn();
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
afterEach(() => {
|
|
14
|
+
if (editor) {
|
|
15
|
+
editor.destroy();
|
|
16
|
+
}
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
function createTables() {
|
|
20
|
+
return useTables({
|
|
21
|
+
contentRef: editor.contentRef,
|
|
22
|
+
onContentChange
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Helper to create a table HTML structure
|
|
28
|
+
*/
|
|
29
|
+
function createTableHtml(rows: number, cols: number, headerContent?: string[], bodyContent?: string[][]): string {
|
|
30
|
+
let html = "<table><thead><tr>";
|
|
31
|
+
for (let c = 0; c < cols; c++) {
|
|
32
|
+
const content = headerContent?.[c] || `Header ${c + 1}`;
|
|
33
|
+
html += `<th>${content}</th>`;
|
|
34
|
+
}
|
|
35
|
+
html += "</tr></thead>";
|
|
36
|
+
|
|
37
|
+
if (rows > 1) {
|
|
38
|
+
html += "<tbody>";
|
|
39
|
+
for (let r = 1; r < rows; r++) {
|
|
40
|
+
html += "<tr>";
|
|
41
|
+
for (let c = 0; c < cols; c++) {
|
|
42
|
+
const content = bodyContent?.[r - 1]?.[c] || `Cell ${r}-${c + 1}`;
|
|
43
|
+
html += `<td>${content}</td>`;
|
|
44
|
+
}
|
|
45
|
+
html += "</tr>";
|
|
46
|
+
}
|
|
47
|
+
html += "</tbody>";
|
|
48
|
+
}
|
|
49
|
+
html += "</table>";
|
|
50
|
+
return html;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Helper to set cursor in a table cell
|
|
55
|
+
*/
|
|
56
|
+
function setCursorInCell(cell: HTMLTableCellElement, offset: number = 0): void {
|
|
57
|
+
const walker = document.createTreeWalker(cell, NodeFilter.SHOW_TEXT);
|
|
58
|
+
const textNode = walker.nextNode() as Text | null;
|
|
59
|
+
|
|
60
|
+
if (textNode) {
|
|
61
|
+
editor.setCursor(textNode, Math.min(offset, textNode.textContent?.length || 0));
|
|
62
|
+
} else {
|
|
63
|
+
// If no text node, set cursor in the cell itself
|
|
64
|
+
const range = document.createRange();
|
|
65
|
+
if (cell.firstChild) {
|
|
66
|
+
range.setStartBefore(cell.firstChild);
|
|
67
|
+
} else {
|
|
68
|
+
range.setStart(cell, 0);
|
|
69
|
+
}
|
|
70
|
+
range.collapse(true);
|
|
71
|
+
const sel = window.getSelection();
|
|
72
|
+
sel?.removeAllRanges();
|
|
73
|
+
sel?.addRange(range);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Helper to get a cell at specific position
|
|
79
|
+
*/
|
|
80
|
+
function getCell(table: HTMLTableElement, rowIndex: number, colIndex: number): HTMLTableCellElement | null {
|
|
81
|
+
const rows: HTMLTableRowElement[] = [];
|
|
82
|
+
if (table.tHead) {
|
|
83
|
+
rows.push(...Array.from(table.tHead.rows));
|
|
84
|
+
}
|
|
85
|
+
for (const tbody of Array.from(table.tBodies)) {
|
|
86
|
+
rows.push(...Array.from(tbody.rows));
|
|
87
|
+
}
|
|
88
|
+
if (rowIndex < 0 || rowIndex >= rows.length) return null;
|
|
89
|
+
const row = rows[rowIndex];
|
|
90
|
+
if (colIndex < 0 || colIndex >= row.cells.length) return null;
|
|
91
|
+
return row.cells[colIndex];
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
describe("isInTable", () => {
|
|
95
|
+
it("returns true when cursor is in a table cell", () => {
|
|
96
|
+
editor = createTestEditor(createTableHtml(2, 2));
|
|
97
|
+
const tables = createTables();
|
|
98
|
+
const cell = editor.container.querySelector("th") as HTMLTableCellElement;
|
|
99
|
+
setCursorInCell(cell);
|
|
100
|
+
|
|
101
|
+
expect(tables.isInTable()).toBe(true);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it("returns true when cursor is in tbody cell", () => {
|
|
105
|
+
editor = createTestEditor(createTableHtml(3, 2));
|
|
106
|
+
const tables = createTables();
|
|
107
|
+
const cell = editor.container.querySelector("td") as HTMLTableCellElement;
|
|
108
|
+
setCursorInCell(cell);
|
|
109
|
+
|
|
110
|
+
expect(tables.isInTable()).toBe(true);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it("returns false when cursor is in a paragraph", () => {
|
|
114
|
+
editor = createTestEditor("<p>Hello world</p>");
|
|
115
|
+
const tables = createTables();
|
|
116
|
+
editor.setCursorInBlock(0, 5);
|
|
117
|
+
|
|
118
|
+
expect(tables.isInTable()).toBe(false);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it("returns false when cursor is outside table but in same container", () => {
|
|
122
|
+
editor = createTestEditor(`<p>Before table</p>${createTableHtml(2, 2)}`);
|
|
123
|
+
const tables = createTables();
|
|
124
|
+
editor.setCursorInBlock(0, 5);
|
|
125
|
+
|
|
126
|
+
expect(tables.isInTable()).toBe(false);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it("returns false when contentRef is null", () => {
|
|
130
|
+
editor = createTestEditor(createTableHtml(2, 2));
|
|
131
|
+
const { isInTable } = useTables({
|
|
132
|
+
contentRef: { value: null },
|
|
133
|
+
onContentChange
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
expect(isInTable()).toBe(false);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it("returns false when no selection exists", () => {
|
|
140
|
+
editor = createTestEditor(createTableHtml(2, 2));
|
|
141
|
+
const tables = createTables();
|
|
142
|
+
window.getSelection()?.removeAllRanges();
|
|
143
|
+
|
|
144
|
+
expect(tables.isInTable()).toBe(false);
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
describe("isInTableCell", () => {
|
|
149
|
+
it("returns true when cursor is in TH", () => {
|
|
150
|
+
editor = createTestEditor(createTableHtml(2, 2));
|
|
151
|
+
const tables = createTables();
|
|
152
|
+
const th = editor.container.querySelector("th") as HTMLTableCellElement;
|
|
153
|
+
setCursorInCell(th);
|
|
154
|
+
|
|
155
|
+
expect(tables.isInTableCell()).toBe(true);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it("returns true when cursor is in TD", () => {
|
|
159
|
+
editor = createTestEditor(createTableHtml(2, 2));
|
|
160
|
+
const tables = createTables();
|
|
161
|
+
const td = editor.container.querySelector("td") as HTMLTableCellElement;
|
|
162
|
+
setCursorInCell(td);
|
|
163
|
+
|
|
164
|
+
expect(tables.isInTableCell()).toBe(true);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it("returns false when cursor is in paragraph", () => {
|
|
168
|
+
editor = createTestEditor("<p>Hello world</p>");
|
|
169
|
+
const tables = createTables();
|
|
170
|
+
editor.setCursorInBlock(0, 5);
|
|
171
|
+
|
|
172
|
+
expect(tables.isInTableCell()).toBe(false);
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it("returns false when contentRef is null", () => {
|
|
176
|
+
editor = createTestEditor(createTableHtml(2, 2));
|
|
177
|
+
const { isInTableCell } = useTables({
|
|
178
|
+
contentRef: { value: null },
|
|
179
|
+
onContentChange
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
expect(isInTableCell()).toBe(false);
|
|
183
|
+
});
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
describe("getCurrentTable", () => {
|
|
187
|
+
it("returns table element when cursor is in table", () => {
|
|
188
|
+
editor = createTestEditor(createTableHtml(2, 2));
|
|
189
|
+
const tables = createTables();
|
|
190
|
+
const th = editor.container.querySelector("th") as HTMLTableCellElement;
|
|
191
|
+
setCursorInCell(th);
|
|
192
|
+
|
|
193
|
+
const result = tables.getCurrentTable();
|
|
194
|
+
expect(result).toBeInstanceOf(HTMLTableElement);
|
|
195
|
+
expect(result).toBe(editor.container.querySelector("table"));
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it("returns null when cursor is outside table", () => {
|
|
199
|
+
editor = createTestEditor("<p>Not in table</p>");
|
|
200
|
+
const tables = createTables();
|
|
201
|
+
editor.setCursorInBlock(0, 0);
|
|
202
|
+
|
|
203
|
+
expect(tables.getCurrentTable()).toBeNull();
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
it("returns null when contentRef is null", () => {
|
|
207
|
+
editor = createTestEditor(createTableHtml(2, 2));
|
|
208
|
+
const { getCurrentTable } = useTables({
|
|
209
|
+
contentRef: { value: null },
|
|
210
|
+
onContentChange
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
expect(getCurrentTable()).toBeNull();
|
|
214
|
+
});
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
describe("getCurrentCell", () => {
|
|
218
|
+
it("returns TH element when cursor is in header", () => {
|
|
219
|
+
editor = createTestEditor(createTableHtml(2, 2));
|
|
220
|
+
const tables = createTables();
|
|
221
|
+
const th = editor.container.querySelector("th") as HTMLTableCellElement;
|
|
222
|
+
setCursorInCell(th);
|
|
223
|
+
|
|
224
|
+
const result = tables.getCurrentCell();
|
|
225
|
+
expect(result).toBeInstanceOf(HTMLTableCellElement);
|
|
226
|
+
expect(result?.tagName).toBe("TH");
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
it("returns TD element when cursor is in body cell", () => {
|
|
230
|
+
editor = createTestEditor(createTableHtml(2, 2));
|
|
231
|
+
const tables = createTables();
|
|
232
|
+
const td = editor.container.querySelector("td") as HTMLTableCellElement;
|
|
233
|
+
setCursorInCell(td);
|
|
234
|
+
|
|
235
|
+
const result = tables.getCurrentCell();
|
|
236
|
+
expect(result).toBeInstanceOf(HTMLTableCellElement);
|
|
237
|
+
expect(result?.tagName).toBe("TD");
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
it("returns null when cursor is outside table", () => {
|
|
241
|
+
editor = createTestEditor("<p>Not in table</p>");
|
|
242
|
+
const tables = createTables();
|
|
243
|
+
editor.setCursorInBlock(0, 0);
|
|
244
|
+
|
|
245
|
+
expect(tables.getCurrentCell()).toBeNull();
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
it("returns null when contentRef is null", () => {
|
|
249
|
+
editor = createTestEditor(createTableHtml(2, 2));
|
|
250
|
+
const { getCurrentCell } = useTables({
|
|
251
|
+
contentRef: { value: null },
|
|
252
|
+
onContentChange
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
expect(getCurrentCell()).toBeNull();
|
|
256
|
+
});
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
describe("createTable", () => {
|
|
260
|
+
it("creates table with correct number of rows and columns", () => {
|
|
261
|
+
editor = createTestEditor("<p>Insert here</p>");
|
|
262
|
+
const tables = createTables();
|
|
263
|
+
editor.setCursorInBlock(0, 5);
|
|
264
|
+
|
|
265
|
+
tables.createTable(3, 4);
|
|
266
|
+
|
|
267
|
+
const table = editor.container.querySelector("table");
|
|
268
|
+
expect(table).not.toBeNull();
|
|
269
|
+
|
|
270
|
+
// Check header row (1 row in thead)
|
|
271
|
+
const thead = table?.querySelector("thead");
|
|
272
|
+
expect(thead?.querySelectorAll("th").length).toBe(4);
|
|
273
|
+
|
|
274
|
+
// Check body rows (2 rows in tbody)
|
|
275
|
+
const tbody = table?.querySelector("tbody");
|
|
276
|
+
expect(tbody?.querySelectorAll("tr").length).toBe(2);
|
|
277
|
+
expect(tbody?.querySelectorAll("td").length).toBe(8);
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
it("creates table with thead and th cells for header row", () => {
|
|
281
|
+
editor = createTestEditor("<p>Insert here</p>");
|
|
282
|
+
const tables = createTables();
|
|
283
|
+
editor.setCursorInBlock(0, 0);
|
|
284
|
+
|
|
285
|
+
tables.createTable(2, 2);
|
|
286
|
+
|
|
287
|
+
const table = editor.container.querySelector("table");
|
|
288
|
+
expect(table?.querySelector("thead")).not.toBeNull();
|
|
289
|
+
expect(table?.querySelectorAll("th").length).toBe(2);
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
it("creates table with tbody and td cells for body rows", () => {
|
|
293
|
+
editor = createTestEditor("<p>Insert here</p>");
|
|
294
|
+
const tables = createTables();
|
|
295
|
+
editor.setCursorInBlock(0, 0);
|
|
296
|
+
|
|
297
|
+
tables.createTable(3, 2);
|
|
298
|
+
|
|
299
|
+
const table = editor.container.querySelector("table");
|
|
300
|
+
expect(table?.querySelector("tbody")).not.toBeNull();
|
|
301
|
+
expect(table?.querySelectorAll("td").length).toBe(4);
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
it("creates cells with BR placeholder for focusability", () => {
|
|
305
|
+
editor = createTestEditor("<p>Insert here</p>");
|
|
306
|
+
const tables = createTables();
|
|
307
|
+
editor.setCursorInBlock(0, 0);
|
|
308
|
+
|
|
309
|
+
tables.createTable(2, 2);
|
|
310
|
+
|
|
311
|
+
const cells = editor.container.querySelectorAll("th, td");
|
|
312
|
+
cells.forEach(cell => {
|
|
313
|
+
expect(cell.querySelector("br")).not.toBeNull();
|
|
314
|
+
});
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
it("calls onContentChange after creation", () => {
|
|
318
|
+
editor = createTestEditor("<p>Insert here</p>");
|
|
319
|
+
const tables = createTables();
|
|
320
|
+
editor.setCursorInBlock(0, 0);
|
|
321
|
+
|
|
322
|
+
tables.createTable(2, 2);
|
|
323
|
+
|
|
324
|
+
expect(onContentChange).toHaveBeenCalled();
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
it("does nothing with invalid dimensions", () => {
|
|
328
|
+
editor = createTestEditor("<p>Insert here</p>");
|
|
329
|
+
const tables = createTables();
|
|
330
|
+
editor.setCursorInBlock(0, 0);
|
|
331
|
+
|
|
332
|
+
tables.createTable(0, 2);
|
|
333
|
+
expect(editor.container.querySelector("table")).toBeNull();
|
|
334
|
+
|
|
335
|
+
tables.createTable(2, 0);
|
|
336
|
+
expect(editor.container.querySelector("table")).toBeNull();
|
|
337
|
+
|
|
338
|
+
tables.createTable(-1, 2);
|
|
339
|
+
expect(editor.container.querySelector("table")).toBeNull();
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
it("creates table with only header row when rows=1", () => {
|
|
343
|
+
editor = createTestEditor("<p>Insert here</p>");
|
|
344
|
+
const tables = createTables();
|
|
345
|
+
editor.setCursorInBlock(0, 0);
|
|
346
|
+
|
|
347
|
+
tables.createTable(1, 3);
|
|
348
|
+
|
|
349
|
+
const table = editor.container.querySelector("table");
|
|
350
|
+
expect(table?.querySelector("thead")).not.toBeNull();
|
|
351
|
+
expect(table?.querySelector("tbody")).toBeNull();
|
|
352
|
+
expect(table?.querySelectorAll("th").length).toBe(3);
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
it("does nothing when contentRef is null", () => {
|
|
356
|
+
editor = createTestEditor("<p>Test</p>");
|
|
357
|
+
const { createTable } = useTables({
|
|
358
|
+
contentRef: { value: null },
|
|
359
|
+
onContentChange
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
createTable(2, 2);
|
|
363
|
+
|
|
364
|
+
expect(editor.container.querySelector("table")).toBeNull();
|
|
365
|
+
expect(onContentChange).not.toHaveBeenCalled();
|
|
366
|
+
});
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
describe("navigateToNextCell", () => {
|
|
370
|
+
it("moves to next cell in same row", () => {
|
|
371
|
+
editor = createTestEditor(createTableHtml(2, 3));
|
|
372
|
+
const tables = createTables();
|
|
373
|
+
const table = editor.container.querySelector("table") as HTMLTableElement;
|
|
374
|
+
const firstCell = getCell(table, 0, 0)!;
|
|
375
|
+
setCursorInCell(firstCell);
|
|
376
|
+
|
|
377
|
+
const result = tables.navigateToNextCell();
|
|
378
|
+
|
|
379
|
+
expect(result).toBe(true);
|
|
380
|
+
const currentCell = tables.getCurrentCell();
|
|
381
|
+
expect(currentCell).toBe(getCell(table, 0, 1));
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
it("moves to first cell of next row at end of row", () => {
|
|
385
|
+
editor = createTestEditor(createTableHtml(2, 3));
|
|
386
|
+
const tables = createTables();
|
|
387
|
+
const table = editor.container.querySelector("table") as HTMLTableElement;
|
|
388
|
+
const lastCellInFirstRow = getCell(table, 0, 2)!;
|
|
389
|
+
setCursorInCell(lastCellInFirstRow);
|
|
390
|
+
|
|
391
|
+
const result = tables.navigateToNextCell();
|
|
392
|
+
|
|
393
|
+
expect(result).toBe(true);
|
|
394
|
+
const currentCell = tables.getCurrentCell();
|
|
395
|
+
expect(currentCell).toBe(getCell(table, 1, 0));
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
it("returns false at end of table", () => {
|
|
399
|
+
editor = createTestEditor(createTableHtml(2, 2));
|
|
400
|
+
const tables = createTables();
|
|
401
|
+
const table = editor.container.querySelector("table") as HTMLTableElement;
|
|
402
|
+
const lastCell = getCell(table, 1, 1)!;
|
|
403
|
+
setCursorInCell(lastCell);
|
|
404
|
+
|
|
405
|
+
const result = tables.navigateToNextCell();
|
|
406
|
+
|
|
407
|
+
expect(result).toBe(false);
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
it("returns false when not in table", () => {
|
|
411
|
+
editor = createTestEditor("<p>Not in table</p>");
|
|
412
|
+
const tables = createTables();
|
|
413
|
+
editor.setCursorInBlock(0, 0);
|
|
414
|
+
|
|
415
|
+
expect(tables.navigateToNextCell()).toBe(false);
|
|
416
|
+
});
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
describe("navigateToPreviousCell", () => {
|
|
420
|
+
it("moves to previous cell in same row", () => {
|
|
421
|
+
editor = createTestEditor(createTableHtml(2, 3));
|
|
422
|
+
const tables = createTables();
|
|
423
|
+
const table = editor.container.querySelector("table") as HTMLTableElement;
|
|
424
|
+
const secondCell = getCell(table, 0, 1)!;
|
|
425
|
+
setCursorInCell(secondCell);
|
|
426
|
+
|
|
427
|
+
const result = tables.navigateToPreviousCell();
|
|
428
|
+
|
|
429
|
+
expect(result).toBe(true);
|
|
430
|
+
const currentCell = tables.getCurrentCell();
|
|
431
|
+
expect(currentCell).toBe(getCell(table, 0, 0));
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
it("moves to last cell of previous row at start of row", () => {
|
|
435
|
+
editor = createTestEditor(createTableHtml(2, 3));
|
|
436
|
+
const tables = createTables();
|
|
437
|
+
const table = editor.container.querySelector("table") as HTMLTableElement;
|
|
438
|
+
const firstCellInSecondRow = getCell(table, 1, 0)!;
|
|
439
|
+
setCursorInCell(firstCellInSecondRow);
|
|
440
|
+
|
|
441
|
+
const result = tables.navigateToPreviousCell();
|
|
442
|
+
|
|
443
|
+
expect(result).toBe(true);
|
|
444
|
+
const currentCell = tables.getCurrentCell();
|
|
445
|
+
expect(currentCell).toBe(getCell(table, 0, 2));
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
it("returns false at start of table", () => {
|
|
449
|
+
editor = createTestEditor(createTableHtml(2, 2));
|
|
450
|
+
const tables = createTables();
|
|
451
|
+
const table = editor.container.querySelector("table") as HTMLTableElement;
|
|
452
|
+
const firstCell = getCell(table, 0, 0)!;
|
|
453
|
+
setCursorInCell(firstCell);
|
|
454
|
+
|
|
455
|
+
const result = tables.navigateToPreviousCell();
|
|
456
|
+
|
|
457
|
+
expect(result).toBe(false);
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
it("returns false when not in table", () => {
|
|
461
|
+
editor = createTestEditor("<p>Not in table</p>");
|
|
462
|
+
const tables = createTables();
|
|
463
|
+
editor.setCursorInBlock(0, 0);
|
|
464
|
+
|
|
465
|
+
expect(tables.navigateToPreviousCell()).toBe(false);
|
|
466
|
+
});
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
describe("navigateToCellBelow", () => {
|
|
470
|
+
it("moves to cell directly below", () => {
|
|
471
|
+
editor = createTestEditor(createTableHtml(3, 2));
|
|
472
|
+
const tables = createTables();
|
|
473
|
+
const table = editor.container.querySelector("table") as HTMLTableElement;
|
|
474
|
+
const headerCell = getCell(table, 0, 0)!;
|
|
475
|
+
setCursorInCell(headerCell);
|
|
476
|
+
|
|
477
|
+
const result = tables.navigateToCellBelow();
|
|
478
|
+
|
|
479
|
+
expect(result).toBe(true);
|
|
480
|
+
const currentCell = tables.getCurrentCell();
|
|
481
|
+
expect(currentCell).toBe(getCell(table, 1, 0));
|
|
482
|
+
});
|
|
483
|
+
|
|
484
|
+
it("maintains column position when moving down", () => {
|
|
485
|
+
editor = createTestEditor(createTableHtml(3, 3));
|
|
486
|
+
const tables = createTables();
|
|
487
|
+
const table = editor.container.querySelector("table") as HTMLTableElement;
|
|
488
|
+
const cell = getCell(table, 0, 1)!;
|
|
489
|
+
setCursorInCell(cell);
|
|
490
|
+
|
|
491
|
+
tables.navigateToCellBelow();
|
|
492
|
+
|
|
493
|
+
const currentCell = tables.getCurrentCell();
|
|
494
|
+
expect(currentCell).toBe(getCell(table, 1, 1));
|
|
495
|
+
});
|
|
496
|
+
|
|
497
|
+
it("returns false at bottom of table", () => {
|
|
498
|
+
editor = createTestEditor(createTableHtml(2, 2));
|
|
499
|
+
const tables = createTables();
|
|
500
|
+
const table = editor.container.querySelector("table") as HTMLTableElement;
|
|
501
|
+
const bottomCell = getCell(table, 1, 0)!;
|
|
502
|
+
setCursorInCell(bottomCell);
|
|
503
|
+
|
|
504
|
+
const result = tables.navigateToCellBelow();
|
|
505
|
+
|
|
506
|
+
expect(result).toBe(false);
|
|
507
|
+
});
|
|
508
|
+
|
|
509
|
+
it("returns false when not in table", () => {
|
|
510
|
+
editor = createTestEditor("<p>Not in table</p>");
|
|
511
|
+
const tables = createTables();
|
|
512
|
+
editor.setCursorInBlock(0, 0);
|
|
513
|
+
|
|
514
|
+
expect(tables.navigateToCellBelow()).toBe(false);
|
|
515
|
+
});
|
|
516
|
+
});
|
|
517
|
+
|
|
518
|
+
describe("navigateToCellAbove", () => {
|
|
519
|
+
it("moves to cell directly above", () => {
|
|
520
|
+
editor = createTestEditor(createTableHtml(3, 2));
|
|
521
|
+
const tables = createTables();
|
|
522
|
+
const table = editor.container.querySelector("table") as HTMLTableElement;
|
|
523
|
+
const bodyCell = getCell(table, 1, 0)!;
|
|
524
|
+
setCursorInCell(bodyCell);
|
|
525
|
+
|
|
526
|
+
const result = tables.navigateToCellAbove();
|
|
527
|
+
|
|
528
|
+
expect(result).toBe(true);
|
|
529
|
+
const currentCell = tables.getCurrentCell();
|
|
530
|
+
expect(currentCell).toBe(getCell(table, 0, 0));
|
|
531
|
+
});
|
|
532
|
+
|
|
533
|
+
it("maintains column position when moving up", () => {
|
|
534
|
+
editor = createTestEditor(createTableHtml(3, 3));
|
|
535
|
+
const tables = createTables();
|
|
536
|
+
const table = editor.container.querySelector("table") as HTMLTableElement;
|
|
537
|
+
const cell = getCell(table, 2, 1)!;
|
|
538
|
+
setCursorInCell(cell);
|
|
539
|
+
|
|
540
|
+
tables.navigateToCellAbove();
|
|
541
|
+
|
|
542
|
+
const currentCell = tables.getCurrentCell();
|
|
543
|
+
expect(currentCell).toBe(getCell(table, 1, 1));
|
|
544
|
+
});
|
|
545
|
+
|
|
546
|
+
it("returns false at top of table", () => {
|
|
547
|
+
editor = createTestEditor(createTableHtml(2, 2));
|
|
548
|
+
const tables = createTables();
|
|
549
|
+
const table = editor.container.querySelector("table") as HTMLTableElement;
|
|
550
|
+
const topCell = getCell(table, 0, 0)!;
|
|
551
|
+
setCursorInCell(topCell);
|
|
552
|
+
|
|
553
|
+
const result = tables.navigateToCellAbove();
|
|
554
|
+
|
|
555
|
+
expect(result).toBe(false);
|
|
556
|
+
});
|
|
557
|
+
|
|
558
|
+
it("returns false when not in table", () => {
|
|
559
|
+
editor = createTestEditor("<p>Not in table</p>");
|
|
560
|
+
const tables = createTables();
|
|
561
|
+
editor.setCursorInBlock(0, 0);
|
|
562
|
+
|
|
563
|
+
expect(tables.navigateToCellAbove()).toBe(false);
|
|
564
|
+
});
|
|
565
|
+
});
|
|
566
|
+
|
|
567
|
+
describe("cursor position preservation", () => {
|
|
568
|
+
/**
|
|
569
|
+
* Helper to get cursor offset within a cell
|
|
570
|
+
*/
|
|
571
|
+
function getCursorOffsetInCell(cell: HTMLTableCellElement): number {
|
|
572
|
+
const selection = window.getSelection();
|
|
573
|
+
if (!selection || !selection.rangeCount) return -1;
|
|
574
|
+
|
|
575
|
+
const range = selection.getRangeAt(0);
|
|
576
|
+
if (!cell.contains(range.startContainer)) return -1;
|
|
577
|
+
|
|
578
|
+
const preCaretRange = document.createRange();
|
|
579
|
+
preCaretRange.selectNodeContents(cell);
|
|
580
|
+
preCaretRange.setEnd(range.startContainer, range.startOffset);
|
|
581
|
+
return preCaretRange.toString().length;
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
describe("navigateToCellBelow with cursor offset", () => {
|
|
585
|
+
it("preserves cursor position when moving down", () => {
|
|
586
|
+
// Create table with specific content
|
|
587
|
+
editor = createTestEditor(
|
|
588
|
+
"<table><thead><tr><th>Hello</th></tr></thead><tbody><tr><td>World</td></tr></tbody></table>"
|
|
589
|
+
);
|
|
590
|
+
const tables = createTables();
|
|
591
|
+
const table = editor.container.querySelector("table") as HTMLTableElement;
|
|
592
|
+
const headerCell = getCell(table, 0, 0)!;
|
|
593
|
+
|
|
594
|
+
// Place cursor at position 3 in "Hello" (after "Hel")
|
|
595
|
+
setCursorInCell(headerCell, 3);
|
|
596
|
+
|
|
597
|
+
// Verify cursor is at position 3
|
|
598
|
+
expect(getCursorOffsetInCell(headerCell)).toBe(3);
|
|
599
|
+
|
|
600
|
+
// Navigate down
|
|
601
|
+
tables.navigateToCellBelow();
|
|
602
|
+
|
|
603
|
+
// Verify cursor is at position 3 in second row ("Wor|ld")
|
|
604
|
+
const targetCell = getCell(table, 1, 0)!;
|
|
605
|
+
expect(tables.getCurrentCell()).toBe(targetCell);
|
|
606
|
+
expect(getCursorOffsetInCell(targetCell)).toBe(3);
|
|
607
|
+
});
|
|
608
|
+
|
|
609
|
+
it("clamps cursor position when target cell is shorter", () => {
|
|
610
|
+
// Cell 1 has "Hello" (5 chars), Cell 2 has "Hi" (2 chars)
|
|
611
|
+
editor = createTestEditor(
|
|
612
|
+
"<table><thead><tr><th>Hello</th></tr></thead><tbody><tr><td>Hi</td></tr></tbody></table>"
|
|
613
|
+
);
|
|
614
|
+
const tables = createTables();
|
|
615
|
+
const table = editor.container.querySelector("table") as HTMLTableElement;
|
|
616
|
+
const headerCell = getCell(table, 0, 0)!;
|
|
617
|
+
|
|
618
|
+
// Place cursor at position 4 in "Hello"
|
|
619
|
+
setCursorInCell(headerCell, 4);
|
|
620
|
+
expect(getCursorOffsetInCell(headerCell)).toBe(4);
|
|
621
|
+
|
|
622
|
+
// Navigate down
|
|
623
|
+
tables.navigateToCellBelow();
|
|
624
|
+
|
|
625
|
+
// Cursor should be clamped to position 2 (end of "Hi")
|
|
626
|
+
const targetCell = getCell(table, 1, 0)!;
|
|
627
|
+
expect(tables.getCurrentCell()).toBe(targetCell);
|
|
628
|
+
expect(getCursorOffsetInCell(targetCell)).toBe(2);
|
|
629
|
+
});
|
|
630
|
+
|
|
631
|
+
it("places cursor at start for empty target cell", () => {
|
|
632
|
+
// Cell 1 has "Hello", Cell 2 is empty (just BR placeholder)
|
|
633
|
+
editor = createTestEditor(
|
|
634
|
+
"<table><thead><tr><th>Hello</th></tr></thead><tbody><tr><td><br></td></tr></tbody></table>"
|
|
635
|
+
);
|
|
636
|
+
const tables = createTables();
|
|
637
|
+
const table = editor.container.querySelector("table") as HTMLTableElement;
|
|
638
|
+
const headerCell = getCell(table, 0, 0)!;
|
|
639
|
+
|
|
640
|
+
// Place cursor at position 3 in "Hello"
|
|
641
|
+
setCursorInCell(headerCell, 3);
|
|
642
|
+
|
|
643
|
+
// Navigate down
|
|
644
|
+
tables.navigateToCellBelow();
|
|
645
|
+
|
|
646
|
+
// Cursor should be at position 0 in empty cell
|
|
647
|
+
const targetCell = getCell(table, 1, 0)!;
|
|
648
|
+
expect(tables.getCurrentCell()).toBe(targetCell);
|
|
649
|
+
// Empty cell has no text, cursor should be at 0
|
|
650
|
+
expect(getCursorOffsetInCell(targetCell)).toBe(0);
|
|
651
|
+
});
|
|
652
|
+
|
|
653
|
+
it("handles cursor at end of cell when moving down", () => {
|
|
654
|
+
editor = createTestEditor(
|
|
655
|
+
"<table><thead><tr><th>ABC</th></tr></thead><tbody><tr><td>DEFGH</td></tr></tbody></table>"
|
|
656
|
+
);
|
|
657
|
+
const tables = createTables();
|
|
658
|
+
const table = editor.container.querySelector("table") as HTMLTableElement;
|
|
659
|
+
const headerCell = getCell(table, 0, 0)!;
|
|
660
|
+
|
|
661
|
+
// Place cursor at end of "ABC" (position 3)
|
|
662
|
+
setCursorInCell(headerCell, 3);
|
|
663
|
+
|
|
664
|
+
// Navigate down
|
|
665
|
+
tables.navigateToCellBelow();
|
|
666
|
+
|
|
667
|
+
// Cursor should be at position 3 in "DEFGH"
|
|
668
|
+
const targetCell = getCell(table, 1, 0)!;
|
|
669
|
+
expect(getCursorOffsetInCell(targetCell)).toBe(3);
|
|
670
|
+
});
|
|
671
|
+
});
|
|
672
|
+
|
|
673
|
+
describe("navigateToCellAbove with cursor offset", () => {
|
|
674
|
+
it("preserves cursor position when moving up", () => {
|
|
675
|
+
editor = createTestEditor(
|
|
676
|
+
"<table><thead><tr><th>Hello</th></tr></thead><tbody><tr><td>World</td></tr></tbody></table>"
|
|
677
|
+
);
|
|
678
|
+
const tables = createTables();
|
|
679
|
+
const table = editor.container.querySelector("table") as HTMLTableElement;
|
|
680
|
+
const bodyCell = getCell(table, 1, 0)!;
|
|
681
|
+
|
|
682
|
+
// Place cursor at position 2 in "World" (after "Wo")
|
|
683
|
+
setCursorInCell(bodyCell, 2);
|
|
684
|
+
expect(getCursorOffsetInCell(bodyCell)).toBe(2);
|
|
685
|
+
|
|
686
|
+
// Navigate up
|
|
687
|
+
tables.navigateToCellAbove();
|
|
688
|
+
|
|
689
|
+
// Verify cursor is at position 2 in header row ("He|llo")
|
|
690
|
+
const targetCell = getCell(table, 0, 0)!;
|
|
691
|
+
expect(tables.getCurrentCell()).toBe(targetCell);
|
|
692
|
+
expect(getCursorOffsetInCell(targetCell)).toBe(2);
|
|
693
|
+
});
|
|
694
|
+
|
|
695
|
+
it("clamps cursor position when target cell is shorter", () => {
|
|
696
|
+
// Cell 1 (header) has "Hi" (2 chars), Cell 2 has "Hello" (5 chars)
|
|
697
|
+
editor = createTestEditor(
|
|
698
|
+
"<table><thead><tr><th>Hi</th></tr></thead><tbody><tr><td>Hello</td></tr></tbody></table>"
|
|
699
|
+
);
|
|
700
|
+
const tables = createTables();
|
|
701
|
+
const table = editor.container.querySelector("table") as HTMLTableElement;
|
|
702
|
+
const bodyCell = getCell(table, 1, 0)!;
|
|
703
|
+
|
|
704
|
+
// Place cursor at position 4 in "Hello"
|
|
705
|
+
setCursorInCell(bodyCell, 4);
|
|
706
|
+
expect(getCursorOffsetInCell(bodyCell)).toBe(4);
|
|
707
|
+
|
|
708
|
+
// Navigate up
|
|
709
|
+
tables.navigateToCellAbove();
|
|
710
|
+
|
|
711
|
+
// Cursor should be clamped to position 2 (end of "Hi")
|
|
712
|
+
const targetCell = getCell(table, 0, 0)!;
|
|
713
|
+
expect(tables.getCurrentCell()).toBe(targetCell);
|
|
714
|
+
expect(getCursorOffsetInCell(targetCell)).toBe(2);
|
|
715
|
+
});
|
|
716
|
+
|
|
717
|
+
it("places cursor at start for empty target cell", () => {
|
|
718
|
+
// Header cell is empty, body cell has content
|
|
719
|
+
editor = createTestEditor(
|
|
720
|
+
"<table><thead><tr><th><br></th></tr></thead><tbody><tr><td>Hello</td></tr></tbody></table>"
|
|
721
|
+
);
|
|
722
|
+
const tables = createTables();
|
|
723
|
+
const table = editor.container.querySelector("table") as HTMLTableElement;
|
|
724
|
+
const bodyCell = getCell(table, 1, 0)!;
|
|
725
|
+
|
|
726
|
+
// Place cursor at position 3 in "Hello"
|
|
727
|
+
setCursorInCell(bodyCell, 3);
|
|
728
|
+
|
|
729
|
+
// Navigate up
|
|
730
|
+
tables.navigateToCellAbove();
|
|
731
|
+
|
|
732
|
+
// Cursor should be at position 0 in empty header cell
|
|
733
|
+
const targetCell = getCell(table, 0, 0)!;
|
|
734
|
+
expect(tables.getCurrentCell()).toBe(targetCell);
|
|
735
|
+
expect(getCursorOffsetInCell(targetCell)).toBe(0);
|
|
736
|
+
});
|
|
737
|
+
|
|
738
|
+
it("handles multi-row navigation preserving offset", () => {
|
|
739
|
+
// 3-row table to test navigating through multiple rows
|
|
740
|
+
editor = createTestEditor(
|
|
741
|
+
"<table><thead><tr><th>Row1</th></tr></thead><tbody><tr><td>Row2</td></tr><tr><td>Row3</td></tr></tbody></table>"
|
|
742
|
+
);
|
|
743
|
+
const tables = createTables();
|
|
744
|
+
const table = editor.container.querySelector("table") as HTMLTableElement;
|
|
745
|
+
const lastRowCell = getCell(table, 2, 0)!;
|
|
746
|
+
|
|
747
|
+
// Place cursor at position 2 in "Row3"
|
|
748
|
+
setCursorInCell(lastRowCell, 2);
|
|
749
|
+
|
|
750
|
+
// Navigate up twice
|
|
751
|
+
tables.navigateToCellAbove();
|
|
752
|
+
let currentCell = tables.getCurrentCell();
|
|
753
|
+
expect(currentCell).toBe(getCell(table, 1, 0));
|
|
754
|
+
expect(getCursorOffsetInCell(currentCell!)).toBe(2);
|
|
755
|
+
|
|
756
|
+
tables.navigateToCellAbove();
|
|
757
|
+
currentCell = tables.getCurrentCell();
|
|
758
|
+
expect(currentCell).toBe(getCell(table, 0, 0));
|
|
759
|
+
expect(getCursorOffsetInCell(currentCell!)).toBe(2);
|
|
760
|
+
});
|
|
761
|
+
});
|
|
762
|
+
|
|
763
|
+
describe("cursor offset edge cases", () => {
|
|
764
|
+
it("handles cursor at position 0", () => {
|
|
765
|
+
editor = createTestEditor(
|
|
766
|
+
"<table><thead><tr><th>Hello</th></tr></thead><tbody><tr><td>World</td></tr></tbody></table>"
|
|
767
|
+
);
|
|
768
|
+
const tables = createTables();
|
|
769
|
+
const table = editor.container.querySelector("table") as HTMLTableElement;
|
|
770
|
+
const headerCell = getCell(table, 0, 0)!;
|
|
771
|
+
|
|
772
|
+
// Place cursor at position 0
|
|
773
|
+
setCursorInCell(headerCell, 0);
|
|
774
|
+
expect(getCursorOffsetInCell(headerCell)).toBe(0);
|
|
775
|
+
|
|
776
|
+
// Navigate down
|
|
777
|
+
tables.navigateToCellBelow();
|
|
778
|
+
|
|
779
|
+
// Cursor should be at position 0
|
|
780
|
+
const targetCell = getCell(table, 1, 0)!;
|
|
781
|
+
expect(getCursorOffsetInCell(targetCell)).toBe(0);
|
|
782
|
+
});
|
|
783
|
+
|
|
784
|
+
it("handles cells with inline formatting", () => {
|
|
785
|
+
// Cell with bold text
|
|
786
|
+
editor = createTestEditor(
|
|
787
|
+
"<table><thead><tr><th><strong>Bold</strong></th></tr></thead><tbody><tr><td>Plain</td></tr></tbody></table>"
|
|
788
|
+
);
|
|
789
|
+
const tables = createTables();
|
|
790
|
+
const table = editor.container.querySelector("table") as HTMLTableElement;
|
|
791
|
+
const headerCell = getCell(table, 0, 0)!;
|
|
792
|
+
|
|
793
|
+
// Place cursor at position 2 in "Bold" (inside strong tag)
|
|
794
|
+
const strongText = headerCell.querySelector("strong")!.firstChild!;
|
|
795
|
+
editor.setCursor(strongText, 2);
|
|
796
|
+
expect(getCursorOffsetInCell(headerCell)).toBe(2);
|
|
797
|
+
|
|
798
|
+
// Navigate down
|
|
799
|
+
tables.navigateToCellBelow();
|
|
800
|
+
|
|
801
|
+
// Cursor should be at position 2 in "Plain"
|
|
802
|
+
const targetCell = getCell(table, 1, 0)!;
|
|
803
|
+
expect(getCursorOffsetInCell(targetCell)).toBe(2);
|
|
804
|
+
});
|
|
805
|
+
|
|
806
|
+
it("handles mixed content cells", () => {
|
|
807
|
+
// Cell with text + bold + text
|
|
808
|
+
editor = createTestEditor(
|
|
809
|
+
"<table><thead><tr><th>A<strong>B</strong>C</th></tr></thead><tbody><tr><td>DEFGH</td></tr></tbody></table>"
|
|
810
|
+
);
|
|
811
|
+
const tables = createTables();
|
|
812
|
+
const table = editor.container.querySelector("table") as HTMLTableElement;
|
|
813
|
+
const headerCell = getCell(table, 0, 0)!;
|
|
814
|
+
|
|
815
|
+
// Total text is "ABC", place cursor at position 2 (after "AB")
|
|
816
|
+
// Need to find the right text node - the one after strong
|
|
817
|
+
const walker = document.createTreeWalker(headerCell, NodeFilter.SHOW_TEXT);
|
|
818
|
+
walker.nextNode(); // "A"
|
|
819
|
+
walker.nextNode(); // "B" inside strong
|
|
820
|
+
const lastTextNode = walker.nextNode() as Text; // "C"
|
|
821
|
+
|
|
822
|
+
// Set cursor at start of "C" which is offset 2 in total text
|
|
823
|
+
editor.setCursor(lastTextNode, 0);
|
|
824
|
+
expect(getCursorOffsetInCell(headerCell)).toBe(2);
|
|
825
|
+
|
|
826
|
+
// Navigate down
|
|
827
|
+
tables.navigateToCellBelow();
|
|
828
|
+
|
|
829
|
+
// Cursor should be at position 2 in "DEFGH"
|
|
830
|
+
const targetCell = getCell(table, 1, 0)!;
|
|
831
|
+
expect(getCursorOffsetInCell(targetCell)).toBe(2);
|
|
832
|
+
});
|
|
833
|
+
});
|
|
834
|
+
});
|
|
835
|
+
|
|
836
|
+
describe("insertRowAbove", () => {
|
|
837
|
+
it("inserts a new row above the current row", () => {
|
|
838
|
+
editor = createTestEditor(createTableHtml(2, 2, ["H1", "H2"], [["C1", "C2"]]));
|
|
839
|
+
const tables = createTables();
|
|
840
|
+
const table = editor.container.querySelector("table") as HTMLTableElement;
|
|
841
|
+
const bodyCell = getCell(table, 1, 0)!;
|
|
842
|
+
setCursorInCell(bodyCell);
|
|
843
|
+
|
|
844
|
+
tables.insertRowAbove();
|
|
845
|
+
|
|
846
|
+
// Should now have 3 rows total
|
|
847
|
+
const allRows = table.querySelectorAll("tr");
|
|
848
|
+
expect(allRows.length).toBe(3);
|
|
849
|
+
expect(onContentChange).toHaveBeenCalled();
|
|
850
|
+
});
|
|
851
|
+
|
|
852
|
+
it("creates row with correct number of cells", () => {
|
|
853
|
+
editor = createTestEditor(createTableHtml(2, 3));
|
|
854
|
+
const tables = createTables();
|
|
855
|
+
const table = editor.container.querySelector("table") as HTMLTableElement;
|
|
856
|
+
const bodyCell = getCell(table, 1, 0)!;
|
|
857
|
+
setCursorInCell(bodyCell);
|
|
858
|
+
|
|
859
|
+
tables.insertRowAbove();
|
|
860
|
+
|
|
861
|
+
// New row should have 3 cells
|
|
862
|
+
const tbody = table.querySelector("tbody");
|
|
863
|
+
const newRow = tbody?.querySelectorAll("tr")[0];
|
|
864
|
+
expect(newRow?.cells.length).toBe(3);
|
|
865
|
+
});
|
|
866
|
+
|
|
867
|
+
it("does nothing when not in table", () => {
|
|
868
|
+
editor = createTestEditor("<p>Not in table</p>");
|
|
869
|
+
const tables = createTables();
|
|
870
|
+
editor.setCursorInBlock(0, 0);
|
|
871
|
+
|
|
872
|
+
tables.insertRowAbove();
|
|
873
|
+
|
|
874
|
+
expect(onContentChange).not.toHaveBeenCalled();
|
|
875
|
+
});
|
|
876
|
+
|
|
877
|
+
it("does nothing when contentRef is null", () => {
|
|
878
|
+
editor = createTestEditor(createTableHtml(2, 2));
|
|
879
|
+
const { insertRowAbove } = useTables({
|
|
880
|
+
contentRef: { value: null },
|
|
881
|
+
onContentChange
|
|
882
|
+
});
|
|
883
|
+
|
|
884
|
+
insertRowAbove();
|
|
885
|
+
|
|
886
|
+
expect(onContentChange).not.toHaveBeenCalled();
|
|
887
|
+
});
|
|
888
|
+
});
|
|
889
|
+
|
|
890
|
+
describe("insertRowBelow", () => {
|
|
891
|
+
it("inserts a new row below the current row", () => {
|
|
892
|
+
editor = createTestEditor(createTableHtml(2, 2));
|
|
893
|
+
const tables = createTables();
|
|
894
|
+
const table = editor.container.querySelector("table") as HTMLTableElement;
|
|
895
|
+
const bodyCell = getCell(table, 1, 0)!;
|
|
896
|
+
setCursorInCell(bodyCell);
|
|
897
|
+
|
|
898
|
+
tables.insertRowBelow();
|
|
899
|
+
|
|
900
|
+
// Should now have 3 rows total
|
|
901
|
+
const allRows = table.querySelectorAll("tr");
|
|
902
|
+
expect(allRows.length).toBe(3);
|
|
903
|
+
expect(onContentChange).toHaveBeenCalled();
|
|
904
|
+
});
|
|
905
|
+
|
|
906
|
+
it("inserts row into tbody when cursor is in header", () => {
|
|
907
|
+
editor = createTestEditor(createTableHtml(2, 2));
|
|
908
|
+
const tables = createTables();
|
|
909
|
+
const table = editor.container.querySelector("table") as HTMLTableElement;
|
|
910
|
+
const headerCell = getCell(table, 0, 0)!;
|
|
911
|
+
setCursorInCell(headerCell);
|
|
912
|
+
|
|
913
|
+
tables.insertRowBelow();
|
|
914
|
+
|
|
915
|
+
// New row should be in tbody
|
|
916
|
+
const tbody = table.querySelector("tbody");
|
|
917
|
+
expect(tbody?.querySelectorAll("tr").length).toBe(2);
|
|
918
|
+
});
|
|
919
|
+
|
|
920
|
+
it("creates row with TD cells, not TH", () => {
|
|
921
|
+
editor = createTestEditor(createTableHtml(2, 2));
|
|
922
|
+
const tables = createTables();
|
|
923
|
+
const table = editor.container.querySelector("table") as HTMLTableElement;
|
|
924
|
+
const headerCell = getCell(table, 0, 0)!;
|
|
925
|
+
setCursorInCell(headerCell);
|
|
926
|
+
|
|
927
|
+
tables.insertRowBelow();
|
|
928
|
+
|
|
929
|
+
const tbody = table.querySelector("tbody");
|
|
930
|
+
const firstBodyRow = tbody?.querySelector("tr");
|
|
931
|
+
expect(firstBodyRow?.querySelectorAll("td").length).toBe(2);
|
|
932
|
+
expect(firstBodyRow?.querySelectorAll("th").length).toBe(0);
|
|
933
|
+
});
|
|
934
|
+
|
|
935
|
+
it("does nothing when not in table", () => {
|
|
936
|
+
editor = createTestEditor("<p>Not in table</p>");
|
|
937
|
+
const tables = createTables();
|
|
938
|
+
editor.setCursorInBlock(0, 0);
|
|
939
|
+
|
|
940
|
+
tables.insertRowBelow();
|
|
941
|
+
|
|
942
|
+
expect(onContentChange).not.toHaveBeenCalled();
|
|
943
|
+
});
|
|
944
|
+
});
|
|
945
|
+
|
|
946
|
+
describe("deleteCurrentRow", () => {
|
|
947
|
+
it("removes the current row", () => {
|
|
948
|
+
editor = createTestEditor(createTableHtml(3, 2));
|
|
949
|
+
const tables = createTables();
|
|
950
|
+
const table = editor.container.querySelector("table") as HTMLTableElement;
|
|
951
|
+
const middleRowCell = getCell(table, 1, 0)!;
|
|
952
|
+
setCursorInCell(middleRowCell);
|
|
953
|
+
|
|
954
|
+
tables.deleteCurrentRow();
|
|
955
|
+
|
|
956
|
+
const allRows = table.querySelectorAll("tr");
|
|
957
|
+
expect(allRows.length).toBe(2);
|
|
958
|
+
expect(onContentChange).toHaveBeenCalled();
|
|
959
|
+
});
|
|
960
|
+
|
|
961
|
+
it("deletes entire table when last row is deleted", () => {
|
|
962
|
+
editor = createTestEditor(createTableHtml(1, 2));
|
|
963
|
+
const tables = createTables();
|
|
964
|
+
const table = editor.container.querySelector("table") as HTMLTableElement;
|
|
965
|
+
const cell = getCell(table, 0, 0)!;
|
|
966
|
+
setCursorInCell(cell);
|
|
967
|
+
|
|
968
|
+
tables.deleteCurrentRow();
|
|
969
|
+
|
|
970
|
+
expect(editor.container.querySelector("table")).toBeNull();
|
|
971
|
+
});
|
|
972
|
+
|
|
973
|
+
it("focuses adjacent row after deletion", () => {
|
|
974
|
+
editor = createTestEditor(createTableHtml(3, 2));
|
|
975
|
+
const tables = createTables();
|
|
976
|
+
const table = editor.container.querySelector("table") as HTMLTableElement;
|
|
977
|
+
const middleRowCell = getCell(table, 1, 0)!;
|
|
978
|
+
setCursorInCell(middleRowCell);
|
|
979
|
+
|
|
980
|
+
tables.deleteCurrentRow();
|
|
981
|
+
|
|
982
|
+
// Should still be in a table cell
|
|
983
|
+
expect(tables.isInTableCell()).toBe(true);
|
|
984
|
+
});
|
|
985
|
+
|
|
986
|
+
it("does nothing when not in table", () => {
|
|
987
|
+
editor = createTestEditor("<p>Not in table</p>");
|
|
988
|
+
const tables = createTables();
|
|
989
|
+
editor.setCursorInBlock(0, 0);
|
|
990
|
+
|
|
991
|
+
tables.deleteCurrentRow();
|
|
992
|
+
|
|
993
|
+
expect(onContentChange).not.toHaveBeenCalled();
|
|
994
|
+
});
|
|
995
|
+
});
|
|
996
|
+
|
|
997
|
+
describe("insertColumnLeft", () => {
|
|
998
|
+
it("inserts a column to the left", () => {
|
|
999
|
+
editor = createTestEditor(createTableHtml(2, 2));
|
|
1000
|
+
const tables = createTables();
|
|
1001
|
+
const table = editor.container.querySelector("table") as HTMLTableElement;
|
|
1002
|
+
const cell = getCell(table, 0, 1)!;
|
|
1003
|
+
setCursorInCell(cell);
|
|
1004
|
+
|
|
1005
|
+
tables.insertColumnLeft();
|
|
1006
|
+
|
|
1007
|
+
// Should now have 3 columns
|
|
1008
|
+
expect(table.querySelector("thead tr")?.cells.length).toBe(3);
|
|
1009
|
+
expect(onContentChange).toHaveBeenCalled();
|
|
1010
|
+
});
|
|
1011
|
+
|
|
1012
|
+
it("inserts TH in header row, TD in body rows", () => {
|
|
1013
|
+
editor = createTestEditor(createTableHtml(2, 2));
|
|
1014
|
+
const tables = createTables();
|
|
1015
|
+
const table = editor.container.querySelector("table") as HTMLTableElement;
|
|
1016
|
+
const cell = getCell(table, 0, 0)!;
|
|
1017
|
+
setCursorInCell(cell);
|
|
1018
|
+
|
|
1019
|
+
tables.insertColumnLeft();
|
|
1020
|
+
|
|
1021
|
+
const headerRow = table.querySelector("thead tr");
|
|
1022
|
+
expect(headerRow?.cells[0].tagName).toBe("TH");
|
|
1023
|
+
|
|
1024
|
+
const bodyRow = table.querySelector("tbody tr");
|
|
1025
|
+
expect(bodyRow?.cells[0].tagName).toBe("TD");
|
|
1026
|
+
});
|
|
1027
|
+
|
|
1028
|
+
it("does nothing when not in table", () => {
|
|
1029
|
+
editor = createTestEditor("<p>Not in table</p>");
|
|
1030
|
+
const tables = createTables();
|
|
1031
|
+
editor.setCursorInBlock(0, 0);
|
|
1032
|
+
|
|
1033
|
+
tables.insertColumnLeft();
|
|
1034
|
+
|
|
1035
|
+
expect(onContentChange).not.toHaveBeenCalled();
|
|
1036
|
+
});
|
|
1037
|
+
});
|
|
1038
|
+
|
|
1039
|
+
describe("insertColumnRight", () => {
|
|
1040
|
+
it("inserts a column to the right", () => {
|
|
1041
|
+
editor = createTestEditor(createTableHtml(2, 2));
|
|
1042
|
+
const tables = createTables();
|
|
1043
|
+
const table = editor.container.querySelector("table") as HTMLTableElement;
|
|
1044
|
+
const cell = getCell(table, 0, 0)!;
|
|
1045
|
+
setCursorInCell(cell);
|
|
1046
|
+
|
|
1047
|
+
tables.insertColumnRight();
|
|
1048
|
+
|
|
1049
|
+
// Should now have 3 columns
|
|
1050
|
+
expect(table.querySelector("thead tr")?.cells.length).toBe(3);
|
|
1051
|
+
expect(onContentChange).toHaveBeenCalled();
|
|
1052
|
+
});
|
|
1053
|
+
|
|
1054
|
+
it("inserts column after current position", () => {
|
|
1055
|
+
editor = createTestEditor(createTableHtml(2, 2, ["A", "B"], [["1", "2"]]));
|
|
1056
|
+
const tables = createTables();
|
|
1057
|
+
const table = editor.container.querySelector("table") as HTMLTableElement;
|
|
1058
|
+
const cell = getCell(table, 0, 0)!;
|
|
1059
|
+
setCursorInCell(cell);
|
|
1060
|
+
|
|
1061
|
+
tables.insertColumnRight();
|
|
1062
|
+
|
|
1063
|
+
const headerRow = table.querySelector("thead tr");
|
|
1064
|
+
expect(headerRow?.cells[0].textContent).toBe("A");
|
|
1065
|
+
expect(headerRow?.cells[2].textContent).toBe("B");
|
|
1066
|
+
});
|
|
1067
|
+
|
|
1068
|
+
it("does nothing when not in table", () => {
|
|
1069
|
+
editor = createTestEditor("<p>Not in table</p>");
|
|
1070
|
+
const tables = createTables();
|
|
1071
|
+
editor.setCursorInBlock(0, 0);
|
|
1072
|
+
|
|
1073
|
+
tables.insertColumnRight();
|
|
1074
|
+
|
|
1075
|
+
expect(onContentChange).not.toHaveBeenCalled();
|
|
1076
|
+
});
|
|
1077
|
+
});
|
|
1078
|
+
|
|
1079
|
+
describe("deleteCurrentColumn", () => {
|
|
1080
|
+
it("removes the current column", () => {
|
|
1081
|
+
editor = createTestEditor(createTableHtml(2, 3));
|
|
1082
|
+
const tables = createTables();
|
|
1083
|
+
const table = editor.container.querySelector("table") as HTMLTableElement;
|
|
1084
|
+
const cell = getCell(table, 0, 1)!;
|
|
1085
|
+
setCursorInCell(cell);
|
|
1086
|
+
|
|
1087
|
+
tables.deleteCurrentColumn();
|
|
1088
|
+
|
|
1089
|
+
expect(table.querySelector("thead tr")?.cells.length).toBe(2);
|
|
1090
|
+
expect(onContentChange).toHaveBeenCalled();
|
|
1091
|
+
});
|
|
1092
|
+
|
|
1093
|
+
it("deletes entire table when last column is deleted", () => {
|
|
1094
|
+
editor = createTestEditor(createTableHtml(2, 1));
|
|
1095
|
+
const tables = createTables();
|
|
1096
|
+
const table = editor.container.querySelector("table") as HTMLTableElement;
|
|
1097
|
+
const cell = getCell(table, 0, 0)!;
|
|
1098
|
+
setCursorInCell(cell);
|
|
1099
|
+
|
|
1100
|
+
tables.deleteCurrentColumn();
|
|
1101
|
+
|
|
1102
|
+
expect(editor.container.querySelector("table")).toBeNull();
|
|
1103
|
+
});
|
|
1104
|
+
|
|
1105
|
+
it("removes column from all rows", () => {
|
|
1106
|
+
editor = createTestEditor(createTableHtml(3, 3));
|
|
1107
|
+
const tables = createTables();
|
|
1108
|
+
const table = editor.container.querySelector("table") as HTMLTableElement;
|
|
1109
|
+
const cell = getCell(table, 0, 1)!;
|
|
1110
|
+
setCursorInCell(cell);
|
|
1111
|
+
|
|
1112
|
+
tables.deleteCurrentColumn();
|
|
1113
|
+
|
|
1114
|
+
// All rows should have 2 cells now
|
|
1115
|
+
const allRows = table.querySelectorAll("tr");
|
|
1116
|
+
allRows.forEach(row => {
|
|
1117
|
+
expect(row.cells.length).toBe(2);
|
|
1118
|
+
});
|
|
1119
|
+
});
|
|
1120
|
+
|
|
1121
|
+
it("does nothing when not in table", () => {
|
|
1122
|
+
editor = createTestEditor("<p>Not in table</p>");
|
|
1123
|
+
const tables = createTables();
|
|
1124
|
+
editor.setCursorInBlock(0, 0);
|
|
1125
|
+
|
|
1126
|
+
tables.deleteCurrentColumn();
|
|
1127
|
+
|
|
1128
|
+
expect(onContentChange).not.toHaveBeenCalled();
|
|
1129
|
+
});
|
|
1130
|
+
});
|
|
1131
|
+
|
|
1132
|
+
describe("deleteTable", () => {
|
|
1133
|
+
it("removes entire table", () => {
|
|
1134
|
+
editor = createTestEditor(createTableHtml(2, 2));
|
|
1135
|
+
const tables = createTables();
|
|
1136
|
+
const cell = editor.container.querySelector("th") as HTMLTableCellElement;
|
|
1137
|
+
setCursorInCell(cell);
|
|
1138
|
+
|
|
1139
|
+
tables.deleteTable();
|
|
1140
|
+
|
|
1141
|
+
expect(editor.container.querySelector("table")).toBeNull();
|
|
1142
|
+
expect(onContentChange).toHaveBeenCalled();
|
|
1143
|
+
});
|
|
1144
|
+
|
|
1145
|
+
it("creates paragraph if content area becomes empty", () => {
|
|
1146
|
+
editor = createTestEditor(createTableHtml(2, 2));
|
|
1147
|
+
const tables = createTables();
|
|
1148
|
+
const cell = editor.container.querySelector("th") as HTMLTableCellElement;
|
|
1149
|
+
setCursorInCell(cell);
|
|
1150
|
+
|
|
1151
|
+
tables.deleteTable();
|
|
1152
|
+
|
|
1153
|
+
expect(editor.container.querySelector("p")).not.toBeNull();
|
|
1154
|
+
});
|
|
1155
|
+
|
|
1156
|
+
it("focuses next sibling after deletion", () => {
|
|
1157
|
+
editor = createTestEditor(`${createTableHtml(2, 2)}<p>After table</p>`);
|
|
1158
|
+
const tables = createTables();
|
|
1159
|
+
const cell = editor.container.querySelector("th") as HTMLTableCellElement;
|
|
1160
|
+
setCursorInCell(cell);
|
|
1161
|
+
|
|
1162
|
+
tables.deleteTable();
|
|
1163
|
+
|
|
1164
|
+
expect(editor.container.querySelector("table")).toBeNull();
|
|
1165
|
+
expect(editor.container.querySelector("p")?.textContent).toBe("After table");
|
|
1166
|
+
});
|
|
1167
|
+
|
|
1168
|
+
it("does nothing when not in table", () => {
|
|
1169
|
+
editor = createTestEditor("<p>Not in table</p>");
|
|
1170
|
+
const tables = createTables();
|
|
1171
|
+
editor.setCursorInBlock(0, 0);
|
|
1172
|
+
|
|
1173
|
+
tables.deleteTable();
|
|
1174
|
+
|
|
1175
|
+
expect(onContentChange).not.toHaveBeenCalled();
|
|
1176
|
+
});
|
|
1177
|
+
|
|
1178
|
+
it("does nothing when contentRef is null", () => {
|
|
1179
|
+
editor = createTestEditor(createTableHtml(2, 2));
|
|
1180
|
+
const { deleteTable } = useTables({
|
|
1181
|
+
contentRef: { value: null },
|
|
1182
|
+
onContentChange
|
|
1183
|
+
});
|
|
1184
|
+
|
|
1185
|
+
deleteTable();
|
|
1186
|
+
|
|
1187
|
+
expect(editor.container.querySelector("table")).not.toBeNull();
|
|
1188
|
+
expect(onContentChange).not.toHaveBeenCalled();
|
|
1189
|
+
});
|
|
1190
|
+
});
|
|
1191
|
+
|
|
1192
|
+
describe("setColumnAlignmentLeft", () => {
|
|
1193
|
+
it("sets column alignment to left", () => {
|
|
1194
|
+
editor = createTestEditor(createTableHtml(2, 2));
|
|
1195
|
+
const tables = createTables();
|
|
1196
|
+
const table = editor.container.querySelector("table") as HTMLTableElement;
|
|
1197
|
+
const cell = getCell(table, 0, 0)!;
|
|
1198
|
+
cell.style.textAlign = "center";
|
|
1199
|
+
setCursorInCell(cell);
|
|
1200
|
+
|
|
1201
|
+
tables.setColumnAlignmentLeft();
|
|
1202
|
+
|
|
1203
|
+
// Left alignment removes the style property
|
|
1204
|
+
expect(cell.style.textAlign).toBe("");
|
|
1205
|
+
expect(onContentChange).toHaveBeenCalled();
|
|
1206
|
+
});
|
|
1207
|
+
|
|
1208
|
+
it("does nothing when not in table", () => {
|
|
1209
|
+
editor = createTestEditor("<p>Not in table</p>");
|
|
1210
|
+
const tables = createTables();
|
|
1211
|
+
editor.setCursorInBlock(0, 0);
|
|
1212
|
+
|
|
1213
|
+
tables.setColumnAlignmentLeft();
|
|
1214
|
+
|
|
1215
|
+
expect(onContentChange).not.toHaveBeenCalled();
|
|
1216
|
+
});
|
|
1217
|
+
});
|
|
1218
|
+
|
|
1219
|
+
describe("setColumnAlignmentCenter", () => {
|
|
1220
|
+
it("sets column alignment to center", () => {
|
|
1221
|
+
editor = createTestEditor(createTableHtml(2, 2));
|
|
1222
|
+
const tables = createTables();
|
|
1223
|
+
const table = editor.container.querySelector("table") as HTMLTableElement;
|
|
1224
|
+
const cell = getCell(table, 0, 0)!;
|
|
1225
|
+
setCursorInCell(cell);
|
|
1226
|
+
|
|
1227
|
+
tables.setColumnAlignmentCenter();
|
|
1228
|
+
|
|
1229
|
+
expect(cell.style.textAlign).toBe("center");
|
|
1230
|
+
expect(onContentChange).toHaveBeenCalled();
|
|
1231
|
+
});
|
|
1232
|
+
|
|
1233
|
+
it("applies alignment to all cells in column", () => {
|
|
1234
|
+
editor = createTestEditor(createTableHtml(3, 2));
|
|
1235
|
+
const tables = createTables();
|
|
1236
|
+
const table = editor.container.querySelector("table") as HTMLTableElement;
|
|
1237
|
+
const cell = getCell(table, 0, 0)!;
|
|
1238
|
+
setCursorInCell(cell);
|
|
1239
|
+
|
|
1240
|
+
tables.setColumnAlignmentCenter();
|
|
1241
|
+
|
|
1242
|
+
// All cells in first column should be centered
|
|
1243
|
+
expect(getCell(table, 0, 0)?.style.textAlign).toBe("center");
|
|
1244
|
+
expect(getCell(table, 1, 0)?.style.textAlign).toBe("center");
|
|
1245
|
+
expect(getCell(table, 2, 0)?.style.textAlign).toBe("center");
|
|
1246
|
+
});
|
|
1247
|
+
|
|
1248
|
+
it("does nothing when not in table", () => {
|
|
1249
|
+
editor = createTestEditor("<p>Not in table</p>");
|
|
1250
|
+
const tables = createTables();
|
|
1251
|
+
editor.setCursorInBlock(0, 0);
|
|
1252
|
+
|
|
1253
|
+
tables.setColumnAlignmentCenter();
|
|
1254
|
+
|
|
1255
|
+
expect(onContentChange).not.toHaveBeenCalled();
|
|
1256
|
+
});
|
|
1257
|
+
});
|
|
1258
|
+
|
|
1259
|
+
describe("setColumnAlignmentRight", () => {
|
|
1260
|
+
it("sets column alignment to right", () => {
|
|
1261
|
+
editor = createTestEditor(createTableHtml(2, 2));
|
|
1262
|
+
const tables = createTables();
|
|
1263
|
+
const table = editor.container.querySelector("table") as HTMLTableElement;
|
|
1264
|
+
const cell = getCell(table, 0, 0)!;
|
|
1265
|
+
setCursorInCell(cell);
|
|
1266
|
+
|
|
1267
|
+
tables.setColumnAlignmentRight();
|
|
1268
|
+
|
|
1269
|
+
expect(cell.style.textAlign).toBe("right");
|
|
1270
|
+
expect(onContentChange).toHaveBeenCalled();
|
|
1271
|
+
});
|
|
1272
|
+
|
|
1273
|
+
it("does nothing when not in table", () => {
|
|
1274
|
+
editor = createTestEditor("<p>Not in table</p>");
|
|
1275
|
+
const tables = createTables();
|
|
1276
|
+
editor.setCursorInBlock(0, 0);
|
|
1277
|
+
|
|
1278
|
+
tables.setColumnAlignmentRight();
|
|
1279
|
+
|
|
1280
|
+
expect(onContentChange).not.toHaveBeenCalled();
|
|
1281
|
+
});
|
|
1282
|
+
});
|
|
1283
|
+
|
|
1284
|
+
describe("handleTableTab", () => {
|
|
1285
|
+
it("navigates to next cell on Tab", () => {
|
|
1286
|
+
editor = createTestEditor(createTableHtml(2, 3));
|
|
1287
|
+
const tables = createTables();
|
|
1288
|
+
const table = editor.container.querySelector("table") as HTMLTableElement;
|
|
1289
|
+
const firstCell = getCell(table, 0, 0)!;
|
|
1290
|
+
setCursorInCell(firstCell);
|
|
1291
|
+
|
|
1292
|
+
const result = tables.handleTableTab(false);
|
|
1293
|
+
|
|
1294
|
+
expect(result).toBe(true);
|
|
1295
|
+
expect(tables.getCurrentCell()).toBe(getCell(table, 0, 1));
|
|
1296
|
+
});
|
|
1297
|
+
|
|
1298
|
+
it("navigates to previous cell on Shift+Tab", () => {
|
|
1299
|
+
editor = createTestEditor(createTableHtml(2, 3));
|
|
1300
|
+
const tables = createTables();
|
|
1301
|
+
const table = editor.container.querySelector("table") as HTMLTableElement;
|
|
1302
|
+
const secondCell = getCell(table, 0, 1)!;
|
|
1303
|
+
setCursorInCell(secondCell);
|
|
1304
|
+
|
|
1305
|
+
const result = tables.handleTableTab(true);
|
|
1306
|
+
|
|
1307
|
+
expect(result).toBe(true);
|
|
1308
|
+
expect(tables.getCurrentCell()).toBe(getCell(table, 0, 0));
|
|
1309
|
+
});
|
|
1310
|
+
|
|
1311
|
+
it("creates new row when Tab at end of table", () => {
|
|
1312
|
+
editor = createTestEditor(createTableHtml(2, 2));
|
|
1313
|
+
const tables = createTables();
|
|
1314
|
+
const table = editor.container.querySelector("table") as HTMLTableElement;
|
|
1315
|
+
const lastCell = getCell(table, 1, 1)!;
|
|
1316
|
+
setCursorInCell(lastCell);
|
|
1317
|
+
|
|
1318
|
+
const result = tables.handleTableTab(false);
|
|
1319
|
+
|
|
1320
|
+
expect(result).toBe(true);
|
|
1321
|
+
// Should have 3 rows now
|
|
1322
|
+
expect(table.querySelectorAll("tr").length).toBe(3);
|
|
1323
|
+
});
|
|
1324
|
+
|
|
1325
|
+
it("returns false on Shift+Tab at start of table", () => {
|
|
1326
|
+
editor = createTestEditor(createTableHtml(2, 2));
|
|
1327
|
+
const tables = createTables();
|
|
1328
|
+
const table = editor.container.querySelector("table") as HTMLTableElement;
|
|
1329
|
+
const firstCell = getCell(table, 0, 0)!;
|
|
1330
|
+
setCursorInCell(firstCell);
|
|
1331
|
+
|
|
1332
|
+
const result = tables.handleTableTab(true);
|
|
1333
|
+
|
|
1334
|
+
expect(result).toBe(false);
|
|
1335
|
+
});
|
|
1336
|
+
|
|
1337
|
+
it("returns false when not in table cell", () => {
|
|
1338
|
+
editor = createTestEditor("<p>Not in table</p>");
|
|
1339
|
+
const tables = createTables();
|
|
1340
|
+
editor.setCursorInBlock(0, 0);
|
|
1341
|
+
|
|
1342
|
+
expect(tables.handleTableTab(false)).toBe(false);
|
|
1343
|
+
expect(tables.handleTableTab(true)).toBe(false);
|
|
1344
|
+
});
|
|
1345
|
+
});
|
|
1346
|
+
|
|
1347
|
+
describe("handleTableEnter", () => {
|
|
1348
|
+
it("moves to cell below", () => {
|
|
1349
|
+
editor = createTestEditor(createTableHtml(3, 2));
|
|
1350
|
+
const tables = createTables();
|
|
1351
|
+
const table = editor.container.querySelector("table") as HTMLTableElement;
|
|
1352
|
+
const headerCell = getCell(table, 0, 0)!;
|
|
1353
|
+
setCursorInCell(headerCell);
|
|
1354
|
+
|
|
1355
|
+
const result = tables.handleTableEnter();
|
|
1356
|
+
|
|
1357
|
+
expect(result).toBe(true);
|
|
1358
|
+
expect(tables.getCurrentCell()).toBe(getCell(table, 1, 0));
|
|
1359
|
+
});
|
|
1360
|
+
|
|
1361
|
+
it("creates new row when at bottom of table", () => {
|
|
1362
|
+
editor = createTestEditor(createTableHtml(2, 2));
|
|
1363
|
+
const tables = createTables();
|
|
1364
|
+
const table = editor.container.querySelector("table") as HTMLTableElement;
|
|
1365
|
+
const bottomCell = getCell(table, 1, 0)!;
|
|
1366
|
+
setCursorInCell(bottomCell);
|
|
1367
|
+
|
|
1368
|
+
const result = tables.handleTableEnter();
|
|
1369
|
+
|
|
1370
|
+
expect(result).toBe(true);
|
|
1371
|
+
// Should have 3 rows now
|
|
1372
|
+
expect(table.querySelectorAll("tr").length).toBe(3);
|
|
1373
|
+
});
|
|
1374
|
+
|
|
1375
|
+
it("returns false when not in table cell", () => {
|
|
1376
|
+
editor = createTestEditor("<p>Not in table</p>");
|
|
1377
|
+
const tables = createTables();
|
|
1378
|
+
editor.setCursorInBlock(0, 0);
|
|
1379
|
+
|
|
1380
|
+
expect(tables.handleTableEnter()).toBe(false);
|
|
1381
|
+
});
|
|
1382
|
+
|
|
1383
|
+
it("maintains column position when moving down", () => {
|
|
1384
|
+
editor = createTestEditor(createTableHtml(3, 3));
|
|
1385
|
+
const tables = createTables();
|
|
1386
|
+
const table = editor.container.querySelector("table") as HTMLTableElement;
|
|
1387
|
+
const cell = getCell(table, 0, 1)!;
|
|
1388
|
+
setCursorInCell(cell);
|
|
1389
|
+
|
|
1390
|
+
tables.handleTableEnter();
|
|
1391
|
+
|
|
1392
|
+
const currentCell = tables.getCurrentCell();
|
|
1393
|
+
expect(currentCell).toBe(getCell(table, 1, 1));
|
|
1394
|
+
});
|
|
1395
|
+
});
|
|
1396
|
+
|
|
1397
|
+
describe("insertTable", () => {
|
|
1398
|
+
// Mock getBoundingClientRect for Range since jsdom doesn't implement it
|
|
1399
|
+
let originalGetBoundingClientRect: typeof Range.prototype.getBoundingClientRect;
|
|
1400
|
+
|
|
1401
|
+
beforeEach(() => {
|
|
1402
|
+
originalGetBoundingClientRect = Range.prototype.getBoundingClientRect;
|
|
1403
|
+
Range.prototype.getBoundingClientRect = vi.fn(() => ({
|
|
1404
|
+
x: 100,
|
|
1405
|
+
y: 100,
|
|
1406
|
+
width: 0,
|
|
1407
|
+
height: 0,
|
|
1408
|
+
top: 100,
|
|
1409
|
+
right: 100,
|
|
1410
|
+
bottom: 100,
|
|
1411
|
+
left: 100,
|
|
1412
|
+
toJSON: () => ({})
|
|
1413
|
+
}));
|
|
1414
|
+
});
|
|
1415
|
+
|
|
1416
|
+
afterEach(() => {
|
|
1417
|
+
Range.prototype.getBoundingClientRect = originalGetBoundingClientRect;
|
|
1418
|
+
});
|
|
1419
|
+
|
|
1420
|
+
it("creates default 3x3 table when no popover callback provided", () => {
|
|
1421
|
+
editor = createTestEditor("<p>Insert here</p>");
|
|
1422
|
+
const tables = createTables();
|
|
1423
|
+
editor.setCursorInBlock(0, 0);
|
|
1424
|
+
|
|
1425
|
+
tables.insertTable();
|
|
1426
|
+
|
|
1427
|
+
const table = editor.container.querySelector("table");
|
|
1428
|
+
expect(table).not.toBeNull();
|
|
1429
|
+
expect(table?.querySelector("thead")?.querySelectorAll("th").length).toBe(3);
|
|
1430
|
+
// 2 body rows
|
|
1431
|
+
expect(table?.querySelector("tbody")?.querySelectorAll("tr").length).toBe(2);
|
|
1432
|
+
});
|
|
1433
|
+
|
|
1434
|
+
it("calls onShowTablePopover when provided", () => {
|
|
1435
|
+
editor = createTestEditor("<p>Insert here</p>");
|
|
1436
|
+
const onShowTablePopover = vi.fn();
|
|
1437
|
+
const tables = useTables({
|
|
1438
|
+
contentRef: editor.contentRef,
|
|
1439
|
+
onContentChange,
|
|
1440
|
+
onShowTablePopover
|
|
1441
|
+
});
|
|
1442
|
+
editor.setCursorInBlock(0, 0);
|
|
1443
|
+
|
|
1444
|
+
tables.insertTable();
|
|
1445
|
+
|
|
1446
|
+
expect(onShowTablePopover).toHaveBeenCalled();
|
|
1447
|
+
const options = onShowTablePopover.mock.calls[0][0];
|
|
1448
|
+
expect(options.position).toBeDefined();
|
|
1449
|
+
expect(typeof options.onSubmit).toBe("function");
|
|
1450
|
+
expect(typeof options.onCancel).toBe("function");
|
|
1451
|
+
});
|
|
1452
|
+
|
|
1453
|
+
it("creates table with specified dimensions when popover submits", () => {
|
|
1454
|
+
editor = createTestEditor("<p>Insert here</p>");
|
|
1455
|
+
let capturedOptions: any = null;
|
|
1456
|
+
const onShowTablePopover = vi.fn((opts) => {
|
|
1457
|
+
capturedOptions = opts;
|
|
1458
|
+
});
|
|
1459
|
+
const tables = useTables({
|
|
1460
|
+
contentRef: editor.contentRef,
|
|
1461
|
+
onContentChange,
|
|
1462
|
+
onShowTablePopover
|
|
1463
|
+
});
|
|
1464
|
+
editor.setCursorInBlock(0, 0);
|
|
1465
|
+
|
|
1466
|
+
tables.insertTable();
|
|
1467
|
+
// Simulate popover submission
|
|
1468
|
+
capturedOptions.onSubmit(4, 5);
|
|
1469
|
+
|
|
1470
|
+
const table = editor.container.querySelector("table");
|
|
1471
|
+
expect(table?.querySelector("thead")?.querySelectorAll("th").length).toBe(5);
|
|
1472
|
+
// 3 body rows (4 total - 1 header)
|
|
1473
|
+
expect(table?.querySelector("tbody")?.querySelectorAll("tr").length).toBe(3);
|
|
1474
|
+
});
|
|
1475
|
+
|
|
1476
|
+
it("does nothing when contentRef is null", () => {
|
|
1477
|
+
editor = createTestEditor("<p>Test</p>");
|
|
1478
|
+
const { insertTable } = useTables({
|
|
1479
|
+
contentRef: { value: null },
|
|
1480
|
+
onContentChange
|
|
1481
|
+
});
|
|
1482
|
+
|
|
1483
|
+
insertTable();
|
|
1484
|
+
|
|
1485
|
+
expect(editor.container.querySelector("table")).toBeNull();
|
|
1486
|
+
});
|
|
1487
|
+
});
|
|
1488
|
+
|
|
1489
|
+
describe("edge cases", () => {
|
|
1490
|
+
it("handles table without tbody", () => {
|
|
1491
|
+
editor = createTestEditor("<table><thead><tr><th>Header</th></tr></thead></table>");
|
|
1492
|
+
const tables = createTables();
|
|
1493
|
+
const th = editor.container.querySelector("th") as HTMLTableCellElement;
|
|
1494
|
+
setCursorInCell(th);
|
|
1495
|
+
|
|
1496
|
+
expect(tables.isInTable()).toBe(true);
|
|
1497
|
+
expect(tables.isInTableCell()).toBe(true);
|
|
1498
|
+
});
|
|
1499
|
+
|
|
1500
|
+
it("handles table without thead", () => {
|
|
1501
|
+
editor = createTestEditor("<table><tbody><tr><td>Cell</td></tr></tbody></table>");
|
|
1502
|
+
const tables = createTables();
|
|
1503
|
+
const td = editor.container.querySelector("td") as HTMLTableCellElement;
|
|
1504
|
+
setCursorInCell(td);
|
|
1505
|
+
|
|
1506
|
+
expect(tables.isInTable()).toBe(true);
|
|
1507
|
+
expect(tables.getCurrentTable()).not.toBeNull();
|
|
1508
|
+
});
|
|
1509
|
+
|
|
1510
|
+
it("handles empty cells", () => {
|
|
1511
|
+
editor = createTestEditor("<table><thead><tr><th><br></th></tr></thead></table>");
|
|
1512
|
+
const tables = createTables();
|
|
1513
|
+
const th = editor.container.querySelector("th") as HTMLTableCellElement;
|
|
1514
|
+
|
|
1515
|
+
// Set cursor in empty cell
|
|
1516
|
+
const range = document.createRange();
|
|
1517
|
+
range.setStart(th, 0);
|
|
1518
|
+
range.collapse(true);
|
|
1519
|
+
const sel = window.getSelection();
|
|
1520
|
+
sel?.removeAllRanges();
|
|
1521
|
+
sel?.addRange(range);
|
|
1522
|
+
|
|
1523
|
+
expect(tables.isInTableCell()).toBe(true);
|
|
1524
|
+
});
|
|
1525
|
+
|
|
1526
|
+
it("handles nested content in cells", () => {
|
|
1527
|
+
editor = createTestEditor("<table><thead><tr><th><strong>Bold header</strong></th></tr></thead></table>");
|
|
1528
|
+
const tables = createTables();
|
|
1529
|
+
const strong = editor.container.querySelector("strong") as HTMLElement;
|
|
1530
|
+
editor.setCursor(strong.firstChild!, 2);
|
|
1531
|
+
|
|
1532
|
+
expect(tables.isInTable()).toBe(true);
|
|
1533
|
+
expect(tables.isInTableCell()).toBe(true);
|
|
1534
|
+
expect(tables.getCurrentCell()?.tagName).toBe("TH");
|
|
1535
|
+
});
|
|
1536
|
+
|
|
1537
|
+
it("handles multiple tables in document", () => {
|
|
1538
|
+
editor = createTestEditor(`
|
|
1539
|
+
${createTableHtml(2, 2)}
|
|
1540
|
+
<p>Between tables</p>
|
|
1541
|
+
${createTableHtml(3, 3)}
|
|
1542
|
+
`);
|
|
1543
|
+
const tables = createTables();
|
|
1544
|
+
|
|
1545
|
+
// Focus in second table
|
|
1546
|
+
const secondTable = editor.container.querySelectorAll("table")[1];
|
|
1547
|
+
const cell = secondTable.querySelector("th") as HTMLTableCellElement;
|
|
1548
|
+
setCursorInCell(cell);
|
|
1549
|
+
|
|
1550
|
+
expect(tables.getCurrentTable()).toBe(secondTable);
|
|
1551
|
+
});
|
|
1552
|
+
});
|
|
1553
|
+
|
|
1554
|
+
describe("return type", () => {
|
|
1555
|
+
beforeEach(() => {
|
|
1556
|
+
editor = createTestEditor("<p>test</p>");
|
|
1557
|
+
});
|
|
1558
|
+
|
|
1559
|
+
it("returns all expected functions", () => {
|
|
1560
|
+
const tables = createTables();
|
|
1561
|
+
|
|
1562
|
+
// Creation
|
|
1563
|
+
expect(typeof tables.insertTable).toBe("function");
|
|
1564
|
+
expect(typeof tables.createTable).toBe("function");
|
|
1565
|
+
|
|
1566
|
+
// Detection
|
|
1567
|
+
expect(typeof tables.isInTable).toBe("function");
|
|
1568
|
+
expect(typeof tables.isInTableCell).toBe("function");
|
|
1569
|
+
expect(typeof tables.getCurrentTable).toBe("function");
|
|
1570
|
+
expect(typeof tables.getCurrentCell).toBe("function");
|
|
1571
|
+
|
|
1572
|
+
// Navigation
|
|
1573
|
+
expect(typeof tables.navigateToNextCell).toBe("function");
|
|
1574
|
+
expect(typeof tables.navigateToPreviousCell).toBe("function");
|
|
1575
|
+
expect(typeof tables.navigateToCellBelow).toBe("function");
|
|
1576
|
+
expect(typeof tables.navigateToCellAbove).toBe("function");
|
|
1577
|
+
|
|
1578
|
+
// Row operations
|
|
1579
|
+
expect(typeof tables.insertRowAbove).toBe("function");
|
|
1580
|
+
expect(typeof tables.insertRowBelow).toBe("function");
|
|
1581
|
+
expect(typeof tables.deleteCurrentRow).toBe("function");
|
|
1582
|
+
|
|
1583
|
+
// Column operations
|
|
1584
|
+
expect(typeof tables.insertColumnLeft).toBe("function");
|
|
1585
|
+
expect(typeof tables.insertColumnRight).toBe("function");
|
|
1586
|
+
expect(typeof tables.deleteCurrentColumn).toBe("function");
|
|
1587
|
+
|
|
1588
|
+
// Table operations
|
|
1589
|
+
expect(typeof tables.deleteTable).toBe("function");
|
|
1590
|
+
|
|
1591
|
+
// Alignment
|
|
1592
|
+
expect(typeof tables.setColumnAlignmentLeft).toBe("function");
|
|
1593
|
+
expect(typeof tables.setColumnAlignmentCenter).toBe("function");
|
|
1594
|
+
expect(typeof tables.setColumnAlignmentRight).toBe("function");
|
|
1595
|
+
|
|
1596
|
+
// Key handlers
|
|
1597
|
+
expect(typeof tables.handleTableTab).toBe("function");
|
|
1598
|
+
expect(typeof tables.handleTableEnter).toBe("function");
|
|
1599
|
+
});
|
|
1600
|
+
});
|
|
1601
|
+
});
|