@bagelink/vue 1.16.0 → 1.17.6

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.
Files changed (128) hide show
  1. package/dist/assetTransport.d.ts +28 -0
  2. package/dist/assetTransport.d.ts.map +1 -0
  3. package/dist/assetTransport.test.d.ts +2 -0
  4. package/dist/assetTransport.test.d.ts.map +1 -0
  5. package/dist/assets.d.ts +65 -0
  6. package/dist/assets.d.ts.map +1 -0
  7. package/dist/assets.test.d.ts +2 -0
  8. package/dist/assets.test.d.ts.map +1 -0
  9. package/dist/components/AddressSearch.vue.d.ts +40 -36
  10. package/dist/components/AddressSearch.vue.d.ts.map +1 -1
  11. package/dist/components/ContextMenu.vue.d.ts +518 -0
  12. package/dist/components/ContextMenu.vue.d.ts.map +1 -0
  13. package/dist/components/Dropdown.types.d.ts +25 -0
  14. package/dist/components/Dropdown.types.d.ts.map +1 -0
  15. package/dist/components/Dropdown.vue.d.ts +20 -15
  16. package/dist/components/Dropdown.vue.d.ts.map +1 -1
  17. package/dist/components/FilterQuery.vue.d.ts +1 -1
  18. package/dist/components/FilterQuery.vue.d.ts.map +1 -1
  19. package/dist/components/Icon/Icon.vue.d.ts +0 -1
  20. package/dist/components/Icon/Icon.vue.d.ts.map +1 -1
  21. package/dist/components/Icon/constants.d.ts +0 -1
  22. package/dist/components/Icon/constants.d.ts.map +1 -1
  23. package/dist/components/Icon/types.d.ts +2 -3
  24. package/dist/components/Icon/types.d.ts.map +1 -1
  25. package/dist/components/Image.vue.d.ts +7 -1
  26. package/dist/components/Image.vue.d.ts.map +1 -1
  27. package/dist/components/Swiper.vue.d.ts +1 -1
  28. package/dist/components/draggable/useDraggable.d.ts.map +1 -1
  29. package/dist/components/form/inputs/IconPickerInput.vue.d.ts +48 -37
  30. package/dist/components/form/inputs/IconPickerInput.vue.d.ts.map +1 -1
  31. package/dist/components/form/inputs/RichText/components/EditorToolbar.vue.d.ts.map +1 -1
  32. package/dist/components/form/inputs/RichText/composables/useCommands.d.ts.map +1 -1
  33. package/dist/components/form/inputs/RichText/composables/useEditor.d.ts +3 -0
  34. package/dist/components/form/inputs/RichText/composables/useEditor.d.ts.map +1 -1
  35. package/dist/components/form/inputs/RichText/composables/useEditorKeyboard.d.ts +1 -1
  36. package/dist/components/form/inputs/RichText/composables/useEditorKeyboard.d.ts.map +1 -1
  37. package/dist/components/form/inputs/RichText/index.vue.d.ts.map +1 -1
  38. package/dist/components/form/inputs/RichText/utils/commands.d.ts.map +1 -1
  39. package/dist/components/form/inputs/RichText/utils/selection.d.ts +2 -0
  40. package/dist/components/form/inputs/RichText/utils/selection.d.ts.map +1 -1
  41. package/dist/components/form/inputs/RichText/utils/table.d.ts +34 -8
  42. package/dist/components/form/inputs/RichText/utils/table.d.ts.map +1 -1
  43. package/dist/components/form/inputs/SelectInput.vue.d.ts +71 -63
  44. package/dist/components/form/inputs/SelectInput.vue.d.ts.map +1 -1
  45. package/dist/components/form/inputs/TelInput.vue.d.ts +2 -2
  46. package/dist/components/form/inputs/Upload/AssetUploadInput.vue.d.ts +11 -0
  47. package/dist/components/form/inputs/Upload/AssetUploadInput.vue.d.ts.map +1 -0
  48. package/dist/components/form/inputs/Upload/UploadInput.vue.d.ts +2 -2
  49. package/dist/components/form/inputs/Upload/UploadInput.vue.d.ts.map +1 -1
  50. package/dist/components/index.d.ts +3 -1
  51. package/dist/components/index.d.ts.map +1 -1
  52. package/dist/components/lightbox/AssetPreview.vue.d.ts +9 -0
  53. package/dist/components/lightbox/AssetPreview.vue.d.ts.map +1 -0
  54. package/dist/components/lightbox/Lightbox.vue.d.ts.map +1 -1
  55. package/dist/components/lightbox/LightboxImage.vue.d.ts +2 -1
  56. package/dist/components/lightbox/LightboxImage.vue.d.ts.map +1 -1
  57. package/dist/components/lightbox/index.d.ts.map +1 -1
  58. package/dist/components/lightbox/lightbox.types.d.ts +7 -0
  59. package/dist/components/lightbox/lightbox.types.d.ts.map +1 -1
  60. package/dist/composables/index.d.ts +1 -1
  61. package/dist/composables/index.d.ts.map +1 -1
  62. package/dist/composables/useBoundedRectDrag.d.ts.map +1 -1
  63. package/dist/composables/useImageSrc.d.ts +8 -4
  64. package/dist/composables/useImageSrc.d.ts.map +1 -1
  65. package/dist/composables/usePointerSession.d.ts +14 -3
  66. package/dist/composables/usePointerSession.d.ts.map +1 -1
  67. package/dist/core.css +3 -3
  68. package/dist/directives/vResize.d.ts.map +1 -1
  69. package/dist/form-flow/FormFlowPlayer.vue.d.ts +3 -3
  70. package/dist/form-flow/FormFlowPlayer.vue.d.ts.map +1 -1
  71. package/dist/form-flow/form-flow.d.ts +13 -6
  72. package/dist/form-flow/form-flow.d.ts.map +1 -1
  73. package/dist/form-flow/schema-fields.d.ts +6 -0
  74. package/dist/form-flow/schema-fields.d.ts.map +1 -1
  75. package/dist/index.cjs +240 -153
  76. package/dist/index.css +1 -1
  77. package/dist/index.d.ts +3 -0
  78. package/dist/index.d.ts.map +1 -1
  79. package/dist/index.mjs +24167 -23417
  80. package/dist/style.css +3 -3
  81. package/dist/types/BagelForm.d.ts +5 -1
  82. package/dist/types/BagelForm.d.ts.map +1 -1
  83. package/package.json +1 -1
  84. package/src/assetTransport.test.ts +75 -0
  85. package/src/assetTransport.ts +98 -0
  86. package/src/assets.test.ts +82 -0
  87. package/src/assets.ts +162 -0
  88. package/src/components/ContextMenu.vue +113 -0
  89. package/src/components/Dropdown.types.ts +27 -0
  90. package/src/components/Dropdown.vue +129 -64
  91. package/src/components/FilterQuery.vue +1 -1
  92. package/src/components/Icon/Icon.vue +8 -30
  93. package/src/components/Icon/constants.ts +0 -166
  94. package/src/components/Icon/types.ts +2 -3
  95. package/src/components/Image.vue +19 -3
  96. package/src/components/draggable/useDraggable.ts +67 -100
  97. package/src/components/form/inputs/IconPickerInput.vue +14 -9
  98. package/src/components/form/inputs/RichText/components/EditorToolbar.vue +43 -111
  99. package/src/components/form/inputs/RichText/composables/useCommands.ts +32 -54
  100. package/src/components/form/inputs/RichText/composables/useEditor.ts +45 -171
  101. package/src/components/form/inputs/RichText/composables/useEditorKeyboard.ts +90 -14
  102. package/src/components/form/inputs/RichText/index.vue +1195 -1210
  103. package/src/components/form/inputs/RichText/utils/commands.ts +689 -1028
  104. package/src/components/form/inputs/RichText/utils/selection.ts +66 -41
  105. package/src/components/form/inputs/RichText/utils/table.ts +798 -136
  106. package/src/components/form/inputs/SelectInput.vue +1 -1
  107. package/src/components/form/inputs/Upload/AssetUploadInput.vue +67 -0
  108. package/src/components/form/inputs/Upload/UploadInput.vue +5 -1
  109. package/src/components/index.ts +3 -1
  110. package/src/components/lightbox/AssetPreview.vue +16 -0
  111. package/src/components/lightbox/Lightbox.vue +8 -3
  112. package/src/components/lightbox/LightboxImage.vue +4 -3
  113. package/src/components/lightbox/index.ts +29 -4
  114. package/src/components/lightbox/lightbox.types.ts +5 -1
  115. package/src/composables/index.ts +1 -1
  116. package/src/composables/useBoundedRectDrag.ts +9 -12
  117. package/src/composables/useImageSrc.ts +47 -37
  118. package/src/composables/usePointerSession.ts +57 -12
  119. package/src/directives/vResize.ts +6 -7
  120. package/src/form-flow/form-flow.ts +31 -7
  121. package/src/form-flow/schema-fields.ts +28 -1
  122. package/src/index.ts +3 -0
  123. package/src/styles/core.css +1 -18
  124. package/src/types/BagelForm.ts +5 -1
  125. package/dist/components/form/inputs/RichText/utils/formatting.d.ts +0 -10
  126. package/dist/components/form/inputs/RichText/utils/formatting.d.ts.map +0 -1
  127. package/src/components/form/inputs/RichText/components/gridBox.vue +0 -58
  128. package/src/components/form/inputs/RichText/utils/formatting.ts +0 -331
@@ -1,234 +1,896 @@
1
1
  import type { EditorState } from '../richTextTypes'
2
2
 
3
+ export interface GridSlot {
4
+ cell: HTMLTableCellElement
5
+ originRow: number
6
+ originCol: number
7
+ isOrigin: boolean
8
+ }
9
+
10
+ export interface TableGridMap {
11
+ table: HTMLTableElement
12
+ rows: HTMLTableRowElement[]
13
+ grid: (GridSlot | undefined)[][]
14
+ rowCount: number
15
+ colCount: number
16
+ }
17
+
18
+ export function resolveCellFromNode(node: Node | null | undefined): HTMLTableCellElement | null {
19
+ if (!node) { return null }
20
+ const element = node.nodeType === Node.ELEMENT_NODE
21
+ ? node as Element
22
+ : node.parentElement
23
+ return (element?.closest('td, th') as HTMLTableCellElement | null) ?? null
24
+ }
25
+
26
+ export function resolveActiveCell(range: Range | null | undefined, fallbackCell?: HTMLTableCellElement | null): HTMLTableCellElement | null {
27
+ if (fallbackCell && fallbackCell.isConnected) {
28
+ return fallbackCell
29
+ }
30
+ if (!range) { return null }
31
+ return resolveCellFromNode(range.startContainer)
32
+ || resolveCellFromNode(range.commonAncestorContainer)
33
+ }
34
+
35
+ export function buildTableGrid(table: HTMLTableElement): TableGridMap {
36
+ const rows = Array.from(table.rows)
37
+ const grid: (GridSlot | undefined)[][] = rows.map(() => [])
38
+ let maxCols = 0
39
+
40
+ for (let r = 0; r < rows.length; r++) {
41
+ const row = rows[r]
42
+ let c = 0
43
+ for (let i = 0; i < row.cells.length; i++) {
44
+ const cell = row.cells[i]
45
+ while (grid[r][c] !== undefined) {
46
+ c++
47
+ }
48
+ const rowspan = Math.max(1, Number.parseInt(cell.getAttribute('rowspan') || '1', 10) || 1)
49
+ const colspan = Math.max(1, Number.parseInt(cell.getAttribute('colspan') || '1', 10) || 1)
50
+
51
+ for (let dr = 0; dr < rowspan; dr++) {
52
+ const targetRow = r + dr
53
+ if (!grid[targetRow]) {
54
+ grid[targetRow] = []
55
+ }
56
+ for (let dc = 0; dc < colspan; dc++) {
57
+ const targetCol = c + dc
58
+ grid[targetRow][targetCol] = {
59
+ cell,
60
+ originRow: r,
61
+ originCol: c,
62
+ isOrigin: dr === 0 && dc === 0
63
+ }
64
+ if (targetCol + 1 > maxCols) {
65
+ maxCols = targetCol + 1
66
+ }
67
+ }
68
+ }
69
+ c += colspan
70
+ }
71
+ }
72
+
73
+ return {
74
+ table,
75
+ rows,
76
+ grid,
77
+ rowCount: rows.length,
78
+ colCount: maxCols
79
+ }
80
+ }
81
+
82
+ export function findCellCoords(map: TableGridMap, targetCell: HTMLTableCellElement): {
83
+ row: number
84
+ col: number
85
+ rowspan: number
86
+ colspan: number
87
+ } | null {
88
+ for (let r = 0; r < map.rowCount; r++) {
89
+ for (let c = 0; c < map.colCount; c++) {
90
+ const slot = map.grid[r]?.[c]
91
+ if (slot && slot.cell === targetCell && slot.isOrigin) {
92
+ const rowspan = Math.max(1, Number.parseInt(targetCell.getAttribute('rowspan') || '1', 10) || 1)
93
+ const colspan = Math.max(1, Number.parseInt(targetCell.getAttribute('colspan') || '1', 10) || 1)
94
+ return { row: r, col: c, rowspan, colspan }
95
+ }
96
+ }
97
+ }
98
+ return null
99
+ }
100
+
101
+ export function syncTableColgroup(table: HTMLTableElement, targetCols?: number) {
102
+ const doc = table.ownerDocument
103
+ const map = buildTableGrid(table)
104
+ const colCount = targetCols ?? map.colCount
105
+ if (colCount <= 0) { return }
106
+
107
+ const colgroup = Array.from(table.children).find(el => el.tagName === 'COLGROUP') as HTMLElement | undefined
108
+ const equalWidth = Number((100 / colCount).toFixed(2))
109
+ const widths = Array.from({ length: colCount }, (_, idx) =>
110
+ idx === colCount - 1
111
+ ? Number((100 - equalWidth * (colCount - 1)).toFixed(2))
112
+ : equalWidth
113
+ )
114
+
115
+ if (colgroup) {
116
+ colgroup.replaceChildren(...widths.map((w) => {
117
+ const col = doc.createElement('col')
118
+ col.style.width = `${w}%`
119
+ return col
120
+ }))
121
+ }
122
+
123
+ const isFixed = table.style.tableLayout === 'fixed'
124
+ if (isFixed) {
125
+ for (let r = 0; r < map.rowCount; r++) {
126
+ for (let c = 0; c < map.colCount; c++) {
127
+ const slot = map.grid[r]?.[c]
128
+ if (slot?.isOrigin) {
129
+ const colspan = Math.max(1, Number.parseInt(slot.cell.getAttribute('colspan') || '1', 10) || 1)
130
+ const spanWidth = widths.slice(c, c + colspan).reduce((acc, v) => acc + v, 0)
131
+ slot.cell.style.width = `${Number(spanWidth.toFixed(2))}%`
132
+ }
133
+ }
134
+ }
135
+ }
136
+ }
137
+
138
+ function copyCellBaseStyle(source: HTMLElement | null | undefined, target: HTMLElement) {
139
+ if (source) {
140
+ target.style.padding = source.style.padding || '8px'
141
+ target.style.border = source.style.border || '1px solid var(--bgl-border-color, #ddd)'
142
+ if (source.style.backgroundColor) {
143
+ target.style.backgroundColor = source.style.backgroundColor
144
+ }
145
+ if (source.style.color) {
146
+ target.style.color = source.style.color
147
+ }
148
+ if (source.style.textAlign) {
149
+ target.style.textAlign = source.style.textAlign
150
+ }
151
+ } else {
152
+ target.style.padding = '8px'
153
+ target.style.border = '1px solid var(--bgl-border-color, #ddd)'
154
+ }
155
+ }
156
+
157
+ function recordAndSyncTableMutation(doc: Document) {
158
+ const state = (doc as any).editorState as EditorState | undefined
159
+ if (!state) { return }
160
+ const newContent = doc.body.innerHTML
161
+ if (newContent !== state.content) {
162
+ state.undoStack.push(state.content)
163
+ state.redoStack = []
164
+ if (state.undoStack.length > 50) {
165
+ state.undoStack.shift()
166
+ }
167
+ state.content = newContent
168
+ }
169
+ }
170
+
171
+ export function removeTableElement(table: HTMLTableElement) {
172
+ const doc = table.ownerDocument
173
+ const wrapper = table.closest('.table-wrapper')
174
+ if (wrapper) {
175
+ wrapper.remove()
176
+ } else {
177
+ table.remove()
178
+ }
179
+ if (!doc.body.children.length) {
180
+ const p = doc.createElement('p')
181
+ p.dir = doc.body.dir || 'ltr'
182
+ p.innerHTML = '<br>'
183
+ doc.body.appendChild(p)
184
+ }
185
+ recordAndSyncTableMutation(doc)
186
+ }
187
+
188
+ export function focusTableCell(cell: HTMLTableCellElement, collapseToStart = true) {
189
+ const doc = cell.ownerDocument
190
+ const selection = doc.getSelection()
191
+ if (!selection) { return }
192
+
193
+ const range = doc.createRange()
194
+ range.selectNodeContents(cell)
195
+ range.collapse(collapseToStart)
196
+ selection.removeAllRanges()
197
+ selection.addRange(range)
198
+
199
+ const state = (doc as any).editorState as EditorState | undefined
200
+ if (state) {
201
+ state.selection = selection
202
+ state.range = range.cloneRange()
203
+ state.rangeCount = selection.rangeCount
204
+ }
205
+ }
206
+
3
207
  export function insertTable(rows: number, cols: number, state: EditorState) {
4
208
  if (!state.doc) { return }
5
- const table = state.doc.createElement('table')
209
+ const doc = state.doc
210
+ const table = doc.createElement('table')
6
211
  table.style.width = '100%'
7
212
  table.style.borderCollapse = 'collapse'
8
213
  table.style.marginBottom = '1rem'
214
+ table.style.tableLayout = 'fixed'
215
+ table.dir = doc.body.dir || 'ltr'
216
+
217
+ const colgroup = doc.createElement('colgroup')
218
+ const colWidth = Number((100 / cols).toFixed(2))
219
+ for (let j = 0; j < cols; j++) {
220
+ const col = doc.createElement('col')
221
+ const w = j === cols - 1 ? Number((100 - colWidth * (cols - 1)).toFixed(2)) : colWidth
222
+ col.style.width = `${w}%`
223
+ colgroup.appendChild(col)
224
+ }
225
+ table.appendChild(colgroup)
9
226
 
10
227
  // Add a header row
11
- const thead = state.doc.createElement('thead')
228
+ const thead = doc.createElement('thead')
12
229
  const headerRow = thead.insertRow()
13
230
  for (let j = 0; j < cols; j++) {
14
- const th = state.doc.createElement('th')
15
- th.innerHTML = `Header ${j + 1}`
231
+ const th = doc.createElement('th')
232
+ th.textContent = `Header ${j + 1}`
16
233
  th.style.padding = '8px'
17
234
  th.style.border = '1px solid var(--bgl-border-color, #ddd)'
18
235
  th.style.backgroundColor = 'var(--bgl-gray-light, #f4f4f4)'
236
+ th.style.width = `${colWidth}%`
19
237
  headerRow.appendChild(th)
20
238
  }
21
239
  table.appendChild(thead)
22
240
 
23
241
  // Add body rows
24
- const tbody = state.doc.createElement('tbody')
242
+ const tbody = doc.createElement('tbody')
25
243
  for (let i = 0; i < rows; i++) {
26
244
  const row = tbody.insertRow()
27
245
  for (let j = 0; j < cols; j++) {
28
246
  const cell = row.insertCell()
29
- cell.innerHTML = '&nbsp;'
247
+ cell.innerHTML = '<br>'
30
248
  cell.style.padding = '8px'
31
249
  cell.style.border = '1px solid var(--bgl-border-color, #ddd)'
250
+ cell.style.width = `${colWidth}%`
32
251
  }
33
252
  }
34
253
  table.appendChild(tbody)
35
254
 
36
- // Insert the table at the current selection
37
- const { range } = state
38
- if (range) {
39
- range.insertNode(table)
40
-
41
- // Move cursor inside first cell for convenience
42
- if (state.doc.getSelection()) {
43
- const firstCell = table.querySelector('td')
44
- if (firstCell) {
45
- range.selectNodeContents(firstCell)
46
- range.collapse(true)
47
- const selection = state.doc.getSelection()
48
- if (selection) {
49
- selection.removeAllRanges()
50
- selection.addRange(range)
51
- }
52
- }
255
+ const selection = doc.getSelection()
256
+ const activeRange = selection?.rangeCount ? selection.getRangeAt(0) : state.range
257
+ if (activeRange) {
258
+ const hostBlock = (activeRange.startContainer.nodeType === Node.ELEMENT_NODE
259
+ ? activeRange.startContainer as Element
260
+ : activeRange.startContainer.parentElement)?.closest('p,h1,h2,h3,h4,h5,h6')
261
+ if (hostBlock && hostBlock.parentElement === doc.body && !(hostBlock.textContent || '').trim()) {
262
+ hostBlock.replaceWith(table)
263
+ } else if (hostBlock && hostBlock.parentElement === doc.body) {
264
+ hostBlock.insertAdjacentElement('afterend', table)
265
+ } else {
266
+ activeRange.insertNode(table)
53
267
  }
54
268
  } else {
55
- state.doc.body.appendChild(table)
269
+ doc.body.appendChild(table)
56
270
  }
57
- state.content = state.doc.body.innerHTML
58
- }
59
271
 
60
- export function mergeCells(range: Range, doc: Document) {
61
- const cells = Array.from(range.cloneContents().querySelectorAll('td'))
62
- if (cells.length < 2) { return }
63
-
64
- const firstCell = range.startContainer.parentElement?.closest('td')
65
- if (!firstCell) { return }
66
-
67
- firstCell.colSpan = cells.length
68
- firstCell.innerHTML = cells.map(cell => cell.innerHTML).join(' ')
272
+ // Ensure a trailing paragraph exists after the table so the user can always click/arrow below it
273
+ if (!table.nextElementSibling) {
274
+ const trailingP = doc.createElement('p')
275
+ trailingP.dir = doc.body.dir || 'ltr'
276
+ trailingP.innerHTML = '<br>'
277
+ table.insertAdjacentElement('afterend', trailingP)
278
+ }
69
279
 
70
- cells.slice(1).forEach((cell) => {
71
- const actualCell = doc.getElementById(cell.id)
72
- actualCell?.remove()
73
- })
280
+ const addButtons = (doc as any).__addEditButtonsToTables as (() => void) | undefined
281
+ addButtons?.()
74
282
 
75
- // Update state content
76
- const state = (doc as any).editorState as EditorState
77
- if (state) {
78
- state.content = doc.body.innerHTML
283
+ const firstCell = table.querySelector('tbody td, td, th') as HTMLTableCellElement | null
284
+ if (firstCell) {
285
+ focusTableCell(firstCell, true)
79
286
  }
287
+
288
+ recordAndSyncTableMutation(doc)
80
289
  }
81
290
 
82
- export function splitCell(range: Range, doc: Document) {
83
- const cell = range.startContainer.parentElement?.closest('td')
84
- if (!cell || !cell.colSpan || cell.colSpan === 1) { return }
291
+ export function addRow(
292
+ position: 'before' | 'after',
293
+ range: Range | null,
294
+ doc: Document,
295
+ targetCellOverride?: HTMLTableCellElement | null
296
+ ): HTMLTableRowElement | null {
297
+ const cell = resolveActiveCell(range, targetCellOverride)
298
+ if (!cell) { return null }
299
+
300
+ const row = cell.closest('tr') as HTMLTableRowElement | null
301
+ const table = cell.closest('table') as HTMLTableElement | null
302
+ if (!row || !table) { return null }
303
+
304
+ const map = buildTableGrid(table)
305
+ const coords = findCellCoords(map, cell)
306
+ if (!coords) { return null }
307
+
308
+ const isHeaderRow = row.parentElement?.tagName === 'THEAD'
309
+ const insertRowIdx = position === 'before' ? coords.row : coords.row + coords.rowspan - 1
310
+ const referenceRow = map.rows[insertRowIdx] || row
311
+
312
+ const newRow = doc.createElement('tr')
313
+ const templateCell = (table.querySelector('tbody td') || cell) as HTMLTableCellElement
314
+
315
+ for (let c = 0; c < map.colCount; c++) {
316
+ const slotAtBoundary = map.grid[insertRowIdx]?.[c]
317
+ // If a cell spans across the insertion boundary, increment its rowspan instead of adding a cell
318
+ if (slotAtBoundary) {
319
+ const spanCell = slotAtBoundary.cell
320
+ const spanRows = Math.max(1, Number.parseInt(spanCell.getAttribute('rowspan') || '1', 10) || 1)
321
+ const spanEnd = slotAtBoundary.originRow + spanRows - 1
322
+ const crossesBoundary = position === 'before'
323
+ ? (slotAtBoundary.originRow < insertRowIdx && spanEnd >= insertRowIdx)
324
+ : (slotAtBoundary.originRow <= insertRowIdx && spanEnd > insertRowIdx)
325
+
326
+ if (crossesBoundary) {
327
+ if (slotAtBoundary.originCol === c) {
328
+ spanCell.setAttribute('rowspan', String(spanRows + 1))
329
+ }
330
+ continue
331
+ }
332
+ }
85
333
 
86
- const newCells = new Array(cell.colSpan - 1).fill(0).map(() => {
87
334
  const newCell = doc.createElement('td')
88
- newCell.style.border = '1px solid var(--bgl-border-color)'
89
- newCell.style.padding = '8px'
90
- newCell.innerHTML = '&nbsp;'
91
- return newCell
92
- })
93
-
94
- cell.colSpan = 1
95
- cell.insertAdjacentElement('afterend', newCells[0])
96
- newCells.slice(1).forEach((newCell) => {
97
- newCells[0].insertAdjacentElement('afterend', newCell)
98
- })
335
+ newCell.innerHTML = '<br>'
336
+ const refSlot = map.grid[coords.row]?.[c]
337
+ copyCellBaseStyle(refSlot?.cell.tagName === 'TD' ? refSlot.cell : templateCell, newCell)
338
+ newRow.appendChild(newCell)
339
+ }
99
340
 
100
- // Update state content
101
- const state = (doc as any).editorState as EditorState
102
- if (state) {
103
- state.content = doc.body.innerHTML
341
+ // If inserting after a header row in <thead>, place the new row at the start of <tbody>
342
+ if (isHeaderRow) {
343
+ let tbody = table.querySelector('tbody')
344
+ if (!tbody) {
345
+ tbody = doc.createElement('tbody')
346
+ table.appendChild(tbody)
347
+ }
348
+ if (position === 'after') {
349
+ tbody.insertBefore(newRow, tbody.firstChild)
350
+ } else {
351
+ tbody.insertBefore(newRow, tbody.firstChild)
352
+ }
353
+ } else {
354
+ referenceRow.insertAdjacentElement(position === 'before' ? 'beforebegin' : 'afterend', newRow)
104
355
  }
356
+
357
+ syncTableColgroup(table)
358
+ recordAndSyncTableMutation(doc)
359
+ return newRow
105
360
  }
106
361
 
107
- export function addRow(position: 'before' | 'after', range: Range, doc: Document) {
108
- const cell = range.startContainer.parentElement?.closest('td')
362
+ export function deleteRow(range: Range | null, targetCellOverride?: HTMLTableCellElement | null) {
363
+ const cell = resolveActiveCell(range, targetCellOverride)
109
364
  if (!cell) { return }
110
365
 
111
- const row = cell.parentElement
112
- const table = row?.parentElement
366
+ const row = cell.closest('tr') as HTMLTableRowElement | null
367
+ const table = cell.closest('table') as HTMLTableElement | null
113
368
  if (!row || !table) { return }
114
369
 
115
- const newRow = row.cloneNode(true) as HTMLTableRowElement
116
- Array.from(newRow.cells).forEach(cell => cell.innerHTML = '&nbsp;')
370
+ const doc = table.ownerDocument
371
+ const map = buildTableGrid(table)
372
+ const coords = findCellCoords(map, cell)
373
+ if (!coords) { return }
374
+
375
+ // If deleting the last remaining row of the table, remove the table
376
+ if (map.rowCount <= 1) {
377
+ removeTableElement(table)
378
+ return
379
+ }
117
380
 
118
- row.insertAdjacentElement(position === 'before' ? 'beforebegin' : 'afterend', newRow)
381
+ const rowIndex = coords.row
382
+ const handledCells = new Set<HTMLTableCellElement>()
383
+
384
+ for (let c = 0; c < map.colCount; c++) {
385
+ const slot = map.grid[rowIndex]?.[c]
386
+ if (!slot || handledCells.has(slot.cell)) { continue }
387
+ handledCells.add(slot.cell)
388
+
389
+ const rowspan = Math.max(1, Number.parseInt(slot.cell.getAttribute('rowspan') || '1', 10) || 1)
390
+ if (rowspan > 1) {
391
+ if (slot.originRow === rowIndex) {
392
+ // Cell starts in the deleted row and spans down: move it into the next row
393
+ const nextRow = map.rows[rowIndex + 1]
394
+ if (nextRow) {
395
+ const clone = slot.cell.cloneNode(true) as HTMLTableCellElement
396
+ if (rowspan - 1 > 1) {
397
+ clone.setAttribute('rowspan', String(rowspan - 1))
398
+ } else {
399
+ clone.removeAttribute('rowspan')
400
+ }
401
+ // Find insertion position in nextRow based on grid column
402
+ let inserted = false
403
+ for (let nextCol = c + 1; nextCol < map.colCount; nextCol++) {
404
+ const nextSlot = map.grid[rowIndex + 1]?.[nextCol]
405
+ if (nextSlot && nextSlot.originRow === rowIndex + 1 && nextSlot.cell.parentElement === nextRow) {
406
+ nextRow.insertBefore(clone, nextSlot.cell)
407
+ inserted = true
408
+ break
409
+ }
410
+ }
411
+ if (!inserted) {
412
+ nextRow.appendChild(clone)
413
+ }
414
+ }
415
+ } else {
416
+ // Cell starts in an earlier row and spans through this row
417
+ if (rowspan - 1 > 1) {
418
+ slot.cell.setAttribute('rowspan', String(rowspan - 1))
419
+ } else {
420
+ slot.cell.removeAttribute('rowspan')
421
+ }
422
+ }
423
+ }
424
+ }
119
425
 
120
- // Update state content
121
- const state = (doc as any).editorState as EditorState
122
- if (state) {
123
- state.content = doc.body.innerHTML
426
+ const section = row.parentElement as HTMLTableSectionElement | null
427
+ const nextFocusRow = map.rows[rowIndex + 1] || map.rows[rowIndex - 1]
428
+ row.remove()
429
+
430
+ if (section && section.tagName === 'THEAD' && section.rows.length === 0) {
431
+ section.remove()
432
+ }
433
+
434
+ if (table.rows.length === 0) {
435
+ removeTableElement(table)
436
+ return
437
+ }
438
+
439
+ if (nextFocusRow && nextFocusRow.cells.length > 0) {
440
+ const targetFocusCell = nextFocusRow.cells[Math.min(coords.col, nextFocusRow.cells.length - 1)]
441
+ if (targetFocusCell) {
442
+ focusTableCell(targetFocusCell, true)
443
+ }
124
444
  }
445
+
446
+ syncTableColgroup(table)
447
+ recordAndSyncTableMutation(doc)
125
448
  }
126
449
 
127
- export function deleteRow(range: Range) {
128
- const cell = range.startContainer.parentElement?.closest('td')
450
+ export function deleteTable(range: Range | null, targetCellOverride?: HTMLTableCellElement | null) {
451
+ const cell = resolveActiveCell(range, targetCellOverride)
129
452
  if (!cell) { return }
130
453
 
131
- const row = cell.parentElement
132
- if (!row) { return }
133
-
134
- const doc = row.ownerDocument
135
- row.remove()
454
+ const table = cell.closest('table') as HTMLTableElement | null
455
+ if (!table) { return }
136
456
 
137
- // Update state content
138
- const state = (doc as any).editorState as EditorState
139
- if (state) {
140
- state.content = doc.body.innerHTML
141
- }
457
+ removeTableElement(table)
142
458
  }
143
459
 
144
- export function deleteTable(range: Range) {
145
- const cell = range.startContainer.parentElement?.closest('td')
460
+ export function insertColumn(
461
+ position: 'before' | 'after',
462
+ range: Range | null,
463
+ targetCellOverride?: HTMLTableCellElement | null
464
+ ) {
465
+ const cell = resolveActiveCell(range, targetCellOverride)
146
466
  if (!cell) { return }
147
467
 
148
- const table = cell.closest('table')
468
+ const table = cell.closest('table') as HTMLTableElement | null
149
469
  if (!table) { return }
150
470
 
151
471
  const doc = table.ownerDocument
152
- table.remove()
472
+ const map = buildTableGrid(table)
473
+ const coords = findCellCoords(map, cell)
474
+ if (!coords) { return }
475
+
476
+ const targetCol = position === 'before' ? coords.col : coords.col + coords.colspan - 1
477
+ const handledSpanCells = new Set<HTMLTableCellElement>()
478
+
479
+ for (let r = 0; r < map.rowCount; r++) {
480
+ const row = map.rows[r]
481
+ const slot = map.grid[r]?.[targetCol]
482
+
483
+ if (slot) {
484
+ const colspan = Math.max(1, Number.parseInt(slot.cell.getAttribute('colspan') || '1', 10) || 1)
485
+ const spanEnd = slot.originCol + colspan - 1
486
+ const crossesBoundary = position === 'before'
487
+ ? (slot.originCol < targetCol && spanEnd >= targetCol)
488
+ : (slot.originCol <= targetCol && spanEnd > targetCol)
489
+
490
+ if (crossesBoundary) {
491
+ if (!handledSpanCells.has(slot.cell)) {
492
+ handledSpanCells.add(slot.cell)
493
+ slot.cell.setAttribute('colspan', String(colspan + 1))
494
+ }
495
+ continue
496
+ }
153
497
 
154
- // Update state content
155
- const state = (doc as any).editorState as EditorState
156
- if (state) {
157
- state.content = doc.body.innerHTML
498
+ // If this row is covered by a rowspan from an earlier row at the insertion column, skip creating a cell here
499
+ if (slot.originRow < r && (
500
+ (position === 'before' && slot.originCol === targetCol)
501
+ || (position === 'after' && spanEnd === targetCol)
502
+ )) {
503
+ // Only insert if the neighbor isn't also spanning; fall through to insert a regular cell in this row
504
+ }
505
+ }
506
+
507
+ const isHeader = row.parentElement?.tagName === 'THEAD'
508
+ const newCell = doc.createElement(isHeader ? 'th' : 'td')
509
+ if (isHeader) {
510
+ newCell.textContent = `Header ${map.colCount + 1}`
511
+ } else {
512
+ newCell.appendChild(doc.createElement('br'))
513
+ }
514
+ copyCellBaseStyle(slot?.cell || row.cells[0], newCell)
515
+
516
+ // Find reference cell in this row to insert before/after
517
+ let refCell: HTMLTableCellElement | null = null
518
+ for (let c = position === 'before' ? targetCol : targetCol + 1; c < map.colCount; c++) {
519
+ const candidate = map.grid[r]?.[c]
520
+ if (candidate && candidate.originRow === r && candidate.cell.parentElement === row) {
521
+ refCell = candidate.cell
522
+ break
523
+ }
524
+ }
525
+
526
+ if (refCell) {
527
+ row.insertBefore(newCell, refCell)
528
+ } else {
529
+ row.appendChild(newCell)
530
+ }
158
531
  }
532
+
533
+ syncTableColgroup(table, map.colCount + 1)
534
+ recordAndSyncTableMutation(doc)
159
535
  }
160
536
 
161
- export function insertColumn(position: 'before' | 'after', range: Range) {
162
- const cell = range.startContainer.parentElement?.closest('td')
537
+ export function deleteColumn(range: Range | null, targetCellOverride?: HTMLTableCellElement | null) {
538
+ const cell = resolveActiveCell(range, targetCellOverride)
163
539
  if (!cell) { return }
164
540
 
165
- const table = cell.closest('table')
541
+ const table = cell.closest('table') as HTMLTableElement | null
166
542
  if (!table) { return }
167
543
 
168
- const columnIndex = cell.cellIndex
169
- const { rows } = table
544
+ const doc = table.ownerDocument
545
+ const map = buildTableGrid(table)
546
+ const coords = findCellCoords(map, cell)
547
+ if (!coords) { return }
170
548
 
171
- for (let i = 0; i < rows.length; i++) {
172
- const newCell = rows[i].insertCell(position === 'after' ? columnIndex + 1 : columnIndex)
173
- newCell.innerHTML = '&nbsp;'
174
- newCell.style.border = '1px solid var(--bgl-border-color)'
175
- newCell.style.padding = '8px'
549
+ if (map.colCount <= 1) {
550
+ removeTableElement(table)
551
+ return
176
552
  }
177
553
 
178
- // Update state content
179
- const state = (table.ownerDocument as any).editorState as EditorState
180
- if (state) {
181
- state.content = table.ownerDocument.body.innerHTML
554
+ const colIndex = coords.col
555
+ const handledCells = new Set<HTMLTableCellElement>()
556
+
557
+ for (let r = 0; r < map.rowCount; r++) {
558
+ const slot = map.grid[r]?.[colIndex]
559
+ if (!slot || handledCells.has(slot.cell)) { continue }
560
+ handledCells.add(slot.cell)
561
+
562
+ const colspan = Math.max(1, Number.parseInt(slot.cell.getAttribute('colspan') || '1', 10) || 1)
563
+ if (colspan > 1) {
564
+ if (colspan - 1 > 1) {
565
+ slot.cell.setAttribute('colspan', String(colspan - 1))
566
+ } else {
567
+ slot.cell.removeAttribute('colspan')
568
+ }
569
+ } else {
570
+ slot.cell.remove()
571
+ }
182
572
  }
573
+
574
+ // Clean up any empty rows
575
+ Array.from(table.rows).forEach((row) => {
576
+ if (row.cells.length === 0) {
577
+ row.remove()
578
+ }
579
+ })
580
+
581
+ if (table.rows.length === 0 || (table.rows[0]?.cells.length ?? 0) === 0) {
582
+ removeTableElement(table)
583
+ return
584
+ }
585
+
586
+ syncTableColgroup(table, map.colCount - 1)
587
+ recordAndSyncTableMutation(doc)
183
588
  }
184
589
 
185
- export function deleteColumn(range: Range) {
186
- const cell = range.startContainer.parentElement?.closest('td')
187
- if (!cell) { return }
590
+ export function mergeCells(
591
+ range: Range | null,
592
+ doc: Document,
593
+ targetCellOverride?: HTMLTableCellElement | null,
594
+ direction: 'right' | 'down' | 'selection' = 'selection'
595
+ ) {
596
+ const activeCell = resolveActiveCell(range, targetCellOverride)
597
+ if (!activeCell) { return }
188
598
 
189
- const table = cell.closest('table')
599
+ const table = activeCell.closest('table') as HTMLTableElement | null
190
600
  if (!table) { return }
191
601
 
192
- const columnIndex = cell.cellIndex
193
- const { rows } = table
194
- const doc = table.ownerDocument
602
+ const map = buildTableGrid(table)
603
+ const coords = findCellCoords(map, activeCell)
604
+ if (!coords) { return }
605
+
606
+ const isRTL = table.dir === 'rtl'
607
+
608
+ if (direction === 'right') {
609
+ const targetCol = isRTL ? coords.col - 1 : coords.col + coords.colspan
610
+ const neighborSlot = map.grid[coords.row]?.[targetCol]
611
+ if (!neighborSlot || neighborSlot.cell === activeCell) { return }
612
+
613
+ const neighborCell = neighborSlot.cell
614
+ if (neighborCell.parentElement !== activeCell.parentElement) { return }
195
615
 
196
- for (let i = 0; i < rows.length; i++) {
197
- rows[i].deleteCell(columnIndex)
616
+ const neighborRowspan = Math.max(1, Number.parseInt(neighborCell.getAttribute('rowspan') || '1', 10) || 1)
617
+ if (neighborRowspan !== coords.rowspan) { return }
618
+
619
+ const neighborColspan = Math.max(1, Number.parseInt(neighborCell.getAttribute('colspan') || '1', 10) || 1)
620
+ const hasActiveContent = Boolean(activeCell.textContent?.trim() || activeCell.querySelector('img, video, iframe'))
621
+ const hasNeighborContent = Boolean(neighborCell.textContent?.trim() || neighborCell.querySelector('img, video, iframe'))
622
+
623
+ if (!hasActiveContent) {
624
+ activeCell.replaceChildren()
625
+ }
626
+ if (hasNeighborContent) {
627
+ if (hasActiveContent) {
628
+ activeCell.appendChild(doc.createTextNode(' '))
629
+ }
630
+ while (neighborCell.firstChild) {
631
+ activeCell.appendChild(neighborCell.firstChild)
632
+ }
633
+ } else if (!hasActiveContent) {
634
+ activeCell.appendChild(doc.createElement('br'))
635
+ }
636
+ activeCell.setAttribute('colspan', String(coords.colspan + neighborColspan))
637
+ neighborCell.remove()
638
+ syncTableColgroup(table)
639
+ recordAndSyncTableMutation(doc)
640
+ return
198
641
  }
199
642
 
200
- // If no columns left, remove the table
201
- if (rows[0].cells.length === 0) {
202
- table.remove()
643
+ if (direction === 'down') {
644
+ const targetRow = coords.row + coords.rowspan
645
+ const neighborSlot = map.grid[targetRow]?.[coords.col]
646
+ if (!neighborSlot || neighborSlot.cell === activeCell) { return }
647
+
648
+ const neighborCell = neighborSlot.cell
649
+ // Do not merge across <thead> and <tbody>
650
+ if (neighborCell.parentElement?.parentElement !== activeCell.parentElement?.parentElement) {
651
+ return
652
+ }
653
+
654
+ const neighborColspan = Math.max(1, Number.parseInt(neighborCell.getAttribute('colspan') || '1', 10) || 1)
655
+ if (neighborSlot.originCol !== coords.col || neighborColspan !== coords.colspan) { return }
656
+
657
+ const neighborRowspan = Math.max(1, Number.parseInt(neighborCell.getAttribute('rowspan') || '1', 10) || 1)
658
+ const hasActiveContent = Boolean(activeCell.textContent?.trim() || activeCell.querySelector('img, video, iframe'))
659
+ const hasNeighborContent = Boolean(neighborCell.textContent?.trim() || neighborCell.querySelector('img, video, iframe'))
660
+
661
+ if (!hasActiveContent) {
662
+ activeCell.replaceChildren()
663
+ }
664
+ if (hasNeighborContent) {
665
+ if (hasActiveContent) {
666
+ activeCell.appendChild(doc.createElement('br'))
667
+ }
668
+ while (neighborCell.firstChild) {
669
+ activeCell.appendChild(neighborCell.firstChild)
670
+ }
671
+ } else if (!hasActiveContent) {
672
+ activeCell.appendChild(doc.createElement('br'))
673
+ }
674
+ activeCell.setAttribute('rowspan', String(coords.rowspan + neighborRowspan))
675
+ neighborCell.remove()
676
+ syncTableColgroup(table)
677
+ recordAndSyncTableMutation(doc)
678
+ return
203
679
  }
204
680
 
205
- // Update state content
206
- const state = (doc as any).editorState as EditorState
207
- if (state) {
208
- state.content = doc.body.innerHTML
681
+ // Selection-based or toolbar merge: find all cells intersecting the range, or fall back to merging right
682
+ const selectedCells = Array.from(table.querySelectorAll<HTMLTableCellElement>('td, th')).filter(
683
+ c => c.classList.contains('table-cell-selected') || (range && !range.collapsed && range.intersectsNode(c))
684
+ )
685
+
686
+ if (selectedCells.length < 2) {
687
+ mergeCells(range, doc, activeCell, 'right')
688
+ return
689
+ }
690
+
691
+ let minRow = Infinity
692
+ let maxRow = -1
693
+ let minCol = Infinity
694
+ let maxCol = -1
695
+
696
+ for (const cell of selectedCells) {
697
+ const cellCoords = findCellCoords(map, cell)
698
+ if (!cellCoords) { continue }
699
+ minRow = Math.min(minRow, cellCoords.row)
700
+ maxRow = Math.max(maxRow, cellCoords.row + cellCoords.rowspan - 1)
701
+ minCol = Math.min(minCol, cellCoords.col)
702
+ maxCol = Math.max(maxCol, cellCoords.col + cellCoords.colspan - 1)
703
+ }
704
+
705
+ if (minRow > maxRow || minCol > maxCol) { return }
706
+
707
+ const originSlot = map.grid[minRow]?.[minCol]
708
+ if (!originSlot) { return }
709
+ const primaryCell = originSlot.cell
710
+ const primarySection = primaryCell.parentElement?.parentElement
711
+
712
+ const cellsToMerge = new Set<HTMLTableCellElement>()
713
+ for (let r = minRow; r <= maxRow; r++) {
714
+ for (let c = minCol; c <= maxCol; c++) {
715
+ const slot = map.grid[r]?.[c]
716
+ if (!slot) { continue }
717
+ // Do not merge across thead/tbody boundaries
718
+ if (slot.cell.parentElement?.parentElement !== primarySection) { return }
719
+ cellsToMerge.add(slot.cell)
720
+ }
721
+ }
722
+
723
+ if (cellsToMerge.size < 2) { return }
724
+
725
+ const mergedFragment = doc.createDocumentFragment()
726
+ let hasAnyMergedContent = false
727
+ cellsToMerge.forEach((cell) => {
728
+ const hasContent = Boolean(cell.textContent?.trim() || cell.querySelector('img, video, iframe'))
729
+ if (hasContent) {
730
+ if (hasAnyMergedContent) {
731
+ mergedFragment.appendChild(doc.createTextNode(' '))
732
+ }
733
+ while (cell.firstChild) {
734
+ mergedFragment.appendChild(cell.firstChild)
735
+ }
736
+ hasAnyMergedContent = true
737
+ }
738
+ if (cell !== primaryCell) {
739
+ cell.remove()
740
+ }
741
+ })
742
+
743
+ if (hasAnyMergedContent) {
744
+ primaryCell.replaceChildren(mergedFragment)
745
+ } else {
746
+ primaryCell.replaceChildren(doc.createElement('br'))
747
+ }
748
+ const totalRows = maxRow - minRow + 1
749
+ const totalCols = maxCol - minCol + 1
750
+ if (totalRows > 1) {
751
+ primaryCell.setAttribute('rowspan', String(totalRows))
752
+ } else {
753
+ primaryCell.removeAttribute('rowspan')
754
+ }
755
+ if (totalCols > 1) {
756
+ primaryCell.setAttribute('colspan', String(totalCols))
757
+ } else {
758
+ primaryCell.removeAttribute('colspan')
209
759
  }
760
+
761
+ focusTableCell(primaryCell, false)
762
+ syncTableColgroup(table)
763
+ recordAndSyncTableMutation(doc)
210
764
  }
211
765
 
212
- export function alignColumn(range: Range, alignment: 'start' | 'center' | 'end' | 'justify') {
213
- const cell = range.startContainer.parentElement?.closest('td')
766
+ export function splitCell(
767
+ range: Range | null,
768
+ doc: Document,
769
+ targetCellOverride?: HTMLTableCellElement | null
770
+ ) {
771
+ const cell = resolveActiveCell(range, targetCellOverride)
214
772
  if (!cell) { return }
215
773
 
216
- const table = cell.closest('table')
774
+ const table = cell.closest('table') as HTMLTableElement | null
217
775
  if (!table) { return }
218
776
 
219
- const columnIndex = cell.cellIndex
220
- const { rows } = table
777
+ const map = buildTableGrid(table)
778
+ const coords = findCellCoords(map, cell)
779
+ if (!coords) { return }
780
+
781
+ const { row: startRow, col: startCol, rowspan, colspan } = coords
782
+ if (rowspan <= 1 && colspan <= 1) { return }
783
+
784
+ cell.removeAttribute('colspan')
785
+ cell.removeAttribute('rowspan')
786
+
787
+ const tagName = cell.tagName.toLowerCase() as 'td' | 'th'
788
+
789
+ for (let dr = 0; dr < rowspan; dr++) {
790
+ const targetRowIdx = startRow + dr
791
+ const targetRow = map.rows[targetRowIdx]
792
+ if (!targetRow) { continue }
793
+
794
+ for (let dc = 0; dc < colspan; dc++) {
795
+ if (dr === 0 && dc === 0) { continue }
796
+
797
+ const newCell = doc.createElement(tagName)
798
+ newCell.innerHTML = '<br>'
799
+ copyCellBaseStyle(cell, newCell)
800
+
801
+ // Find the next cell in targetRow whose grid column is > startCol + dc
802
+ let refCell: HTMLTableCellElement | null = null
803
+ for (let c = startCol + colspan; c < map.colCount; c++) {
804
+ const slot = map.grid[targetRowIdx]?.[c]
805
+ if (slot && slot.originRow === targetRowIdx && slot.cell.parentElement === targetRow) {
806
+ refCell = slot.cell
807
+ break
808
+ }
809
+ }
221
810
 
222
- for (let i = 0; i < rows.length; i++) {
223
- const cell = rows[i].cells[columnIndex]
224
- if (cell) {
225
- cell.style.textAlign = alignment
811
+ if (refCell) {
812
+ targetRow.insertBefore(newCell, refCell)
813
+ } else {
814
+ targetRow.appendChild(newCell)
815
+ }
226
816
  }
227
817
  }
228
818
 
229
- // Update state content
230
- const state = (table.ownerDocument as any).editorState as EditorState
231
- if (state) {
232
- state.content = table.ownerDocument.body.innerHTML
819
+ syncTableColgroup(table)
820
+ recordAndSyncTableMutation(doc)
821
+ }
822
+
823
+ export function alignColumn(
824
+ range: Range | null,
825
+ alignment: 'start' | 'center' | 'end' | 'justify',
826
+ targetCellOverride?: HTMLTableCellElement | null
827
+ ) {
828
+ const cell = resolveActiveCell(range, targetCellOverride)
829
+ if (!cell) { return }
830
+
831
+ const table = cell.closest('table') as HTMLTableElement | null
832
+ if (!table) { return }
833
+
834
+ const map = buildTableGrid(table)
835
+ const coords = findCellCoords(map, cell)
836
+ if (!coords) { return }
837
+
838
+ for (let r = 0; r < map.rowCount; r++) {
839
+ const slot = map.grid[r]?.[coords.col]
840
+ if (slot?.cell) {
841
+ slot.cell.style.textAlign = alignment
842
+ }
233
843
  }
844
+
845
+ recordAndSyncTableMutation(table.ownerDocument)
846
+ }
847
+
848
+ export function moveColumn(table: HTMLTableElement, fromCol: number, toCol: number): boolean {
849
+ const map = buildTableGrid(table)
850
+ if (fromCol === toCol || fromCol < 0 || toCol < 0 || fromCol >= map.colCount || toCol >= map.colCount) {
851
+ return false
852
+ }
853
+
854
+ // Only move when cells in the source and target columns don't have multi-column spans crossing boundaries
855
+ for (let r = 0; r < map.rowCount; r++) {
856
+ const fromSlot = map.grid[r]?.[fromCol]
857
+ const toSlot = map.grid[r]?.[toCol]
858
+ if (!fromSlot || !toSlot) { return false }
859
+ const fromSpan = Math.max(1, Number.parseInt(fromSlot.cell.getAttribute('colspan') || '1', 10) || 1)
860
+ const toSpan = Math.max(1, Number.parseInt(toSlot.cell.getAttribute('colspan') || '1', 10) || 1)
861
+ if (fromSpan > 1 || toSpan > 1 || fromSlot.originRow !== r || toSlot.originRow !== r) {
862
+ return false
863
+ }
864
+ }
865
+
866
+ // Move cells in each row
867
+ for (let r = 0; r < map.rowCount; r++) {
868
+ const row = map.rows[r]
869
+ const sourceCell = map.grid[r]?.[fromCol]?.cell
870
+ const targetCell = map.grid[r]?.[toCol]?.cell
871
+ if (!sourceCell || !targetCell || sourceCell === targetCell) { continue }
872
+
873
+ if (fromCol < toCol) {
874
+ targetCell.insertAdjacentElement('afterend', sourceCell)
875
+ } else {
876
+ row.insertBefore(sourceCell, targetCell)
877
+ }
878
+ }
879
+
880
+ // Move corresponding <col> in <colgroup> if present
881
+ const colgroup = Array.from(table.children).find(el => el.tagName === 'COLGROUP') as HTMLElement | undefined
882
+ if (colgroup && colgroup.children.length === map.colCount) {
883
+ const sourceColEl = colgroup.children[fromCol]
884
+ const targetColEl = colgroup.children[toCol]
885
+ if (sourceColEl && targetColEl && sourceColEl !== targetColEl) {
886
+ if (fromCol < toCol) {
887
+ targetColEl.insertAdjacentElement('afterend', sourceColEl)
888
+ } else {
889
+ colgroup.insertBefore(sourceColEl, targetColEl)
890
+ }
891
+ }
892
+ }
893
+
894
+ recordAndSyncTableMutation(table.ownerDocument)
895
+ return true
234
896
  }