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,1107 @@
|
|
|
1
|
+
import { Ref } from "vue";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Position for table popover
|
|
5
|
+
*/
|
|
6
|
+
export interface TablePopoverPosition {
|
|
7
|
+
x: number;
|
|
8
|
+
y: number;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Options passed to the onShowTablePopover callback
|
|
13
|
+
*/
|
|
14
|
+
export interface ShowTablePopoverOptions {
|
|
15
|
+
/** Position in viewport where popover should appear */
|
|
16
|
+
position: TablePopoverPosition;
|
|
17
|
+
/** Callback to complete the table insertion with specified dimensions */
|
|
18
|
+
onSubmit: (rows: number, cols: number) => void;
|
|
19
|
+
/** Callback to cancel the operation */
|
|
20
|
+
onCancel: () => void;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Options for useTables composable
|
|
25
|
+
*/
|
|
26
|
+
export interface UseTablesOptions {
|
|
27
|
+
contentRef: Ref<HTMLElement | null>;
|
|
28
|
+
onContentChange: () => void;
|
|
29
|
+
/** Callback to show the table popover UI for dimension selection */
|
|
30
|
+
onShowTablePopover?: (options: ShowTablePopoverOptions) => void;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Return type for useTables composable
|
|
35
|
+
*/
|
|
36
|
+
export interface UseTablesReturn {
|
|
37
|
+
// Creation
|
|
38
|
+
/** Insert a table - shows popover if callback provided, otherwise creates default 3x3 */
|
|
39
|
+
insertTable: () => void;
|
|
40
|
+
/** Create a table with specific dimensions */
|
|
41
|
+
createTable: (rows: number, cols: number) => void;
|
|
42
|
+
|
|
43
|
+
// Detection
|
|
44
|
+
/** Check if cursor is inside a table */
|
|
45
|
+
isInTable: () => boolean;
|
|
46
|
+
/** Check if cursor is inside a table cell */
|
|
47
|
+
isInTableCell: () => boolean;
|
|
48
|
+
/** Get the current table element */
|
|
49
|
+
getCurrentTable: () => HTMLTableElement | null;
|
|
50
|
+
/** Get the current table cell element */
|
|
51
|
+
getCurrentCell: () => HTMLTableCellElement | null;
|
|
52
|
+
|
|
53
|
+
// Navigation
|
|
54
|
+
/** Navigate to the next cell (right, then next row). Returns false if at end. */
|
|
55
|
+
navigateToNextCell: () => boolean;
|
|
56
|
+
/** Navigate to the previous cell. Returns false if at start. */
|
|
57
|
+
navigateToPreviousCell: () => boolean;
|
|
58
|
+
/** Navigate to the cell directly below. Returns false if at bottom row. */
|
|
59
|
+
navigateToCellBelow: () => boolean;
|
|
60
|
+
/** Navigate to the cell directly above. Returns false if at top row. */
|
|
61
|
+
navigateToCellAbove: () => boolean;
|
|
62
|
+
|
|
63
|
+
// Row operations
|
|
64
|
+
/** Insert a new row above the current row */
|
|
65
|
+
insertRowAbove: () => void;
|
|
66
|
+
/** Insert a new row below the current row */
|
|
67
|
+
insertRowBelow: () => void;
|
|
68
|
+
/** Delete the current row */
|
|
69
|
+
deleteCurrentRow: () => void;
|
|
70
|
+
|
|
71
|
+
// Column operations
|
|
72
|
+
/** Insert a new column to the left */
|
|
73
|
+
insertColumnLeft: () => void;
|
|
74
|
+
/** Insert a new column to the right */
|
|
75
|
+
insertColumnRight: () => void;
|
|
76
|
+
/** Delete the current column */
|
|
77
|
+
deleteCurrentColumn: () => void;
|
|
78
|
+
|
|
79
|
+
// Table operations
|
|
80
|
+
/** Delete the entire table */
|
|
81
|
+
deleteTable: () => void;
|
|
82
|
+
|
|
83
|
+
// Alignment
|
|
84
|
+
/** Set column alignment to left */
|
|
85
|
+
setColumnAlignmentLeft: () => void;
|
|
86
|
+
/** Set column alignment to center */
|
|
87
|
+
setColumnAlignmentCenter: () => void;
|
|
88
|
+
/** Set column alignment to right */
|
|
89
|
+
setColumnAlignmentRight: () => void;
|
|
90
|
+
|
|
91
|
+
// Key handlers
|
|
92
|
+
/** Handle Tab key in table - returns true if handled */
|
|
93
|
+
handleTableTab: (shift: boolean) => boolean;
|
|
94
|
+
/** Handle Enter key in table - returns true if handled */
|
|
95
|
+
handleTableEnter: () => boolean;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Get the cursor position in viewport coordinates
|
|
100
|
+
*/
|
|
101
|
+
function getCursorPosition(): TablePopoverPosition {
|
|
102
|
+
const selection = window.getSelection();
|
|
103
|
+
if (!selection || !selection.rangeCount) {
|
|
104
|
+
return { x: window.innerWidth / 2, y: window.innerHeight / 2 };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const range = selection.getRangeAt(0);
|
|
108
|
+
const rect = range.getBoundingClientRect();
|
|
109
|
+
|
|
110
|
+
// If rect has no dimensions (collapsed cursor), use the start position
|
|
111
|
+
if (rect.width === 0 && rect.height === 0) {
|
|
112
|
+
return {
|
|
113
|
+
x: rect.left || window.innerWidth / 2,
|
|
114
|
+
y: rect.bottom || window.innerHeight / 2
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Center horizontally on the selection, position below
|
|
119
|
+
return {
|
|
120
|
+
x: rect.left + (rect.width / 2),
|
|
121
|
+
y: rect.bottom
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Find the table ancestor if one exists
|
|
127
|
+
*/
|
|
128
|
+
function findTableAncestor(node: Node | null, contentRef: HTMLElement): HTMLTableElement | null {
|
|
129
|
+
if (!node) return null;
|
|
130
|
+
|
|
131
|
+
let current: Node | null = node;
|
|
132
|
+
while (current && current !== contentRef) {
|
|
133
|
+
if (current.nodeType === Node.ELEMENT_NODE && (current as Element).tagName === "TABLE") {
|
|
134
|
+
return current as HTMLTableElement;
|
|
135
|
+
}
|
|
136
|
+
current = current.parentNode;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Find the table cell (TD or TH) ancestor if one exists
|
|
144
|
+
*/
|
|
145
|
+
function findCellAncestor(node: Node | null, contentRef: HTMLElement): HTMLTableCellElement | null {
|
|
146
|
+
if (!node) return null;
|
|
147
|
+
|
|
148
|
+
let current: Node | null = node;
|
|
149
|
+
while (current && current !== contentRef) {
|
|
150
|
+
if (current.nodeType === Node.ELEMENT_NODE) {
|
|
151
|
+
const tag = (current as Element).tagName;
|
|
152
|
+
if (tag === "TD" || tag === "TH") {
|
|
153
|
+
return current as HTMLTableCellElement;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
current = current.parentNode;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return null;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Get the row and column indices of a cell
|
|
164
|
+
*/
|
|
165
|
+
function getCellCoordinates(cell: HTMLTableCellElement): { row: number; col: number } {
|
|
166
|
+
const row = cell.parentElement as HTMLTableRowElement | null;
|
|
167
|
+
if (!row) return { row: -1, col: -1 };
|
|
168
|
+
|
|
169
|
+
const table = row.parentElement?.parentElement as HTMLTableElement | null
|
|
170
|
+
|| row.parentElement as HTMLTableElement | null;
|
|
171
|
+
if (!table) return { row: -1, col: -1 };
|
|
172
|
+
|
|
173
|
+
// Get column index
|
|
174
|
+
const colIndex = Array.from(row.cells).indexOf(cell);
|
|
175
|
+
|
|
176
|
+
// Get row index (accounting for thead/tbody)
|
|
177
|
+
let rowIndex = 0;
|
|
178
|
+
const rows = getAllTableRows(table);
|
|
179
|
+
for (let i = 0; i < rows.length; i++) {
|
|
180
|
+
if (rows[i] === row) {
|
|
181
|
+
rowIndex = i;
|
|
182
|
+
break;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return { row: rowIndex, col: colIndex };
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Get all rows from a table (including thead and tbody)
|
|
191
|
+
*/
|
|
192
|
+
function getAllTableRows(table: HTMLTableElement): HTMLTableRowElement[] {
|
|
193
|
+
const rows: HTMLTableRowElement[] = [];
|
|
194
|
+
|
|
195
|
+
// Get rows from thead
|
|
196
|
+
if (table.tHead) {
|
|
197
|
+
rows.push(...Array.from(table.tHead.rows));
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Get rows from tbody(s)
|
|
201
|
+
for (const tbody of Array.from(table.tBodies)) {
|
|
202
|
+
rows.push(...Array.from(tbody.rows));
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Get any direct row children (tables without thead/tbody)
|
|
206
|
+
for (const child of Array.from(table.children)) {
|
|
207
|
+
if (child.tagName === "TR") {
|
|
208
|
+
rows.push(child as HTMLTableRowElement);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
return rows;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Get the number of columns in a table
|
|
217
|
+
*/
|
|
218
|
+
function getColumnCount(table: HTMLTableElement): number {
|
|
219
|
+
const rows = getAllTableRows(table);
|
|
220
|
+
if (rows.length === 0) return 0;
|
|
221
|
+
return rows[0].cells.length;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Get a cell at specific coordinates
|
|
226
|
+
*/
|
|
227
|
+
function getCellAt(table: HTMLTableElement, rowIndex: number, colIndex: number): HTMLTableCellElement | null {
|
|
228
|
+
const rows = getAllTableRows(table);
|
|
229
|
+
if (rowIndex < 0 || rowIndex >= rows.length) return null;
|
|
230
|
+
|
|
231
|
+
const row = rows[rowIndex];
|
|
232
|
+
if (colIndex < 0 || colIndex >= row.cells.length) return null;
|
|
233
|
+
|
|
234
|
+
return row.cells[colIndex];
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Get the cursor offset (character position) within a cell
|
|
239
|
+
* This measures the text length from the start of the cell to the cursor position
|
|
240
|
+
*/
|
|
241
|
+
function getCursorOffsetInCell(cell: HTMLTableCellElement): number {
|
|
242
|
+
const selection = window.getSelection();
|
|
243
|
+
if (!selection || !selection.rangeCount) return 0;
|
|
244
|
+
|
|
245
|
+
const range = selection.getRangeAt(0);
|
|
246
|
+
|
|
247
|
+
// Create a range from cell start to cursor position
|
|
248
|
+
const preCaretRange = document.createRange();
|
|
249
|
+
preCaretRange.selectNodeContents(cell);
|
|
250
|
+
preCaretRange.setEnd(range.startContainer, range.startOffset);
|
|
251
|
+
|
|
252
|
+
// Get text length up to cursor
|
|
253
|
+
return preCaretRange.toString().length;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Set the cursor at a specific character offset within a cell
|
|
258
|
+
* If the offset exceeds the cell's text length, places cursor at the end
|
|
259
|
+
*/
|
|
260
|
+
function setCursorOffsetInCell(cell: HTMLTableCellElement, targetOffset: number): void {
|
|
261
|
+
const selection = window.getSelection();
|
|
262
|
+
if (!selection) return;
|
|
263
|
+
|
|
264
|
+
const textContent = cell.textContent || "";
|
|
265
|
+
const maxOffset = textContent.length;
|
|
266
|
+
const offset = Math.min(targetOffset, maxOffset);
|
|
267
|
+
|
|
268
|
+
// Find the text node and position for this offset
|
|
269
|
+
let currentOffset = 0;
|
|
270
|
+
const walker = document.createTreeWalker(cell, NodeFilter.SHOW_TEXT);
|
|
271
|
+
let node: Text | null = null;
|
|
272
|
+
|
|
273
|
+
while ((node = walker.nextNode() as Text | null)) {
|
|
274
|
+
const nodeLength = node.textContent?.length || 0;
|
|
275
|
+
if (currentOffset + nodeLength >= offset) {
|
|
276
|
+
// Found the right node
|
|
277
|
+
const range = document.createRange();
|
|
278
|
+
range.setStart(node, offset - currentOffset);
|
|
279
|
+
range.collapse(true);
|
|
280
|
+
selection.removeAllRanges();
|
|
281
|
+
selection.addRange(range);
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
currentOffset += nodeLength;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// Fallback: place at end of cell
|
|
288
|
+
const range = document.createRange();
|
|
289
|
+
range.selectNodeContents(cell);
|
|
290
|
+
range.collapse(false);
|
|
291
|
+
selection.removeAllRanges();
|
|
292
|
+
selection.addRange(range);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Get the first text node with actual content in an element
|
|
297
|
+
*/
|
|
298
|
+
function getFirstTextNode(node: Node): Text | null {
|
|
299
|
+
if (node.nodeType === Node.TEXT_NODE) {
|
|
300
|
+
// Return the text node even if it's empty/whitespace - we can position at offset 0
|
|
301
|
+
return node as Text;
|
|
302
|
+
}
|
|
303
|
+
for (const child of Array.from(node.childNodes)) {
|
|
304
|
+
// Skip BR elements - they're placeholders for empty cells
|
|
305
|
+
if (child.nodeType === Node.ELEMENT_NODE && (child as Element).tagName === "BR") {
|
|
306
|
+
continue;
|
|
307
|
+
}
|
|
308
|
+
const found = getFirstTextNode(child);
|
|
309
|
+
if (found) return found;
|
|
310
|
+
}
|
|
311
|
+
return null;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* Place cursor at the start of a cell without selecting text
|
|
316
|
+
*/
|
|
317
|
+
function focusCell(cell: HTMLTableCellElement): void {
|
|
318
|
+
const selection = window.getSelection();
|
|
319
|
+
if (!selection) return;
|
|
320
|
+
|
|
321
|
+
const range = document.createRange();
|
|
322
|
+
|
|
323
|
+
// Find first text node
|
|
324
|
+
const firstTextNode = getFirstTextNode(cell);
|
|
325
|
+
|
|
326
|
+
if (firstTextNode) {
|
|
327
|
+
// Place cursor at start of text node
|
|
328
|
+
range.setStart(firstTextNode, 0);
|
|
329
|
+
range.collapse(true); // Collapse to start, no selection
|
|
330
|
+
} else {
|
|
331
|
+
// Empty cell or only has BR - position at start of cell contents
|
|
332
|
+
range.selectNodeContents(cell);
|
|
333
|
+
range.collapse(true); // Collapse to start, no selection
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
selection.removeAllRanges();
|
|
337
|
+
selection.addRange(range);
|
|
338
|
+
|
|
339
|
+
// Ensure the cell element itself has focus for keyboard events
|
|
340
|
+
cell.focus();
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* Select all content in a cell
|
|
345
|
+
*/
|
|
346
|
+
function selectCellContent(cell: HTMLTableCellElement): void {
|
|
347
|
+
const selection = window.getSelection();
|
|
348
|
+
if (!selection) return;
|
|
349
|
+
|
|
350
|
+
const range = document.createRange();
|
|
351
|
+
range.selectNodeContents(cell);
|
|
352
|
+
selection.removeAllRanges();
|
|
353
|
+
selection.addRange(range);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* Dispatch an input event to trigger content sync
|
|
358
|
+
*/
|
|
359
|
+
function dispatchInputEvent(element: HTMLElement): void {
|
|
360
|
+
element.dispatchEvent(new InputEvent("input", { bubbles: true }));
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/**
|
|
364
|
+
* Get the current selection range if valid
|
|
365
|
+
*/
|
|
366
|
+
function getCurrentSelectionRange(): Range | null {
|
|
367
|
+
const selection = window.getSelection();
|
|
368
|
+
if (!selection || !selection.rangeCount) return null;
|
|
369
|
+
return selection.getRangeAt(0);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/**
|
|
373
|
+
* Create a table cell with initial content
|
|
374
|
+
*/
|
|
375
|
+
function createCell(isHeader: boolean): HTMLTableCellElement {
|
|
376
|
+
const cell = document.createElement(isHeader ? "th" : "td");
|
|
377
|
+
// Add a BR to make empty cell focusable
|
|
378
|
+
cell.appendChild(document.createElement("br"));
|
|
379
|
+
return cell;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/**
|
|
383
|
+
* Create a table row with specified number of cells
|
|
384
|
+
*/
|
|
385
|
+
function createRow(colCount: number, isHeader: boolean): HTMLTableRowElement {
|
|
386
|
+
const row = document.createElement("tr");
|
|
387
|
+
for (let i = 0; i < colCount; i++) {
|
|
388
|
+
row.appendChild(createCell(isHeader));
|
|
389
|
+
}
|
|
390
|
+
return row;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/**
|
|
394
|
+
* Get the alignment of a column based on the first cell
|
|
395
|
+
*/
|
|
396
|
+
function getColumnAlignment(table: HTMLTableElement, colIndex: number): "left" | "center" | "right" {
|
|
397
|
+
const rows = getAllTableRows(table);
|
|
398
|
+
if (rows.length === 0) return "left";
|
|
399
|
+
|
|
400
|
+
const cell = rows[0].cells[colIndex];
|
|
401
|
+
if (!cell) return "left";
|
|
402
|
+
|
|
403
|
+
const textAlign = cell.style.textAlign;
|
|
404
|
+
if (textAlign === "center") return "center";
|
|
405
|
+
if (textAlign === "right") return "right";
|
|
406
|
+
return "left";
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/**
|
|
410
|
+
* Set alignment for all cells in a column
|
|
411
|
+
*/
|
|
412
|
+
function setColumnAlignment(table: HTMLTableElement, colIndex: number, alignment: "left" | "center" | "right"): void {
|
|
413
|
+
const rows = getAllTableRows(table);
|
|
414
|
+
for (const row of rows) {
|
|
415
|
+
const cell = row.cells[colIndex];
|
|
416
|
+
if (cell) {
|
|
417
|
+
if (alignment === "left") {
|
|
418
|
+
cell.style.removeProperty("text-align");
|
|
419
|
+
} else {
|
|
420
|
+
cell.style.textAlign = alignment;
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
/**
|
|
427
|
+
* Composable for table operations in markdown editor
|
|
428
|
+
*/
|
|
429
|
+
export function useTables(options: UseTablesOptions): UseTablesReturn {
|
|
430
|
+
const { contentRef, onContentChange, onShowTablePopover } = options;
|
|
431
|
+
|
|
432
|
+
// Store the selection range so we can restore it after popover interaction
|
|
433
|
+
let savedRange: Range | null = null;
|
|
434
|
+
|
|
435
|
+
/**
|
|
436
|
+
* Save the current selection for later restoration
|
|
437
|
+
*/
|
|
438
|
+
function saveSelection(): void {
|
|
439
|
+
const selection = window.getSelection();
|
|
440
|
+
if (selection && selection.rangeCount > 0) {
|
|
441
|
+
savedRange = selection.getRangeAt(0).cloneRange();
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
/**
|
|
446
|
+
* Restore the previously saved selection
|
|
447
|
+
*/
|
|
448
|
+
function restoreSelection(): void {
|
|
449
|
+
if (savedRange) {
|
|
450
|
+
const selection = window.getSelection();
|
|
451
|
+
selection?.removeAllRanges();
|
|
452
|
+
selection?.addRange(savedRange);
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
/**
|
|
457
|
+
* Check if cursor is inside a table
|
|
458
|
+
*/
|
|
459
|
+
function isInTable(): boolean {
|
|
460
|
+
return getCurrentTable() !== null;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
/**
|
|
464
|
+
* Check if cursor is inside a table cell
|
|
465
|
+
*/
|
|
466
|
+
function isInTableCell(): boolean {
|
|
467
|
+
return getCurrentCell() !== null;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
/**
|
|
471
|
+
* Get the current table element
|
|
472
|
+
*/
|
|
473
|
+
function getCurrentTable(): HTMLTableElement | null {
|
|
474
|
+
if (!contentRef.value) return null;
|
|
475
|
+
const range = getCurrentSelectionRange();
|
|
476
|
+
if (!range) return null;
|
|
477
|
+
return findTableAncestor(range.startContainer, contentRef.value);
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
/**
|
|
481
|
+
* Get the current table cell element
|
|
482
|
+
*/
|
|
483
|
+
function getCurrentCell(): HTMLTableCellElement | null {
|
|
484
|
+
if (!contentRef.value) return null;
|
|
485
|
+
const range = getCurrentSelectionRange();
|
|
486
|
+
if (!range) return null;
|
|
487
|
+
return findCellAncestor(range.startContainer, contentRef.value);
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
/**
|
|
491
|
+
* Get both current cell and table, or null if not in a table
|
|
492
|
+
*/
|
|
493
|
+
function getCurrentCellAndTable(): { cell: HTMLTableCellElement; table: HTMLTableElement } | null {
|
|
494
|
+
const cell = getCurrentCell();
|
|
495
|
+
const table = getCurrentTable();
|
|
496
|
+
if (!cell || !table) return null;
|
|
497
|
+
return { cell, table };
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
/**
|
|
501
|
+
* Helper to notify content changes after table modifications
|
|
502
|
+
*/
|
|
503
|
+
function notifyContentChange(): void {
|
|
504
|
+
if (contentRef.value) {
|
|
505
|
+
dispatchInputEvent(contentRef.value);
|
|
506
|
+
onContentChange();
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
/**
|
|
511
|
+
* Insert a table - shows popover if callback provided, otherwise creates default 3x3
|
|
512
|
+
*/
|
|
513
|
+
function insertTable(): void {
|
|
514
|
+
if (!contentRef.value) return;
|
|
515
|
+
|
|
516
|
+
saveSelection();
|
|
517
|
+
const position = getCursorPosition();
|
|
518
|
+
|
|
519
|
+
if (onShowTablePopover) {
|
|
520
|
+
onShowTablePopover({
|
|
521
|
+
position,
|
|
522
|
+
onSubmit: (rows: number, cols: number) => {
|
|
523
|
+
restoreSelection();
|
|
524
|
+
createTable(rows, cols);
|
|
525
|
+
},
|
|
526
|
+
onCancel: () => {
|
|
527
|
+
restoreSelection();
|
|
528
|
+
contentRef.value?.focus();
|
|
529
|
+
}
|
|
530
|
+
});
|
|
531
|
+
} else {
|
|
532
|
+
// Default to 3x3 table if no popover callback
|
|
533
|
+
createTable(3, 3);
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
/**
|
|
538
|
+
* Create a table with specific dimensions
|
|
539
|
+
*/
|
|
540
|
+
function createTable(rows: number, cols: number): void {
|
|
541
|
+
if (!contentRef.value) return;
|
|
542
|
+
if (rows < 1 || cols < 1) return;
|
|
543
|
+
|
|
544
|
+
const range = getCurrentSelectionRange();
|
|
545
|
+
if (!range) return;
|
|
546
|
+
|
|
547
|
+
// Check if selection is within our content area
|
|
548
|
+
if (!contentRef.value.contains(range.startContainer)) return;
|
|
549
|
+
|
|
550
|
+
// Create table structure
|
|
551
|
+
const table = document.createElement("table");
|
|
552
|
+
|
|
553
|
+
// Create thead with first row (headers)
|
|
554
|
+
const thead = document.createElement("thead");
|
|
555
|
+
thead.appendChild(createRow(cols, true));
|
|
556
|
+
table.appendChild(thead);
|
|
557
|
+
|
|
558
|
+
// Create tbody with remaining rows
|
|
559
|
+
if (rows > 1) {
|
|
560
|
+
const tbody = document.createElement("tbody");
|
|
561
|
+
for (let i = 1; i < rows; i++) {
|
|
562
|
+
tbody.appendChild(createRow(cols, false));
|
|
563
|
+
}
|
|
564
|
+
table.appendChild(tbody);
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
// Insert table at cursor position
|
|
568
|
+
// First, find a suitable insertion point
|
|
569
|
+
let insertionPoint: Node | null = range.startContainer;
|
|
570
|
+
|
|
571
|
+
// Walk up to find a block-level element
|
|
572
|
+
while (insertionPoint && insertionPoint !== contentRef.value) {
|
|
573
|
+
if (insertionPoint.nodeType === Node.ELEMENT_NODE) {
|
|
574
|
+
const tag = (insertionPoint as Element).tagName;
|
|
575
|
+
if (["P", "DIV", "H1", "H2", "H3", "H4", "H5", "H6"].includes(tag)) {
|
|
576
|
+
break;
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
insertionPoint = insertionPoint.parentNode;
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
if (insertionPoint && insertionPoint !== contentRef.value) {
|
|
583
|
+
// Insert table after the current block
|
|
584
|
+
insertionPoint.parentNode?.insertBefore(table, insertionPoint.nextSibling);
|
|
585
|
+
} else {
|
|
586
|
+
// Insert at end of content
|
|
587
|
+
contentRef.value.appendChild(table);
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
// Focus the first cell
|
|
591
|
+
const firstCell = table.querySelector("th, td") as HTMLTableCellElement | null;
|
|
592
|
+
if (firstCell) {
|
|
593
|
+
focusCell(firstCell);
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
notifyContentChange();
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
/**
|
|
600
|
+
* Navigate to the next cell (right, then next row)
|
|
601
|
+
* Returns false if at the end of the table
|
|
602
|
+
*/
|
|
603
|
+
function navigateToNextCell(): boolean {
|
|
604
|
+
const context = getCurrentCellAndTable();
|
|
605
|
+
if (!context) return false;
|
|
606
|
+
|
|
607
|
+
const { cell, table } = context;
|
|
608
|
+
const { row, col } = getCellCoordinates(cell);
|
|
609
|
+
const rows = getAllTableRows(table);
|
|
610
|
+
const colCount = getColumnCount(table);
|
|
611
|
+
|
|
612
|
+
// Try next column
|
|
613
|
+
if (col + 1 < colCount) {
|
|
614
|
+
const nextCell = getCellAt(table, row, col + 1);
|
|
615
|
+
if (nextCell) {
|
|
616
|
+
selectCellContent(nextCell);
|
|
617
|
+
return true;
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
// Try first column of next row
|
|
622
|
+
if (row + 1 < rows.length) {
|
|
623
|
+
const nextCell = getCellAt(table, row + 1, 0);
|
|
624
|
+
if (nextCell) {
|
|
625
|
+
selectCellContent(nextCell);
|
|
626
|
+
return true;
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
return false;
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
/**
|
|
634
|
+
* Navigate to the previous cell
|
|
635
|
+
* Returns false if at the start of the table
|
|
636
|
+
*/
|
|
637
|
+
function navigateToPreviousCell(): boolean {
|
|
638
|
+
const context = getCurrentCellAndTable();
|
|
639
|
+
if (!context) return false;
|
|
640
|
+
|
|
641
|
+
const { cell, table } = context;
|
|
642
|
+
const { row, col } = getCellCoordinates(cell);
|
|
643
|
+
const colCount = getColumnCount(table);
|
|
644
|
+
|
|
645
|
+
// Try previous column
|
|
646
|
+
if (col > 0) {
|
|
647
|
+
const prevCell = getCellAt(table, row, col - 1);
|
|
648
|
+
if (prevCell) {
|
|
649
|
+
selectCellContent(prevCell);
|
|
650
|
+
return true;
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
// Try last column of previous row
|
|
655
|
+
if (row > 0) {
|
|
656
|
+
const prevCell = getCellAt(table, row - 1, colCount - 1);
|
|
657
|
+
if (prevCell) {
|
|
658
|
+
selectCellContent(prevCell);
|
|
659
|
+
return true;
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
return false;
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
/**
|
|
667
|
+
* Navigate to the cell directly below
|
|
668
|
+
* Returns false if at the bottom row of the table
|
|
669
|
+
* Preserves cursor offset position from the source cell
|
|
670
|
+
*/
|
|
671
|
+
function navigateToCellBelow(): boolean {
|
|
672
|
+
const context = getCurrentCellAndTable();
|
|
673
|
+
if (!context) return false;
|
|
674
|
+
|
|
675
|
+
const { cell, table } = context;
|
|
676
|
+
|
|
677
|
+
// Get current cursor offset BEFORE navigating
|
|
678
|
+
const cursorOffset = getCursorOffsetInCell(cell);
|
|
679
|
+
|
|
680
|
+
const { row, col } = getCellCoordinates(cell);
|
|
681
|
+
const rows = getAllTableRows(table);
|
|
682
|
+
|
|
683
|
+
if (row + 1 < rows.length) {
|
|
684
|
+
const belowCell = getCellAt(table, row + 1, col);
|
|
685
|
+
if (belowCell) {
|
|
686
|
+
// Set cursor at same offset in target cell (clamped to cell length)
|
|
687
|
+
setCursorOffsetInCell(belowCell, cursorOffset);
|
|
688
|
+
return true;
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
return false;
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
/**
|
|
696
|
+
* Navigate to the cell directly above
|
|
697
|
+
* Returns false if at the top row of the table
|
|
698
|
+
* Preserves cursor offset position from the source cell
|
|
699
|
+
*/
|
|
700
|
+
function navigateToCellAbove(): boolean {
|
|
701
|
+
const context = getCurrentCellAndTable();
|
|
702
|
+
if (!context) return false;
|
|
703
|
+
|
|
704
|
+
const { cell, table } = context;
|
|
705
|
+
|
|
706
|
+
// Get current cursor offset BEFORE navigating
|
|
707
|
+
const cursorOffset = getCursorOffsetInCell(cell);
|
|
708
|
+
|
|
709
|
+
const { row, col } = getCellCoordinates(cell);
|
|
710
|
+
|
|
711
|
+
if (row > 0) {
|
|
712
|
+
const aboveCell = getCellAt(table, row - 1, col);
|
|
713
|
+
if (aboveCell) {
|
|
714
|
+
// Set cursor at same offset in target cell (clamped to cell length)
|
|
715
|
+
setCursorOffsetInCell(aboveCell, cursorOffset);
|
|
716
|
+
return true;
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
return false;
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
/**
|
|
724
|
+
* Insert a new row above the current row
|
|
725
|
+
*/
|
|
726
|
+
function insertRowAbove(): void {
|
|
727
|
+
const context = getCurrentCellAndTable();
|
|
728
|
+
if (!context) return;
|
|
729
|
+
|
|
730
|
+
const { cell } = context;
|
|
731
|
+
const row = cell.parentElement as HTMLTableRowElement | null;
|
|
732
|
+
if (!row) return;
|
|
733
|
+
|
|
734
|
+
const colCount = row.cells.length;
|
|
735
|
+
|
|
736
|
+
// Create new row (use TD cells even if inserting above header)
|
|
737
|
+
const newRow = createRow(colCount, false);
|
|
738
|
+
|
|
739
|
+
// Insert new row
|
|
740
|
+
row.parentNode?.insertBefore(newRow, row);
|
|
741
|
+
|
|
742
|
+
// Focus the first cell of the new row
|
|
743
|
+
const firstNewCell = newRow.cells[0];
|
|
744
|
+
if (firstNewCell) {
|
|
745
|
+
focusCell(firstNewCell);
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
notifyContentChange();
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
/**
|
|
752
|
+
* Insert a new row below the current row
|
|
753
|
+
*/
|
|
754
|
+
function insertRowBelow(): void {
|
|
755
|
+
const context = getCurrentCellAndTable();
|
|
756
|
+
if (!context) return;
|
|
757
|
+
|
|
758
|
+
const { cell, table } = context;
|
|
759
|
+
const row = cell.parentElement as HTMLTableRowElement | null;
|
|
760
|
+
if (!row) return;
|
|
761
|
+
|
|
762
|
+
const colCount = row.cells.length;
|
|
763
|
+
const isInHeader = row.parentElement?.tagName === "THEAD";
|
|
764
|
+
|
|
765
|
+
// Create new row with TD cells
|
|
766
|
+
const newRow = createRow(colCount, false);
|
|
767
|
+
|
|
768
|
+
// If inserting below header row, insert into tbody
|
|
769
|
+
if (isInHeader) {
|
|
770
|
+
// Ensure tbody exists
|
|
771
|
+
let tbody = table.tBodies[0];
|
|
772
|
+
if (!tbody) {
|
|
773
|
+
tbody = document.createElement("tbody");
|
|
774
|
+
table.appendChild(tbody);
|
|
775
|
+
}
|
|
776
|
+
// Insert at beginning of tbody
|
|
777
|
+
tbody.insertBefore(newRow, tbody.firstChild);
|
|
778
|
+
} else {
|
|
779
|
+
// Insert after current row
|
|
780
|
+
row.parentNode?.insertBefore(newRow, row.nextSibling);
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
// Focus the first cell of the new row
|
|
784
|
+
const firstNewCell = newRow.cells[0];
|
|
785
|
+
if (firstNewCell) {
|
|
786
|
+
focusCell(firstNewCell);
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
notifyContentChange();
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
/**
|
|
793
|
+
* Delete the current row
|
|
794
|
+
*/
|
|
795
|
+
function deleteCurrentRow(): void {
|
|
796
|
+
const context = getCurrentCellAndTable();
|
|
797
|
+
if (!context) return;
|
|
798
|
+
|
|
799
|
+
const { cell, table } = context;
|
|
800
|
+
const row = cell.parentElement as HTMLTableRowElement | null;
|
|
801
|
+
if (!row) return;
|
|
802
|
+
|
|
803
|
+
const rows = getAllTableRows(table);
|
|
804
|
+
|
|
805
|
+
// If this is the last row, delete the entire table
|
|
806
|
+
if (rows.length <= 1) {
|
|
807
|
+
deleteTable();
|
|
808
|
+
return;
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
const { row: rowIndex, col } = getCellCoordinates(cell);
|
|
812
|
+
|
|
813
|
+
// Remove the row
|
|
814
|
+
row.remove();
|
|
815
|
+
|
|
816
|
+
// Focus a cell in an adjacent row
|
|
817
|
+
const newRows = getAllTableRows(table);
|
|
818
|
+
const targetRowIndex = Math.min(rowIndex, newRows.length - 1);
|
|
819
|
+
const targetCell = getCellAt(table, targetRowIndex, col);
|
|
820
|
+
|
|
821
|
+
if (targetCell) {
|
|
822
|
+
focusCell(targetCell);
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
notifyContentChange();
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
/**
|
|
829
|
+
* Insert a new column to the left
|
|
830
|
+
*/
|
|
831
|
+
function insertColumnLeft(): void {
|
|
832
|
+
const context = getCurrentCellAndTable();
|
|
833
|
+
if (!context) return;
|
|
834
|
+
|
|
835
|
+
const { cell, table } = context;
|
|
836
|
+
const { col } = getCellCoordinates(cell);
|
|
837
|
+
const rows = getAllTableRows(table);
|
|
838
|
+
|
|
839
|
+
// Insert a cell in each row at the specified column
|
|
840
|
+
for (const row of rows) {
|
|
841
|
+
const isHeader = row.parentElement?.tagName === "THEAD";
|
|
842
|
+
const newCell = createCell(isHeader);
|
|
843
|
+
const referenceCell = row.cells[col];
|
|
844
|
+
if (referenceCell) {
|
|
845
|
+
row.insertBefore(newCell, referenceCell);
|
|
846
|
+
} else {
|
|
847
|
+
row.appendChild(newCell);
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
// Focus the new cell in the current row
|
|
852
|
+
const currentRow = cell.parentElement as HTMLTableRowElement;
|
|
853
|
+
const newCell = currentRow?.cells[col];
|
|
854
|
+
if (newCell) {
|
|
855
|
+
focusCell(newCell);
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
notifyContentChange();
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
/**
|
|
862
|
+
* Insert a new column to the right
|
|
863
|
+
*/
|
|
864
|
+
function insertColumnRight(): void {
|
|
865
|
+
const context = getCurrentCellAndTable();
|
|
866
|
+
if (!context) return;
|
|
867
|
+
|
|
868
|
+
const { cell, table } = context;
|
|
869
|
+
const { col } = getCellCoordinates(cell);
|
|
870
|
+
const rows = getAllTableRows(table);
|
|
871
|
+
|
|
872
|
+
// Insert a cell in each row after the specified column
|
|
873
|
+
for (const row of rows) {
|
|
874
|
+
const isHeader = row.parentElement?.tagName === "THEAD";
|
|
875
|
+
const newCell = createCell(isHeader);
|
|
876
|
+
const referenceCell = row.cells[col + 1];
|
|
877
|
+
if (referenceCell) {
|
|
878
|
+
row.insertBefore(newCell, referenceCell);
|
|
879
|
+
} else {
|
|
880
|
+
row.appendChild(newCell);
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
// Focus the new cell in the current row
|
|
885
|
+
const currentRow = cell.parentElement as HTMLTableRowElement;
|
|
886
|
+
const newCell = currentRow?.cells[col + 1];
|
|
887
|
+
if (newCell) {
|
|
888
|
+
focusCell(newCell);
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
notifyContentChange();
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
/**
|
|
895
|
+
* Delete the current column
|
|
896
|
+
*/
|
|
897
|
+
function deleteCurrentColumn(): void {
|
|
898
|
+
const context = getCurrentCellAndTable();
|
|
899
|
+
if (!context) return;
|
|
900
|
+
|
|
901
|
+
const { cell, table } = context;
|
|
902
|
+
const { row: rowIndex, col } = getCellCoordinates(cell);
|
|
903
|
+
const colCount = getColumnCount(table);
|
|
904
|
+
const rows = getAllTableRows(table);
|
|
905
|
+
|
|
906
|
+
// If this is the last column, delete the entire table
|
|
907
|
+
if (colCount <= 1) {
|
|
908
|
+
deleteTable();
|
|
909
|
+
return;
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
// Remove the cell from each row
|
|
913
|
+
for (const row of rows) {
|
|
914
|
+
const cellToRemove = row.cells[col];
|
|
915
|
+
if (cellToRemove) {
|
|
916
|
+
cellToRemove.remove();
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
// Focus a cell in an adjacent column
|
|
921
|
+
const targetColIndex = Math.min(col, colCount - 2);
|
|
922
|
+
const targetCell = getCellAt(table, rowIndex, targetColIndex);
|
|
923
|
+
|
|
924
|
+
if (targetCell) {
|
|
925
|
+
focusCell(targetCell);
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
notifyContentChange();
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
/**
|
|
932
|
+
* Delete the entire table
|
|
933
|
+
*/
|
|
934
|
+
function deleteTable(): void {
|
|
935
|
+
if (!contentRef.value) return;
|
|
936
|
+
|
|
937
|
+
const table = getCurrentTable();
|
|
938
|
+
if (!table) return;
|
|
939
|
+
|
|
940
|
+
// Get the next sibling to focus after deletion
|
|
941
|
+
const nextSibling = table.nextElementSibling;
|
|
942
|
+
const prevSibling = table.previousElementSibling;
|
|
943
|
+
|
|
944
|
+
// Remove the table
|
|
945
|
+
table.remove();
|
|
946
|
+
|
|
947
|
+
// Try to focus next/previous element or create a paragraph
|
|
948
|
+
if (nextSibling && (nextSibling as HTMLElement).focus) {
|
|
949
|
+
const focusable = nextSibling.querySelector("[contenteditable], input, textarea") as HTMLElement
|
|
950
|
+
|| nextSibling as HTMLElement;
|
|
951
|
+
if (focusable.focus) {
|
|
952
|
+
focusable.focus();
|
|
953
|
+
}
|
|
954
|
+
} else if (prevSibling && (prevSibling as HTMLElement).focus) {
|
|
955
|
+
const focusable = prevSibling.querySelector("[contenteditable], input, textarea") as HTMLElement
|
|
956
|
+
|| prevSibling as HTMLElement;
|
|
957
|
+
if (focusable.focus) {
|
|
958
|
+
focusable.focus();
|
|
959
|
+
}
|
|
960
|
+
} else {
|
|
961
|
+
// Create a paragraph if the content area is now empty
|
|
962
|
+
if (contentRef.value.children.length === 0) {
|
|
963
|
+
const p = document.createElement("p");
|
|
964
|
+
p.appendChild(document.createElement("br"));
|
|
965
|
+
contentRef.value.appendChild(p);
|
|
966
|
+
focusCell(p as unknown as HTMLTableCellElement);
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
notifyContentChange();
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
/**
|
|
974
|
+
* Set column alignment to left
|
|
975
|
+
*/
|
|
976
|
+
function setColumnAlignmentLeft(): void {
|
|
977
|
+
const context = getCurrentCellAndTable();
|
|
978
|
+
if (!context) return;
|
|
979
|
+
|
|
980
|
+
const { cell, table } = context;
|
|
981
|
+
const { col } = getCellCoordinates(cell);
|
|
982
|
+
setColumnAlignment(table, col, "left");
|
|
983
|
+
notifyContentChange();
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
/**
|
|
987
|
+
* Set column alignment to center
|
|
988
|
+
*/
|
|
989
|
+
function setColumnAlignmentCenter(): void {
|
|
990
|
+
const context = getCurrentCellAndTable();
|
|
991
|
+
if (!context) return;
|
|
992
|
+
|
|
993
|
+
const { cell, table } = context;
|
|
994
|
+
const { col } = getCellCoordinates(cell);
|
|
995
|
+
setColumnAlignment(table, col, "center");
|
|
996
|
+
notifyContentChange();
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
/**
|
|
1000
|
+
* Set column alignment to right
|
|
1001
|
+
*/
|
|
1002
|
+
function setColumnAlignmentRight(): void {
|
|
1003
|
+
const context = getCurrentCellAndTable();
|
|
1004
|
+
if (!context) return;
|
|
1005
|
+
|
|
1006
|
+
const { cell, table } = context;
|
|
1007
|
+
const { col } = getCellCoordinates(cell);
|
|
1008
|
+
setColumnAlignment(table, col, "right");
|
|
1009
|
+
notifyContentChange();
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
/**
|
|
1013
|
+
* Handle Tab key in table
|
|
1014
|
+
* - Tab: navigate to next cell, create new row if at end
|
|
1015
|
+
* - Shift+Tab: navigate to previous cell, exit table if at start
|
|
1016
|
+
* @returns true if handled, false to let browser handle it
|
|
1017
|
+
*/
|
|
1018
|
+
function handleTableTab(shift: boolean): boolean {
|
|
1019
|
+
if (!isInTableCell()) return false;
|
|
1020
|
+
|
|
1021
|
+
if (shift) {
|
|
1022
|
+
// Shift+Tab: go to previous cell
|
|
1023
|
+
const moved = navigateToPreviousCell();
|
|
1024
|
+
if (!moved) {
|
|
1025
|
+
// At start of table - exit table (return false to let browser handle)
|
|
1026
|
+
return false;
|
|
1027
|
+
}
|
|
1028
|
+
return true;
|
|
1029
|
+
} else {
|
|
1030
|
+
// Tab: go to next cell
|
|
1031
|
+
const moved = navigateToNextCell();
|
|
1032
|
+
if (!moved) {
|
|
1033
|
+
// At end of table - create new row
|
|
1034
|
+
insertRowBelow();
|
|
1035
|
+
const table = getCurrentTable();
|
|
1036
|
+
if (table) {
|
|
1037
|
+
const rows = getAllTableRows(table);
|
|
1038
|
+
const lastRow = rows[rows.length - 1];
|
|
1039
|
+
if (lastRow && lastRow.cells[0]) {
|
|
1040
|
+
focusCell(lastRow.cells[0]);
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
1044
|
+
return true;
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
/**
|
|
1049
|
+
* Handle Enter key in table
|
|
1050
|
+
* - Moves to cell directly below
|
|
1051
|
+
* - Creates new row if at bottom
|
|
1052
|
+
* @returns true if handled, false to let browser handle it
|
|
1053
|
+
*/
|
|
1054
|
+
function handleTableEnter(): boolean {
|
|
1055
|
+
if (!isInTableCell()) return false;
|
|
1056
|
+
|
|
1057
|
+
const moved = navigateToCellBelow();
|
|
1058
|
+
if (!moved) {
|
|
1059
|
+
// At bottom of table - create new row
|
|
1060
|
+
insertRowBelow();
|
|
1061
|
+
// Navigate to the new row
|
|
1062
|
+
navigateToCellBelow();
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
return true;
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
return {
|
|
1069
|
+
// Creation
|
|
1070
|
+
insertTable,
|
|
1071
|
+
createTable,
|
|
1072
|
+
|
|
1073
|
+
// Detection
|
|
1074
|
+
isInTable,
|
|
1075
|
+
isInTableCell,
|
|
1076
|
+
getCurrentTable,
|
|
1077
|
+
getCurrentCell,
|
|
1078
|
+
|
|
1079
|
+
// Navigation
|
|
1080
|
+
navigateToNextCell,
|
|
1081
|
+
navigateToPreviousCell,
|
|
1082
|
+
navigateToCellBelow,
|
|
1083
|
+
navigateToCellAbove,
|
|
1084
|
+
|
|
1085
|
+
// Row operations
|
|
1086
|
+
insertRowAbove,
|
|
1087
|
+
insertRowBelow,
|
|
1088
|
+
deleteCurrentRow,
|
|
1089
|
+
|
|
1090
|
+
// Column operations
|
|
1091
|
+
insertColumnLeft,
|
|
1092
|
+
insertColumnRight,
|
|
1093
|
+
deleteCurrentColumn,
|
|
1094
|
+
|
|
1095
|
+
// Table operations
|
|
1096
|
+
deleteTable,
|
|
1097
|
+
|
|
1098
|
+
// Alignment
|
|
1099
|
+
setColumnAlignmentLeft,
|
|
1100
|
+
setColumnAlignmentCenter,
|
|
1101
|
+
setColumnAlignmentRight,
|
|
1102
|
+
|
|
1103
|
+
// Key handlers
|
|
1104
|
+
handleTableTab,
|
|
1105
|
+
handleTableEnter
|
|
1106
|
+
};
|
|
1107
|
+
}
|