@openleaf-editor/plugins-table 0.1.0-beta.4 → 0.1.0-beta.5
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/README.md +39 -3
- package/dist/commands.d.ts +9 -0
- package/dist/commands.d.ts.map +1 -1
- package/dist/commands.js +348 -68
- package/dist/commands.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +15 -3
- package/dist/index.js.map +1 -1
- package/dist/menu.d.ts.map +1 -1
- package/dist/menu.js +2 -2
- package/dist/menu.js.map +1 -1
- package/dist/paste.d.ts +61 -0
- package/dist/paste.d.ts.map +1 -0
- package/dist/paste.js +113 -0
- package/dist/paste.js.map +1 -0
- package/dist/resize.d.ts +29 -0
- package/dist/resize.d.ts.map +1 -0
- package/dist/resize.js +162 -0
- package/dist/resize.js.map +1 -0
- package/package.json +5 -5
package/README.md
CHANGED
|
@@ -67,6 +67,42 @@ Without that, every remaining column inherited the previous `<col>`'s width and
|
|
|
67
67
|
class. Column resize still patches widths onto the same elements, so the two
|
|
68
68
|
paths do not fight.
|
|
69
69
|
|
|
70
|
+
Merge and split keep that contract too. A merged cell's `colwidth` is one
|
|
71
|
+
entry per column it covers — the concatenation of those columns' widths, filled
|
|
72
|
+
from other cells in the column and then from the stored colgroup, never folded
|
|
73
|
+
into a single number — and `scope` follows the new span: a header that covers
|
|
74
|
+
two columns is `scope="colgroup"`, and a body cell that still carried `scope`
|
|
75
|
+
from an earlier header conversion has it cleared. Split writes each entry back
|
|
76
|
+
onto the cell that covers that column.
|
|
77
|
+
|
|
78
|
+
Inserting a row or column copies the neighbour cell's alignment and background
|
|
79
|
+
so the new cells look like the ones the author was in. It does not copy `class`
|
|
80
|
+
(often a band the author wanted on one row), `scope` (owned by the header pass
|
|
81
|
+
above), or — on a new column — `colwidth`, because a new column is an unmeasured
|
|
82
|
+
`<col>` and stamping the neighbour's width would make the next resize fight it.
|
|
83
|
+
A body row inserted below a header copies from a body neighbour of the same
|
|
84
|
+
type, not from the header's chrome.
|
|
85
|
+
|
|
86
|
+
## Nested tables
|
|
87
|
+
|
|
88
|
+
A cell may contain a table — `table_cell` is `block+`, and Insert table inside a
|
|
89
|
+
cell is how you make one. Two editing rules follow from that:
|
|
90
|
+
|
|
91
|
+
- **Column resize.** Dragging a column border targets the table whose border the
|
|
92
|
+
pointer is on. When the pointer sits on an outer column edge that crosses a
|
|
93
|
+
nested table, the outer grid wins, so parent column widths can be restored
|
|
94
|
+
without first leaving the inner table.
|
|
95
|
+
- **Paste.** A whole table pasted at a text caret inside a cell is inserted as a
|
|
96
|
+
nested table, and it keeps the table's own attributes — caption, colgroup,
|
|
97
|
+
header and footer row counts, `border`, `class`. That is true of external HTML
|
|
98
|
+
(a browser, Word, or Excel table, which arrives as a closed table) and of an
|
|
99
|
+
internal copy of every cell (which arrives as that same table node, open by
|
|
100
|
+
one). Loose cells pasted at a text caret are wrapped in a new table and nested
|
|
101
|
+
the same way; a copied rectangle is not a table, so it does not inherit a
|
|
102
|
+
caption it did not carry. A cell selection still maps pasted cells onto the
|
|
103
|
+
selected rectangle. In no case does a caret-paste rewrite `colspan` on cells
|
|
104
|
+
the author did not select.
|
|
105
|
+
|
|
70
106
|
## License
|
|
71
107
|
|
|
72
108
|
Apache-2.0.
|
|
@@ -83,9 +119,9 @@ the ones you want in the element's `toolbar` attribute:
|
|
|
83
119
|
| `rowProperties` | Row properties dialog |
|
|
84
120
|
| `cellProperties` | Cell properties dialog |
|
|
85
121
|
| `tableCaption` | Caption dialog |
|
|
86
|
-
| `addRowBefore`, `addRowAfter`, `deleteRow` | Row commands. They keep `headerRows` / `footerRows` in step so `<thead>` and `<tfoot>` stay on the header and footer rows the author actually has. |
|
|
87
|
-
| `addColumnBefore`, `addColumnAfter`, `deleteColumn` | Column commands |
|
|
88
|
-
| `mergeCells`, `splitCell` | Cell merge and split |
|
|
122
|
+
| `addRowBefore`, `addRowAfter`, `deleteRow` | Row commands. They keep `headerRows` / `footerRows` in step so `<thead>` and `<tfoot>` stay on the header and footer rows the author actually has. A new row copies the neighbour cell's alignment, background, and `colwidth`. |
|
|
123
|
+
| `addColumnBefore`, `addColumnAfter`, `deleteColumn` | Column commands. A new column copies alignment and background, not `colwidth`. |
|
|
124
|
+
| `mergeCells`, `splitCell` | Cell merge and split. They keep per-column `colwidth` and rewrite `scope` for the new span. |
|
|
89
125
|
| `toggleHeaderRow` | Promote or demote the header row |
|
|
90
126
|
| `deleteTable` | Delete the whole table |
|
|
91
127
|
|
package/dist/commands.d.ts
CHANGED
|
@@ -6,6 +6,13 @@
|
|
|
6
6
|
* delete also reindex the stored colgroup here: the upstream commands only move
|
|
7
7
|
* cells, and `colgroupFromCellWidths` never runs unless a cell already has
|
|
8
8
|
* `colwidth`.
|
|
9
|
+
*
|
|
10
|
+
* `mergeCells` and `splitCell` stay the stock commands. This file wraps them
|
|
11
|
+
* so that after they run, `colwidth` is one entry per covered column (the
|
|
12
|
+
* same array `colgroupFromCellWidths` already walks) and `applyCellScope`
|
|
13
|
+
* sees the new span. The colgroup sync plugin does the same repair for a
|
|
14
|
+
* stock `mergeCells` dispatched from elsewhere, so the two paths cannot
|
|
15
|
+
* disagree.
|
|
9
16
|
*/
|
|
10
17
|
import type { Node, ResolvedPos } from 'prosemirror-model';
|
|
11
18
|
import type { Command, EditorState } from 'prosemirror-state';
|
|
@@ -40,6 +47,8 @@ export declare const addRowAfter: Command;
|
|
|
40
47
|
export declare const addRowBefore: Command;
|
|
41
48
|
export declare const deleteRow: Command;
|
|
42
49
|
export declare const toggleHeaderRow: Command;
|
|
50
|
+
export declare const mergeCells: Command;
|
|
51
|
+
export declare const splitCell: Command;
|
|
43
52
|
export declare function selectedCellPositions(state: EditorState): number[];
|
|
44
53
|
/**
|
|
45
54
|
* Patch declarations onto a style attribute, validating every value written.
|
package/dist/commands.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"commands.d.ts","sourceRoot":"","sources":["../src/commands.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"commands.d.ts","sourceRoot":"","sources":["../src/commands.ts"],"names":[],"mappings":"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"}
|
package/dist/commands.js
CHANGED
|
@@ -6,10 +6,17 @@
|
|
|
6
6
|
* delete also reindex the stored colgroup here: the upstream commands only move
|
|
7
7
|
* cells, and `colgroupFromCellWidths` never runs unless a cell already has
|
|
8
8
|
* `colwidth`.
|
|
9
|
+
*
|
|
10
|
+
* `mergeCells` and `splitCell` stay the stock commands. This file wraps them
|
|
11
|
+
* so that after they run, `colwidth` is one entry per covered column (the
|
|
12
|
+
* same array `colgroupFromCellWidths` already walks) and `applyCellScope`
|
|
13
|
+
* sees the new span. The colgroup sync plugin does the same repair for a
|
|
14
|
+
* stock `mergeCells` dispatched from elsewhere, so the two paths cannot
|
|
15
|
+
* disagree.
|
|
9
16
|
*/
|
|
10
17
|
import { canInsert, isNodeActive, parseDeclarations, safeColor, safeTableStyleValue, serializeDeclarations, } from '@openleaf-editor/core';
|
|
11
18
|
import { Plugin, TextSelection } from 'prosemirror-state';
|
|
12
|
-
import { addColumnAfter as addColumnAfterRaw, addColumnBefore as addColumnBeforeRaw, addRowAfter as addRowAfterRaw, addRowBefore as addRowBeforeRaw, CellSelection, deleteColumn as deleteColumnRaw, deleteRow as deleteRowRaw, TableMap, toggleHeaderRow as toggleHeaderRowRaw, } from 'prosemirror-tables';
|
|
19
|
+
import { addColumnAfter as addColumnAfterRaw, addColumnBefore as addColumnBeforeRaw, addRowAfter as addRowAfterRaw, addRowBefore as addRowBeforeRaw, CellSelection, deleteColumn as deleteColumnRaw, deleteRow as deleteRowRaw, mergeCells as mergeCellsRaw, splitCell as splitCellRaw, TableMap, toggleHeaderRow as toggleHeaderRowRaw, } from 'prosemirror-tables';
|
|
13
20
|
export function inTable(state) {
|
|
14
21
|
return isNodeActive(state, 'table');
|
|
15
22
|
}
|
|
@@ -56,7 +63,39 @@ export function insertTable(rows = 3, cols = 3) {
|
|
|
56
63
|
};
|
|
57
64
|
}
|
|
58
65
|
/**
|
|
59
|
-
*
|
|
66
|
+
* The `scope` this header should carry, given where it sits and how far it spans.
|
|
67
|
+
*
|
|
68
|
+
* Axis is the rule `applyCellScope` has always used: the first row, and every
|
|
69
|
+
* header that is not the row-opening cell, heads a column. `colspan` / `rowspan`
|
|
70
|
+
* then promote that axis to the HTML group tokens — a two-column header is
|
|
71
|
+
* `colgroup`, not `col`, which is what a screen reader needs to hear.
|
|
72
|
+
*/
|
|
73
|
+
function expectedHeaderScope(rowIndex, cellIndex, colspan, rowspan) {
|
|
74
|
+
const axis = rowIndex === 0 || cellIndex > 0 ? 'col' : 'row';
|
|
75
|
+
if (axis === 'col' && colspan > 1)
|
|
76
|
+
return 'colgroup';
|
|
77
|
+
if (axis === 'row' && rowspan > 1)
|
|
78
|
+
return 'rowgroup';
|
|
79
|
+
return axis;
|
|
80
|
+
}
|
|
81
|
+
function scopeOf(node) {
|
|
82
|
+
const scope = node.attrs['scope'];
|
|
83
|
+
return scope === null || scope === undefined || scope === '' ? null : String(scope);
|
|
84
|
+
}
|
|
85
|
+
function scopeSpanMismatch(current, expected) {
|
|
86
|
+
return ((current === 'col' && expected === 'colgroup') ||
|
|
87
|
+
(current === 'colgroup' && expected === 'col') ||
|
|
88
|
+
(current === 'row' && expected === 'rowgroup') ||
|
|
89
|
+
(current === 'rowgroup' && expected === 'row'));
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Rewrite header cell `scope` for a table.
|
|
93
|
+
*
|
|
94
|
+
* `fillMissing` is how insert and toggle-header give a new `<th>` the same
|
|
95
|
+
* `scope` `insertTable` uses. Merge and split already have a scope, just the
|
|
96
|
+
* wrong one for the new span, so the colgroup sync plugin calls this with
|
|
97
|
+
* `fillMissing: false` and only repairs contradictions — otherwise a keystroke
|
|
98
|
+
* inside a table would invent `scope` on headers the author had left bare.
|
|
60
99
|
*
|
|
61
100
|
* The selection is captured and restored around the rewrite. Every change here
|
|
62
101
|
* is a `setNodeMarkup`, which replaces the cell node, and mapping a text
|
|
@@ -65,21 +104,23 @@ export function insertTable(rows = 3, cols = 3) {
|
|
|
65
104
|
* out of the cell the author was editing, so the next table command found no
|
|
66
105
|
* cell and did nothing at all.
|
|
67
106
|
*/
|
|
68
|
-
function applyCellScope(tr) {
|
|
107
|
+
function applyCellScope(tr, tablePos, fillMissing = true) {
|
|
69
108
|
const header = tr.doc.type.schema.nodes['table_header'];
|
|
70
109
|
const cell = tr.doc.type.schema.nodes['table_cell'];
|
|
71
110
|
if (!header || !cell)
|
|
72
111
|
return tr;
|
|
73
|
-
let
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
112
|
+
let pos = tablePos ?? -1;
|
|
113
|
+
if (pos < 0) {
|
|
114
|
+
for (let depth = tr.selection.$from.depth; depth > 0; depth -= 1) {
|
|
115
|
+
if (tr.selection.$from.node(depth).type.spec['tableRole'] === 'table') {
|
|
116
|
+
pos = tr.selection.$from.before(depth);
|
|
117
|
+
break;
|
|
118
|
+
}
|
|
78
119
|
}
|
|
79
120
|
}
|
|
80
|
-
if (
|
|
121
|
+
if (pos < 0)
|
|
81
122
|
return tr;
|
|
82
|
-
const table = tr.doc.nodeAt(
|
|
123
|
+
const table = tr.doc.nodeAt(pos);
|
|
83
124
|
if (!table)
|
|
84
125
|
return tr;
|
|
85
126
|
// Only a plain text selection needs restoring: CellSelection maps itself and
|
|
@@ -87,22 +128,28 @@ function applyCellScope(tr) {
|
|
|
87
128
|
const restore = tr.selection instanceof TextSelection ? tr.selection.from : null;
|
|
88
129
|
const stepsBefore = tr.steps.length;
|
|
89
130
|
table.forEach((row, rowOffset, rowIndex) => {
|
|
90
|
-
const rowPos =
|
|
131
|
+
const rowPos = pos + 1 + rowOffset;
|
|
91
132
|
row.forEach((cellNode, cellOffset, cellIndex) => {
|
|
92
|
-
const
|
|
93
|
-
if (cellNode.type === cell
|
|
94
|
-
|
|
133
|
+
const cellPos = rowPos + 1 + cellOffset;
|
|
134
|
+
if (cellNode.type === cell) {
|
|
135
|
+
if (cellNode.attrs['scope']) {
|
|
136
|
+
tr.setNodeMarkup(cellPos, undefined, { ...cellNode.attrs, scope: null });
|
|
137
|
+
}
|
|
95
138
|
return;
|
|
96
139
|
}
|
|
97
140
|
if (cellNode.type !== header)
|
|
98
141
|
return;
|
|
99
|
-
const
|
|
100
|
-
|
|
142
|
+
const expected = expectedHeaderScope(rowIndex, cellIndex, cellNode.attrs['colspan'] || 1, cellNode.attrs['rowspan'] || 1);
|
|
143
|
+
const current = scopeOf(cellNode);
|
|
144
|
+
if (current === expected)
|
|
101
145
|
return;
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
146
|
+
// A set `scope` is author intent — a first-row cell can still be a row
|
|
147
|
+
// header — unless the span has outgrown the token (`col` on colspan=2).
|
|
148
|
+
if (current !== null && !scopeSpanMismatch(current, expected))
|
|
149
|
+
return;
|
|
150
|
+
if (current === null && !fillMissing)
|
|
151
|
+
return;
|
|
152
|
+
tr.setNodeMarkup(cellPos, undefined, { ...cellNode.attrs, scope: expected });
|
|
106
153
|
});
|
|
107
154
|
});
|
|
108
155
|
// Mapped through only the steps this pass added, so the caret lands where it
|
|
@@ -189,13 +236,225 @@ function applyColgroupColumnChange(tr, range, kind) {
|
|
|
189
236
|
return;
|
|
190
237
|
tr.setNodeMarkup(pos, undefined, { ...table.attrs, colgroup: next });
|
|
191
238
|
}
|
|
192
|
-
|
|
193
|
-
|
|
239
|
+
/**
|
|
240
|
+
* Visual cell attributes Word/Excel authors expect a new row or column to
|
|
241
|
+
* inherit from the cell they were in.
|
|
242
|
+
*
|
|
243
|
+
* Copied: `align`, `valign`, `style` (background and padding), `bgcolor`, and
|
|
244
|
+
* — on a new *row* only — `colwidth`, so every cell in a column still carries
|
|
245
|
+
* the same stored widths the next resize will read.
|
|
246
|
+
*
|
|
247
|
+
* Not copied, on purpose:
|
|
248
|
+
* - `class` — often a band (`odd`/`even`) the author wanted on one row
|
|
249
|
+
* - `scope` / `headers` / `abbr` — identity of the source cell; `applyCellScope`
|
|
250
|
+
* owns `scope`
|
|
251
|
+
* - `colwidth` on a new *column* — `withColgroupColumns` inserts a bare `<col>`
|
|
252
|
+
* so later columns keep their own widths; stamping the neighbour's colwidth
|
|
253
|
+
* onto the new cells would make the sync plugin fight that bare column
|
|
254
|
+
* - header vs body type — stock `addRow` / `addColumn` already copy the type,
|
|
255
|
+
* and `maintainTableSections` refuses to insert into `<thead>` / `<tfoot>`
|
|
256
|
+
* - header `style` onto a body cell — the neighbour lookup requires matching
|
|
257
|
+
* type, so a body row inserted below a header copies from the body row on
|
|
258
|
+
* the other side, not from the header chrome
|
|
259
|
+
*/
|
|
260
|
+
const COPIED_CELL_ATTRS = ['align', 'valign', 'style', 'bgcolor'];
|
|
261
|
+
function withCopiedCellFormatting(command) {
|
|
262
|
+
return (state, dispatch, view) => {
|
|
263
|
+
if (!dispatch)
|
|
264
|
+
return command(state, undefined, view);
|
|
265
|
+
const found = findTable(state.selection.$from);
|
|
266
|
+
const before = found ? { pos: found.pos, node: found.node } : null;
|
|
267
|
+
return command(state, (tr) => {
|
|
268
|
+
if (before)
|
|
269
|
+
copyFormattingOntoInsertedCells(tr, before);
|
|
270
|
+
dispatch(tr);
|
|
271
|
+
}, view);
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
function copyFormattingOntoInsertedCells(tr, before) {
|
|
275
|
+
const pos = tr.mapping.map(before.pos);
|
|
276
|
+
const after = tr.doc.nodeAt(pos);
|
|
277
|
+
if (!after || after.type.spec['tableRole'] !== 'table')
|
|
278
|
+
return;
|
|
279
|
+
const insertedRow = insertedRowIndex(before.node, after);
|
|
280
|
+
const insertedCol = insertedRow === null ? insertedColumnIndex(before.node, after) : null;
|
|
281
|
+
if (insertedRow === null && insertedCol === null)
|
|
282
|
+
return;
|
|
283
|
+
const map = TableMap.get(after);
|
|
284
|
+
after.forEach((row, rowOffset, rowIndex) => {
|
|
285
|
+
const rowPos = pos + 1 + rowOffset;
|
|
286
|
+
let col = 0;
|
|
287
|
+
row.forEach((cell, cellOffset) => {
|
|
288
|
+
const colspan = cell.attrs['colspan'] || 1;
|
|
289
|
+
const hit = insertedRow !== null ? rowIndex === insertedRow : col === insertedCol;
|
|
290
|
+
if (hit &&
|
|
291
|
+
!cell.textContent &&
|
|
292
|
+
colspan === 1 &&
|
|
293
|
+
(cell.attrs['rowspan'] || 1) === 1) {
|
|
294
|
+
const source = neighbourCell(after, map, rowIndex, col, insertedRow !== null ? 'row' : 'col', cell.type);
|
|
295
|
+
if (source) {
|
|
296
|
+
tr.setNodeMarkup(rowPos + 1 + cellOffset, undefined, {
|
|
297
|
+
...cell.attrs,
|
|
298
|
+
...copiedCellAttrs(source, insertedRow !== null),
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
col += colspan;
|
|
303
|
+
});
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
function insertedRowIndex(before, after) {
|
|
307
|
+
if (after.childCount !== before.childCount + 1)
|
|
308
|
+
return null;
|
|
309
|
+
const text = (row) => row.textContent;
|
|
310
|
+
for (let at = 0; at <= before.childCount; at += 1) {
|
|
311
|
+
let matches = true;
|
|
312
|
+
for (let i = 0; i < before.childCount; i += 1) {
|
|
313
|
+
const afterIndex = i < at ? i : i + 1;
|
|
314
|
+
if (text(before.child(i)) !== text(after.child(afterIndex))) {
|
|
315
|
+
matches = false;
|
|
316
|
+
break;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
if (matches)
|
|
320
|
+
return at;
|
|
321
|
+
}
|
|
322
|
+
return null;
|
|
323
|
+
}
|
|
324
|
+
function insertedColumnIndex(before, after) {
|
|
325
|
+
const beforeMap = TableMap.get(before);
|
|
326
|
+
const afterMap = TableMap.get(after);
|
|
327
|
+
if (afterMap.width !== beforeMap.width + 1 || afterMap.height !== beforeMap.height) {
|
|
328
|
+
return null;
|
|
329
|
+
}
|
|
330
|
+
const textAt = (table, map, row, col) => table.nodeAt(map.map[row * map.width + col] ?? 0)?.textContent ?? '';
|
|
331
|
+
for (let at = 0; at <= beforeMap.width; at += 1) {
|
|
332
|
+
let matches = true;
|
|
333
|
+
outer: for (let row = 0; row < beforeMap.height; row += 1) {
|
|
334
|
+
for (let col = 0; col < beforeMap.width; col += 1) {
|
|
335
|
+
const afterCol = col < at ? col : col + 1;
|
|
336
|
+
if (textAt(before, beforeMap, row, col) !== textAt(after, afterMap, row, afterCol)) {
|
|
337
|
+
matches = false;
|
|
338
|
+
break outer;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
if (matches)
|
|
343
|
+
return at;
|
|
344
|
+
}
|
|
345
|
+
return null;
|
|
346
|
+
}
|
|
347
|
+
function neighbourCell(table, map, row, col, axis, type) {
|
|
348
|
+
const prefer = axis === 'row' ? (row > 0 ? -1 : 1) : col > 0 ? -1 : 1;
|
|
349
|
+
const limit = axis === 'row' ? map.height : map.width;
|
|
350
|
+
for (const dir of [prefer, -prefer]) {
|
|
351
|
+
const at = (axis === 'row' ? row : col) + dir;
|
|
352
|
+
if (at < 0 || at >= limit)
|
|
353
|
+
continue;
|
|
354
|
+
const index = axis === 'row' ? at * map.width + col : row * map.width + at;
|
|
355
|
+
const node = table.nodeAt(map.map[index] ?? 0);
|
|
356
|
+
if (node && node.type === type)
|
|
357
|
+
return node;
|
|
358
|
+
}
|
|
359
|
+
return null;
|
|
360
|
+
}
|
|
361
|
+
function copiedCellAttrs(source, includeColwidth) {
|
|
362
|
+
const next = {};
|
|
363
|
+
for (const key of COPIED_CELL_ATTRS) {
|
|
364
|
+
if (source.attrs[key] != null)
|
|
365
|
+
next[key] = source.attrs[key];
|
|
366
|
+
}
|
|
367
|
+
if (includeColwidth &&
|
|
368
|
+
source.attrs['colwidth'] != null &&
|
|
369
|
+
(source.attrs['colspan'] || 1) === 1) {
|
|
370
|
+
// A spanning neighbour holds one width per column it covers. Copying that
|
|
371
|
+
// array onto a colspan=1 cell would disagree with the schema's length check.
|
|
372
|
+
next['colwidth'] = source.attrs['colwidth'];
|
|
373
|
+
}
|
|
374
|
+
return next;
|
|
375
|
+
}
|
|
376
|
+
/**
|
|
377
|
+
* Snapshot each column's width before a merge or split, then write those
|
|
378
|
+
* widths back onto the cells the command produced.
|
|
379
|
+
*
|
|
380
|
+
* Stock `mergeCells` grows `colwidth` with `addColSpan`, which inserts `0` for
|
|
381
|
+
* every newly covered column rather than taking the other cell's width. Stock
|
|
382
|
+
* `splitCell` then treats `0` as "no width". The array is one entry per
|
|
383
|
+
* covered column — the same invariant `colgroupFromCellWidths` already
|
|
384
|
+
* documents — so "sum" here means concatenate, not fold into a single number.
|
|
385
|
+
* Folding would make the next resize fight the colgroup: the spanning cell
|
|
386
|
+
* would report one width for every column it covers.
|
|
387
|
+
*
|
|
388
|
+
* Missing entries are filled from another cell in the same column, then from
|
|
389
|
+
* the stored colgroup. A column nobody knows a width for stays unknown: we do
|
|
390
|
+
* not invent one, and we do not divide a neighbour's width across the hole
|
|
391
|
+
* (100 and unknown is not 50/50). An odd pair such as 100 and 101 stays
|
|
392
|
+
* `[100, 101]` so a later split restores the same integers.
|
|
393
|
+
*/
|
|
394
|
+
function withPreservedColumnWidths(command) {
|
|
395
|
+
return (state, dispatch, view) => {
|
|
396
|
+
if (!dispatch)
|
|
397
|
+
return command(state, undefined, view);
|
|
398
|
+
const found = findTable(state.selection.$from);
|
|
399
|
+
const snapshot = found ? { pos: found.pos, widths: columnWidths(found.node) } : null;
|
|
400
|
+
return command(state, (tr) => {
|
|
401
|
+
if (snapshot) {
|
|
402
|
+
const pos = tr.mapping.map(snapshot.pos);
|
|
403
|
+
const table = tr.doc.nodeAt(pos);
|
|
404
|
+
if (table?.type.spec['tableRole'] === 'table') {
|
|
405
|
+
writeColwidthOntoCells(tr, pos, table, snapshot.widths);
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
dispatch(tr);
|
|
409
|
+
}, view);
|
|
410
|
+
};
|
|
411
|
+
}
|
|
412
|
+
function writeColwidthOntoCells(tr, tablePos, table, widths, spanningOnly = false) {
|
|
413
|
+
const map = TableMap.get(table);
|
|
414
|
+
const seen = new Set();
|
|
415
|
+
for (let i = 0; i < map.map.length; i += 1) {
|
|
416
|
+
const rel = map.map[i] ?? 0;
|
|
417
|
+
if (seen.has(rel))
|
|
418
|
+
continue;
|
|
419
|
+
seen.add(rel);
|
|
420
|
+
const cell = table.nodeAt(rel);
|
|
421
|
+
if (!cell)
|
|
422
|
+
continue;
|
|
423
|
+
const colspan = cell.attrs['colspan'] || 1;
|
|
424
|
+
if (spanningOnly && colspan < 2)
|
|
425
|
+
continue;
|
|
426
|
+
const col = i % map.width;
|
|
427
|
+
const next = colwidthForSpan(widths, col, col + colspan);
|
|
428
|
+
const current = cell.attrs['colwidth'];
|
|
429
|
+
// Incomplete knowledge (`null`) is not a reason to wipe a partial array
|
|
430
|
+
// stock merge left behind. Only write when every covered column is known.
|
|
431
|
+
if (next === null || sameColwidth(current, next))
|
|
432
|
+
continue;
|
|
433
|
+
tr.setNodeMarkup(tablePos + 1 + rel, undefined, { ...cell.attrs, colwidth: next });
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
function colwidthForSpan(widths, left, right) {
|
|
437
|
+
const slice = widths.slice(left, right);
|
|
438
|
+
if (slice.length === 0 || slice.some((width) => width == null))
|
|
439
|
+
return null;
|
|
440
|
+
return slice;
|
|
441
|
+
}
|
|
442
|
+
function sameColwidth(a, b) {
|
|
443
|
+
if (a == null && b == null)
|
|
444
|
+
return true;
|
|
445
|
+
if (a == null || b == null)
|
|
446
|
+
return false;
|
|
447
|
+
return a.length === b.length && a.every((width, i) => width === b[i]);
|
|
448
|
+
}
|
|
449
|
+
export const addColumnAfter = withColgroupColumns('after', withCellScope(withCopiedCellFormatting(addColumnAfterRaw)));
|
|
450
|
+
export const addColumnBefore = withColgroupColumns('before', withCellScope(withCopiedCellFormatting(addColumnBeforeRaw)));
|
|
194
451
|
export const deleteColumn = withColgroupColumns('delete', deleteColumnRaw);
|
|
195
|
-
export const addRowAfter = withCellScope(maintainTableSections(addRowAfterRaw, 'after'));
|
|
196
|
-
export const addRowBefore = withCellScope(maintainTableSections(addRowBeforeRaw, 'before'));
|
|
452
|
+
export const addRowAfter = withCellScope(withCopiedCellFormatting(maintainTableSections(addRowAfterRaw, 'after')));
|
|
453
|
+
export const addRowBefore = withCellScope(withCopiedCellFormatting(maintainTableSections(addRowBeforeRaw, 'before')));
|
|
197
454
|
export const deleteRow = withCellScope(maintainTableSections(deleteRowRaw, 'delete'));
|
|
198
455
|
export const toggleHeaderRow = withCellScope(toggleHeaderRowRaw);
|
|
456
|
+
export const mergeCells = withCellScope(withPreservedColumnWidths(mergeCellsRaw));
|
|
457
|
+
export const splitCell = withCellScope(withPreservedColumnWidths(splitCellRaw));
|
|
199
458
|
/**
|
|
200
459
|
* Keep `headerRows` / `footerRows` attached to the rows that actually live in
|
|
201
460
|
* those sections.
|
|
@@ -637,40 +896,57 @@ function setWidth(col, width) {
|
|
|
637
896
|
export function setTableColgroup(widths) {
|
|
638
897
|
return setTableAttrs({ colgroup: colgroupHtmlFromWidths(widths) });
|
|
639
898
|
}
|
|
640
|
-
|
|
899
|
+
/**
|
|
900
|
+
* One width per column, from cells first, then the stored colgroup.
|
|
901
|
+
*
|
|
902
|
+
* The first row is not enough: merging it is exactly when its `colwidth`
|
|
903
|
+
* becomes `[100, 0]`, and the other rows still hold the second column's 200.
|
|
904
|
+
* Falling through to the colgroup covers the case where the width never lived
|
|
905
|
+
* on a cell at all — inherited `<col width>` furniture — so a later resize
|
|
906
|
+
* does not invent zeros that then blank those `<col>`s.
|
|
907
|
+
*/
|
|
908
|
+
function columnWidths(table) {
|
|
641
909
|
const map = TableMap.get(table);
|
|
642
|
-
const widths =
|
|
643
|
-
let
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
const colwidth = cell?.attrs['colwidth'];
|
|
661
|
-
const width = colwidth?.[offset];
|
|
662
|
-
if (width) {
|
|
663
|
-
widths.push(String(width));
|
|
664
|
-
any = true;
|
|
665
|
-
}
|
|
666
|
-
else {
|
|
667
|
-
widths.push('');
|
|
910
|
+
const widths = Array.from({ length: map.width }, () => null);
|
|
911
|
+
for (let row = 0; row < map.height; row += 1) {
|
|
912
|
+
let previous = -1;
|
|
913
|
+
let offset = 0;
|
|
914
|
+
for (let col = 0; col < map.width; col += 1) {
|
|
915
|
+
const pos = map.map[row * map.width + col] ?? 0;
|
|
916
|
+
if (pos === previous)
|
|
917
|
+
offset += 1;
|
|
918
|
+
else {
|
|
919
|
+
previous = pos;
|
|
920
|
+
offset = 0;
|
|
921
|
+
}
|
|
922
|
+
if (widths[col] != null)
|
|
923
|
+
continue;
|
|
924
|
+
const cell = table.nodeAt(pos);
|
|
925
|
+
const width = cell?.attrs['colwidth']?.[offset];
|
|
926
|
+
if (typeof width === 'number' && width > 0)
|
|
927
|
+
widths[col] = width;
|
|
668
928
|
}
|
|
669
929
|
}
|
|
930
|
+
const fromGroup = widthsFromColgroup(table.attrs['colgroup'], map.width);
|
|
931
|
+
for (let col = 0; col < map.width; col += 1) {
|
|
932
|
+
if (widths[col] != null)
|
|
933
|
+
continue;
|
|
934
|
+
const raw = fromGroup[col];
|
|
935
|
+
if (!raw)
|
|
936
|
+
continue;
|
|
937
|
+
const parsed = Number.parseInt(raw, 10);
|
|
938
|
+
if (parsed > 0)
|
|
939
|
+
widths[col] = parsed;
|
|
940
|
+
}
|
|
941
|
+
return widths;
|
|
942
|
+
}
|
|
943
|
+
function colgroupFromWidths(table, widths) {
|
|
944
|
+
if (widths.every((width) => width == null))
|
|
945
|
+
return null;
|
|
670
946
|
// Patched onto whatever the table already stored, for the same reason the
|
|
671
947
|
// properties dialog patches: a column resize must not cost an inherited
|
|
672
948
|
// colgroup its class or its other attributes.
|
|
673
|
-
return
|
|
949
|
+
return colgroupHtmlWithWidths(table.attrs['colgroup'], widths.map((width) => (width != null ? String(width) : '')));
|
|
674
950
|
}
|
|
675
951
|
/**
|
|
676
952
|
* What `colgroupFromCellWidths` last said about a given table node.
|
|
@@ -690,16 +966,6 @@ function colgroupFromCellWidths(table) {
|
|
|
690
966
|
* has moved past is collectable.
|
|
691
967
|
*/
|
|
692
968
|
const colgroupForNode = new WeakMap();
|
|
693
|
-
function cachedColgroupFromCellWidths(table) {
|
|
694
|
-
const known = colgroupForNode.get(table);
|
|
695
|
-
// `null` is a real answer -- "this table has no resized columns" -- so the
|
|
696
|
-
// miss test is `undefined`, not falsiness.
|
|
697
|
-
if (known !== undefined)
|
|
698
|
-
return known;
|
|
699
|
-
const computed = colgroupFromCellWidths(table);
|
|
700
|
-
colgroupForNode.set(table, computed);
|
|
701
|
-
return computed;
|
|
702
|
-
}
|
|
703
969
|
/**
|
|
704
970
|
* Keep `<colgroup>` in lockstep with column resizing.
|
|
705
971
|
*
|
|
@@ -724,13 +990,27 @@ export function colgroupSyncPlugin() {
|
|
|
724
990
|
// Cached on the node: a transaction elsewhere in the document reuses
|
|
725
991
|
// every table node it did not touch, so an untouched table answers from
|
|
726
992
|
// the map instead of rebuilding its TableMap and reparsing its colgroup.
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
if (
|
|
993
|
+
// `null` is a real answer -- "this table has no resized columns" -- so
|
|
994
|
+
// the miss test is `undefined`, not falsiness.
|
|
995
|
+
const known = colgroupForNode.get(node);
|
|
996
|
+
if (known !== undefined)
|
|
731
997
|
return false;
|
|
732
|
-
|
|
733
|
-
|
|
998
|
+
// One walk for widths, then the existing write helpers no-op when the
|
|
999
|
+
// cell already agrees. A separate "does this need repair?" pass was a
|
|
1000
|
+
// second copy of the same loop.
|
|
1001
|
+
const widths = columnWidths(node);
|
|
1002
|
+
const next = colgroupFromWidths(node, widths);
|
|
1003
|
+
colgroupForNode.set(node, next);
|
|
1004
|
+
const working = tr ?? state.tr;
|
|
1005
|
+
const stepsBefore = working.steps.length;
|
|
1006
|
+
writeColwidthOntoCells(working, pos, node, widths, true);
|
|
1007
|
+
applyCellScope(working, pos, false);
|
|
1008
|
+
const current = working.doc.nodeAt(pos) ?? node;
|
|
1009
|
+
if (next !== null && next !== current.attrs['colgroup']) {
|
|
1010
|
+
working.setNodeMarkup(pos, undefined, { ...current.attrs, colgroup: next });
|
|
1011
|
+
}
|
|
1012
|
+
if (working.steps.length > stepsBefore)
|
|
1013
|
+
tr = working;
|
|
734
1014
|
return false;
|
|
735
1015
|
});
|
|
736
1016
|
return tr;
|