@bagelink/vue 1.17.2 → 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.
- package/dist/assetTransport.d.ts +9 -31
- package/dist/assetTransport.d.ts.map +1 -1
- package/dist/assets.d.ts +39 -20
- package/dist/assets.d.ts.map +1 -1
- package/dist/components/AddressSearch.vue.d.ts +40 -36
- package/dist/components/AddressSearch.vue.d.ts.map +1 -1
- package/dist/components/ContextMenu.vue.d.ts +518 -0
- package/dist/components/ContextMenu.vue.d.ts.map +1 -0
- package/dist/components/Dropdown.types.d.ts +25 -0
- package/dist/components/Dropdown.types.d.ts.map +1 -0
- package/dist/components/Dropdown.vue.d.ts +20 -15
- package/dist/components/Dropdown.vue.d.ts.map +1 -1
- package/dist/components/FilterQuery.vue.d.ts +1 -1
- package/dist/components/FilterQuery.vue.d.ts.map +1 -1
- package/dist/components/Icon/Icon.vue.d.ts +0 -1
- package/dist/components/Icon/Icon.vue.d.ts.map +1 -1
- package/dist/components/Icon/constants.d.ts +0 -1
- package/dist/components/Icon/constants.d.ts.map +1 -1
- package/dist/components/Icon/types.d.ts +2 -3
- package/dist/components/Icon/types.d.ts.map +1 -1
- package/dist/components/Swiper.vue.d.ts +1 -1
- package/dist/components/draggable/useDraggable.d.ts.map +1 -1
- package/dist/components/form/inputs/IconPickerInput.vue.d.ts +48 -37
- package/dist/components/form/inputs/IconPickerInput.vue.d.ts.map +1 -1
- package/dist/components/form/inputs/RichText/components/EditorToolbar.vue.d.ts.map +1 -1
- package/dist/components/form/inputs/RichText/composables/useCommands.d.ts.map +1 -1
- package/dist/components/form/inputs/RichText/composables/useEditor.d.ts +3 -0
- package/dist/components/form/inputs/RichText/composables/useEditor.d.ts.map +1 -1
- package/dist/components/form/inputs/RichText/composables/useEditorKeyboard.d.ts +1 -1
- package/dist/components/form/inputs/RichText/composables/useEditorKeyboard.d.ts.map +1 -1
- package/dist/components/form/inputs/RichText/index.vue.d.ts.map +1 -1
- package/dist/components/form/inputs/RichText/utils/commands.d.ts.map +1 -1
- package/dist/components/form/inputs/RichText/utils/selection.d.ts +2 -0
- package/dist/components/form/inputs/RichText/utils/selection.d.ts.map +1 -1
- package/dist/components/form/inputs/RichText/utils/table.d.ts +34 -8
- package/dist/components/form/inputs/RichText/utils/table.d.ts.map +1 -1
- package/dist/components/form/inputs/SelectInput.vue.d.ts +71 -63
- package/dist/components/form/inputs/SelectInput.vue.d.ts.map +1 -1
- package/dist/components/form/inputs/TelInput.vue.d.ts +2 -2
- package/dist/components/index.d.ts +3 -1
- package/dist/components/index.d.ts.map +1 -1
- package/dist/composables/index.d.ts +1 -1
- package/dist/composables/index.d.ts.map +1 -1
- package/dist/composables/useBoundedRectDrag.d.ts.map +1 -1
- package/dist/composables/usePointerSession.d.ts +14 -3
- package/dist/composables/usePointerSession.d.ts.map +1 -1
- package/dist/core.css +3 -3
- package/dist/directives/vResize.d.ts.map +1 -1
- package/dist/form-flow/form-flow.d.ts +4 -2
- package/dist/form-flow/form-flow.d.ts.map +1 -1
- package/dist/form-flow/schema-fields.d.ts +4 -1
- package/dist/form-flow/schema-fields.d.ts.map +1 -1
- package/dist/index.cjs +240 -153
- package/dist/index.css +1 -1
- package/dist/index.mjs +18459 -18281
- package/dist/style.css +3 -3
- package/dist/types/BagelForm.d.ts +2 -0
- package/dist/types/BagelForm.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/assetTransport.test.ts +33 -83
- package/src/assetTransport.ts +24 -90
- package/src/assets.test.ts +58 -99
- package/src/assets.ts +78 -161
- package/src/components/ContextMenu.vue +113 -0
- package/src/components/Dropdown.types.ts +27 -0
- package/src/components/Dropdown.vue +129 -64
- package/src/components/FilterQuery.vue +1 -1
- package/src/components/Icon/Icon.vue +8 -30
- package/src/components/Icon/constants.ts +0 -166
- package/src/components/Icon/types.ts +2 -3
- package/src/components/draggable/useDraggable.ts +67 -100
- package/src/components/form/inputs/IconPickerInput.vue +14 -9
- package/src/components/form/inputs/RichText/components/EditorToolbar.vue +43 -111
- package/src/components/form/inputs/RichText/composables/useCommands.ts +32 -54
- package/src/components/form/inputs/RichText/composables/useEditor.ts +45 -171
- package/src/components/form/inputs/RichText/composables/useEditorKeyboard.ts +90 -14
- package/src/components/form/inputs/RichText/index.vue +1195 -1210
- package/src/components/form/inputs/RichText/utils/commands.ts +689 -1028
- package/src/components/form/inputs/RichText/utils/selection.ts +66 -41
- package/src/components/form/inputs/RichText/utils/table.ts +798 -136
- package/src/components/form/inputs/SelectInput.vue +1 -1
- package/src/components/form/inputs/Upload/AssetUploadInput.vue +2 -2
- package/src/components/index.ts +3 -1
- package/src/composables/index.ts +1 -1
- package/src/composables/useBoundedRectDrag.ts +9 -12
- package/src/composables/usePointerSession.ts +57 -12
- package/src/directives/vResize.ts +6 -7
- package/src/form-flow/form-flow.ts +8 -3
- package/src/form-flow/schema-fields.ts +9 -4
- package/src/styles/core.css +1 -18
- package/src/types/BagelForm.ts +2 -0
- package/dist/components/form/inputs/RichText/utils/formatting.d.ts +0 -10
- package/dist/components/form/inputs/RichText/utils/formatting.d.ts.map +0 -1
- package/src/components/form/inputs/RichText/components/gridBox.vue +0 -58
- 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
|
|
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 =
|
|
228
|
+
const thead = doc.createElement('thead')
|
|
12
229
|
const headerRow = thead.insertRow()
|
|
13
230
|
for (let j = 0; j < cols; j++) {
|
|
14
|
-
const th =
|
|
15
|
-
th.
|
|
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 =
|
|
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 = '
|
|
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
|
-
|
|
37
|
-
const
|
|
38
|
-
if (
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
if (
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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
|
-
|
|
269
|
+
doc.body.appendChild(table)
|
|
56
270
|
}
|
|
57
|
-
state.content = state.doc.body.innerHTML
|
|
58
|
-
}
|
|
59
271
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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
|
-
|
|
71
|
-
|
|
72
|
-
actualCell?.remove()
|
|
73
|
-
})
|
|
280
|
+
const addButtons = (doc as any).__addEditButtonsToTables as (() => void) | undefined
|
|
281
|
+
addButtons?.()
|
|
74
282
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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
|
|
83
|
-
|
|
84
|
-
|
|
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.
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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
|
-
//
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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
|
|
108
|
-
const cell = range
|
|
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.
|
|
112
|
-
const table =
|
|
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
|
|
116
|
-
|
|
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
|
-
|
|
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
|
-
|
|
121
|
-
const
|
|
122
|
-
|
|
123
|
-
|
|
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
|
|
128
|
-
const cell = range
|
|
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
|
|
132
|
-
if (!
|
|
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
|
-
|
|
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
|
|
145
|
-
|
|
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
|
|
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
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
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
|
|
162
|
-
const cell = range
|
|
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
|
|
169
|
-
const
|
|
544
|
+
const doc = table.ownerDocument
|
|
545
|
+
const map = buildTableGrid(table)
|
|
546
|
+
const coords = findCellCoords(map, cell)
|
|
547
|
+
if (!coords) { return }
|
|
170
548
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
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
|
-
|
|
179
|
-
const
|
|
180
|
-
|
|
181
|
-
|
|
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
|
|
186
|
-
|
|
187
|
-
|
|
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 =
|
|
599
|
+
const table = activeCell.closest('table') as HTMLTableElement | null
|
|
190
600
|
if (!table) { return }
|
|
191
601
|
|
|
192
|
-
const
|
|
193
|
-
const
|
|
194
|
-
|
|
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
|
-
|
|
197
|
-
|
|
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
|
-
|
|
201
|
-
|
|
202
|
-
|
|
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
|
-
//
|
|
206
|
-
const
|
|
207
|
-
|
|
208
|
-
|
|
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
|
|
213
|
-
|
|
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
|
|
220
|
-
const
|
|
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
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
811
|
+
if (refCell) {
|
|
812
|
+
targetRow.insertBefore(newCell, refCell)
|
|
813
|
+
} else {
|
|
814
|
+
targetRow.appendChild(newCell)
|
|
815
|
+
}
|
|
226
816
|
}
|
|
227
817
|
}
|
|
228
818
|
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
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
|
}
|