@doxi/core 0.11.0
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/LICENSE +21 -0
- package/README.md +81 -0
- package/dist/collab/port.d.ts +46 -0
- package/dist/collab/port.d.ts.map +1 -0
- package/dist/collab/port.js +11 -0
- package/dist/collab/port.js.map +1 -0
- package/dist/commands/block-commands.d.ts +62 -0
- package/dist/commands/block-commands.d.ts.map +1 -0
- package/dist/commands/block-commands.js +208 -0
- package/dist/commands/block-commands.js.map +1 -0
- package/dist/commands/command.d.ts +13 -0
- package/dist/commands/command.d.ts.map +1 -0
- package/dist/commands/command.js +13 -0
- package/dist/commands/command.js.map +1 -0
- package/dist/commands/edit-commands.d.ts +5 -0
- package/dist/commands/edit-commands.d.ts.map +1 -0
- package/dist/commands/edit-commands.js +147 -0
- package/dist/commands/edit-commands.js.map +1 -0
- package/dist/commands/image-commands.d.ts +31 -0
- package/dist/commands/image-commands.d.ts.map +1 -0
- package/dist/commands/image-commands.js +130 -0
- package/dist/commands/image-commands.js.map +1 -0
- package/dist/commands/index.d.ts +11 -0
- package/dist/commands/index.d.ts.map +1 -0
- package/dist/commands/index.js +11 -0
- package/dist/commands/index.js.map +1 -0
- package/dist/commands/keymap.d.ts +34 -0
- package/dist/commands/keymap.d.ts.map +1 -0
- package/dist/commands/keymap.js +84 -0
- package/dist/commands/keymap.js.map +1 -0
- package/dist/commands/link-commands.d.ts +54 -0
- package/dist/commands/link-commands.d.ts.map +1 -0
- package/dist/commands/link-commands.js +151 -0
- package/dist/commands/link-commands.js.map +1 -0
- package/dist/commands/list-commands.d.ts +42 -0
- package/dist/commands/list-commands.d.ts.map +1 -0
- package/dist/commands/list-commands.js +316 -0
- package/dist/commands/list-commands.js.map +1 -0
- package/dist/commands/mark-commands.d.ts +53 -0
- package/dist/commands/mark-commands.d.ts.map +1 -0
- package/dist/commands/mark-commands.js +181 -0
- package/dist/commands/mark-commands.js.map +1 -0
- package/dist/commands/selection-commands.d.ts +3 -0
- package/dist/commands/selection-commands.d.ts.map +1 -0
- package/dist/commands/selection-commands.js +11 -0
- package/dist/commands/selection-commands.js.map +1 -0
- package/dist/commands/table-commands.d.ts +109 -0
- package/dist/commands/table-commands.d.ts.map +1 -0
- package/dist/commands/table-commands.js +884 -0
- package/dist/commands/table-commands.js.map +1 -0
- package/dist/history/history.d.ts +40 -0
- package/dist/history/history.d.ts.map +1 -0
- package/dist/history/history.js +139 -0
- package/dist/history/history.js.map +1 -0
- package/dist/history/index.d.ts +2 -0
- package/dist/history/index.d.ts.map +1 -0
- package/dist/history/index.js +2 -0
- package/dist/history/index.js.map +1 -0
- package/dist/html/index.d.ts +3 -0
- package/dist/html/index.d.ts.map +1 -0
- package/dist/html/index.js +3 -0
- package/dist/html/index.js.map +1 -0
- package/dist/html/parse.d.ts +4 -0
- package/dist/html/parse.d.ts.map +1 -0
- package/dist/html/parse.js +0 -0
- package/dist/html/parse.js.map +1 -0
- package/dist/html/serialize.d.ts +4 -0
- package/dist/html/serialize.d.ts.map +1 -0
- package/dist/html/serialize.js +75 -0
- package/dist/html/serialize.js.map +1 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +13 -0
- package/dist/index.js.map +1 -0
- package/dist/layout/index.d.ts +6 -0
- package/dist/layout/index.d.ts.map +1 -0
- package/dist/layout/index.js +6 -0
- package/dist/layout/index.js.map +1 -0
- package/dist/layout/layout-engine.d.ts +20 -0
- package/dist/layout/layout-engine.d.ts.map +1 -0
- package/dist/layout/layout-engine.js +198 -0
- package/dist/layout/layout-engine.js.map +1 -0
- package/dist/layout/measure.d.ts +9 -0
- package/dist/layout/measure.d.ts.map +1 -0
- package/dist/layout/measure.js +37 -0
- package/dist/layout/measure.js.map +1 -0
- package/dist/layout/split-paragraph.d.ts +28 -0
- package/dist/layout/split-paragraph.d.ts.map +1 -0
- package/dist/layout/split-paragraph.js +122 -0
- package/dist/layout/split-paragraph.js.map +1 -0
- package/dist/layout/split-table.d.ts +46 -0
- package/dist/layout/split-table.d.ts.map +1 -0
- package/dist/layout/split-table.js +84 -0
- package/dist/layout/split-table.js.map +1 -0
- package/dist/layout/types.d.ts +73 -0
- package/dist/layout/types.d.ts.map +1 -0
- package/dist/layout/types.js +36 -0
- package/dist/layout/types.js.map +1 -0
- package/dist/layout/widow-orphan.d.ts +15 -0
- package/dist/layout/widow-orphan.d.ts.map +1 -0
- package/dist/layout/widow-orphan.js +14 -0
- package/dist/layout/widow-orphan.js.map +1 -0
- package/dist/model/content-expr.d.ts +32 -0
- package/dist/model/content-expr.d.ts.map +1 -0
- package/dist/model/content-expr.js +106 -0
- package/dist/model/content-expr.js.map +1 -0
- package/dist/model/fragment.d.ts +17 -0
- package/dist/model/fragment.d.ts.map +1 -0
- package/dist/model/fragment.js +44 -0
- package/dist/model/fragment.js.map +1 -0
- package/dist/model/index.d.ts +10 -0
- package/dist/model/index.d.ts.map +1 -0
- package/dist/model/index.js +10 -0
- package/dist/model/index.js.map +1 -0
- package/dist/model/mark.d.ts +35 -0
- package/dist/model/mark.d.ts.map +1 -0
- package/dist/model/mark.js +89 -0
- package/dist/model/mark.js.map +1 -0
- package/dist/model/node-type.d.ts +36 -0
- package/dist/model/node-type.d.ts.map +1 -0
- package/dist/model/node-type.js +14 -0
- package/dist/model/node-type.js.map +1 -0
- package/dist/model/node.d.ts +36 -0
- package/dist/model/node.d.ts.map +1 -0
- package/dist/model/node.js +192 -0
- package/dist/model/node.js.map +1 -0
- package/dist/model/position.d.ts +66 -0
- package/dist/model/position.d.ts.map +1 -0
- package/dist/model/position.js +158 -0
- package/dist/model/position.js.map +1 -0
- package/dist/model/schema.d.ts +28 -0
- package/dist/model/schema.d.ts.map +1 -0
- package/dist/model/schema.js +195 -0
- package/dist/model/schema.js.map +1 -0
- package/dist/model/slice.d.ts +26 -0
- package/dist/model/slice.d.ts.map +1 -0
- package/dist/model/slice.js +56 -0
- package/dist/model/slice.js.map +1 -0
- package/dist/model/table-grid.d.ts +71 -0
- package/dist/model/table-grid.d.ts.map +1 -0
- package/dist/model/table-grid.js +130 -0
- package/dist/model/table-grid.js.map +1 -0
- package/dist/plugin/index.d.ts +3 -0
- package/dist/plugin/index.d.ts.map +1 -0
- package/dist/plugin/index.js +3 -0
- package/dist/plugin/index.js.map +1 -0
- package/dist/plugin/plugin-key.d.ts +13 -0
- package/dist/plugin/plugin-key.d.ts.map +1 -0
- package/dist/plugin/plugin-key.js +13 -0
- package/dist/plugin/plugin-key.js.map +1 -0
- package/dist/plugin/plugin-state.d.ts +2 -0
- package/dist/plugin/plugin-state.d.ts.map +1 -0
- package/dist/plugin/plugin-state.js +3 -0
- package/dist/plugin/plugin-state.js.map +1 -0
- package/dist/plugin/plugin.d.ts +39 -0
- package/dist/plugin/plugin.d.ts.map +1 -0
- package/dist/plugin/plugin.js +10 -0
- package/dist/plugin/plugin.js.map +1 -0
- package/dist/schema/default.d.ts +163 -0
- package/dist/schema/default.d.ts.map +1 -0
- package/dist/schema/default.js +94 -0
- package/dist/schema/default.js.map +1 -0
- package/dist/schema/index.d.ts +2 -0
- package/dist/schema/index.d.ts.map +1 -0
- package/dist/schema/index.js +2 -0
- package/dist/schema/index.js.map +1 -0
- package/dist/serialize/index.d.ts +2 -0
- package/dist/serialize/index.d.ts.map +1 -0
- package/dist/serialize/index.js +2 -0
- package/dist/serialize/index.js.map +1 -0
- package/dist/serialize/json.d.ts +15 -0
- package/dist/serialize/json.d.ts.map +1 -0
- package/dist/serialize/json.js +23 -0
- package/dist/serialize/json.js.map +1 -0
- package/dist/state/all-selection.d.ts +11 -0
- package/dist/state/all-selection.d.ts.map +1 -0
- package/dist/state/all-selection.js +17 -0
- package/dist/state/all-selection.js.map +1 -0
- package/dist/state/cell-selection.d.ts +30 -0
- package/dist/state/cell-selection.d.ts.map +1 -0
- package/dist/state/cell-selection.js +38 -0
- package/dist/state/cell-selection.js.map +1 -0
- package/dist/state/editor-state.d.ts +46 -0
- package/dist/state/editor-state.d.ts.map +1 -0
- package/dist/state/editor-state.js +211 -0
- package/dist/state/editor-state.js.map +1 -0
- package/dist/state/index.d.ts +7 -0
- package/dist/state/index.d.ts.map +1 -0
- package/dist/state/index.js +7 -0
- package/dist/state/index.js.map +1 -0
- package/dist/state/node-selection.d.ts +16 -0
- package/dist/state/node-selection.d.ts.map +1 -0
- package/dist/state/node-selection.js +51 -0
- package/dist/state/node-selection.js.map +1 -0
- package/dist/state/selection.d.ts +29 -0
- package/dist/state/selection.d.ts.map +1 -0
- package/dist/state/selection.js +24 -0
- package/dist/state/selection.js.map +1 -0
- package/dist/state/text-selection.d.ts +10 -0
- package/dist/state/text-selection.d.ts.map +1 -0
- package/dist/state/text-selection.js +26 -0
- package/dist/state/text-selection.js.map +1 -0
- package/dist/transform/attr-step.d.ts +16 -0
- package/dist/transform/attr-step.d.ts.map +1 -0
- package/dist/transform/attr-step.js +98 -0
- package/dist/transform/attr-step.js.map +1 -0
- package/dist/transform/index.d.ts +10 -0
- package/dist/transform/index.d.ts.map +1 -0
- package/dist/transform/index.js +10 -0
- package/dist/transform/index.js.map +1 -0
- package/dist/transform/mapping.d.ts +44 -0
- package/dist/transform/mapping.d.ts.map +1 -0
- package/dist/transform/mapping.js +101 -0
- package/dist/transform/mapping.js.map +1 -0
- package/dist/transform/mark-step.d.ts +27 -0
- package/dist/transform/mark-step.d.ts.map +1 -0
- package/dist/transform/mark-step.js +146 -0
- package/dist/transform/mark-step.js.map +1 -0
- package/dist/transform/replace-around-step.d.ts +35 -0
- package/dist/transform/replace-around-step.d.ts.map +1 -0
- package/dist/transform/replace-around-step.js +144 -0
- package/dist/transform/replace-around-step.js.map +1 -0
- package/dist/transform/replace-step.d.ts +17 -0
- package/dist/transform/replace-step.d.ts.map +1 -0
- package/dist/transform/replace-step.js +72 -0
- package/dist/transform/replace-step.js.map +1 -0
- package/dist/transform/replace.d.ts +18 -0
- package/dist/transform/replace.d.ts.map +1 -0
- package/dist/transform/replace.js +132 -0
- package/dist/transform/replace.js.map +1 -0
- package/dist/transform/set-page-meta-step.d.ts +42 -0
- package/dist/transform/set-page-meta-step.d.ts.map +1 -0
- package/dist/transform/set-page-meta-step.js +75 -0
- package/dist/transform/set-page-meta-step.js.map +1 -0
- package/dist/transform/step.d.ts +34 -0
- package/dist/transform/step.d.ts.map +1 -0
- package/dist/transform/step.js +23 -0
- package/dist/transform/step.js.map +1 -0
- package/dist/transform/transaction.d.ts +20 -0
- package/dist/transform/transaction.d.ts.map +1 -0
- package/dist/transform/transaction.js +38 -0
- package/dist/transform/transaction.js.map +1 -0
- package/dist/version.d.ts +2 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +5 -0
- package/dist/version.js.map +1 -0
- package/dist/view/cell-drag.d.ts +33 -0
- package/dist/view/cell-drag.d.ts.map +1 -0
- package/dist/view/cell-drag.js +177 -0
- package/dist/view/cell-drag.js.map +1 -0
- package/dist/view/clipboard.d.ts +5 -0
- package/dist/view/clipboard.d.ts.map +1 -0
- package/dist/view/clipboard.js +97 -0
- package/dist/view/clipboard.js.map +1 -0
- package/dist/view/default-renderer.d.ts +3 -0
- package/dist/view/default-renderer.d.ts.map +1 -0
- package/dist/view/default-renderer.js +142 -0
- package/dist/view/default-renderer.js.map +1 -0
- package/dist/view/dom-spec.d.ts +11 -0
- package/dist/view/dom-spec.d.ts.map +1 -0
- package/dist/view/dom-spec.js +32 -0
- package/dist/view/dom-spec.js.map +1 -0
- package/dist/view/editor-view.d.ts +55 -0
- package/dist/view/editor-view.d.ts.map +1 -0
- package/dist/view/editor-view.js +143 -0
- package/dist/view/editor-view.js.map +1 -0
- package/dist/view/image-resize.d.ts +37 -0
- package/dist/view/image-resize.d.ts.map +1 -0
- package/dist/view/image-resize.js +191 -0
- package/dist/view/image-resize.js.map +1 -0
- package/dist/view/index.d.ts +15 -0
- package/dist/view/index.d.ts.map +1 -0
- package/dist/view/index.js +15 -0
- package/dist/view/index.js.map +1 -0
- package/dist/view/input-pipeline.d.ts +24 -0
- package/dist/view/input-pipeline.d.ts.map +1 -0
- package/dist/view/input-pipeline.js +226 -0
- package/dist/view/input-pipeline.js.map +1 -0
- package/dist/view/mutation-observer.d.ts +17 -0
- package/dist/view/mutation-observer.d.ts.map +1 -0
- package/dist/view/mutation-observer.js +62 -0
- package/dist/view/mutation-observer.js.map +1 -0
- package/dist/view/page-slots.d.ts +56 -0
- package/dist/view/page-slots.d.ts.map +1 -0
- package/dist/view/page-slots.js +230 -0
- package/dist/view/page-slots.js.map +1 -0
- package/dist/view/paginator.d.ts +17 -0
- package/dist/view/paginator.d.ts.map +1 -0
- package/dist/view/paginator.js +93 -0
- package/dist/view/paginator.js.map +1 -0
- package/dist/view/print.d.ts +42 -0
- package/dist/view/print.d.ts.map +1 -0
- package/dist/view/print.js +70 -0
- package/dist/view/print.js.map +1 -0
- package/dist/view/reconcile.d.ts +16 -0
- package/dist/view/reconcile.d.ts.map +1 -0
- package/dist/view/reconcile.js +158 -0
- package/dist/view/reconcile.js.map +1 -0
- package/dist/view/renderer.d.ts +31 -0
- package/dist/view/renderer.d.ts.map +1 -0
- package/dist/view/renderer.js +89 -0
- package/dist/view/renderer.js.map +1 -0
- package/dist/view/selection-sync.d.ts +35 -0
- package/dist/view/selection-sync.d.ts.map +1 -0
- package/dist/view/selection-sync.js +324 -0
- package/dist/view/selection-sync.js.map +1 -0
- package/dist/view/table-resize.d.ts +41 -0
- package/dist/view/table-resize.d.ts.map +1 -0
- package/dist/view/table-resize.js +216 -0
- package/dist/view/table-resize.js.map +1 -0
- package/package.json +93 -0
- package/styles/base.css +269 -0
- package/styles/dark.css +36 -0
- package/styles/light.css +13 -0
- package/styles/page.css +93 -0
- package/styles/print-a4.css +87 -0
- package/styles/print.css +88 -0
|
@@ -0,0 +1,884 @@
|
|
|
1
|
+
import { Fragment } from '../model/fragment.js';
|
|
2
|
+
import { DxNode } from '../model/node.js';
|
|
3
|
+
import { resolve } from '../model/position.js';
|
|
4
|
+
import { Slice } from '../model/slice.js';
|
|
5
|
+
import { tableGrid } from '../model/table-grid.js';
|
|
6
|
+
import { CellSelection } from '../state/cell-selection.js';
|
|
7
|
+
import { TextSelection } from '../state/text-selection.js';
|
|
8
|
+
import { AttrStep } from '../transform/attr-step.js';
|
|
9
|
+
import { ReplaceStep } from '../transform/replace-step.js';
|
|
10
|
+
/**
|
|
11
|
+
* Table commands.
|
|
12
|
+
*
|
|
13
|
+
* The default schema declares:
|
|
14
|
+
* table: { content: 'table_row+', group: 'block', isolating: true }
|
|
15
|
+
* table_row: { content: 'table_cell+', isolating: true }
|
|
16
|
+
* table_cell: { content: 'block+', isolating: true,
|
|
17
|
+
* attrs: { widthPx: number|null, background: string|null } }
|
|
18
|
+
*
|
|
19
|
+
* Every command sets `selectionOverride` so the cursor lands on an editable
|
|
20
|
+
* inline position after the structural reshuffle.
|
|
21
|
+
*/
|
|
22
|
+
function setOverride(tr, pos) {
|
|
23
|
+
;
|
|
24
|
+
tr.selectionOverride =
|
|
25
|
+
new TextSelection(pos, pos);
|
|
26
|
+
}
|
|
27
|
+
function findTableContext(rp) {
|
|
28
|
+
let cellDepth = -1;
|
|
29
|
+
for (let d = rp.depth; d >= 1; d--) {
|
|
30
|
+
if (rp.node(d).type.name === 'table_cell') {
|
|
31
|
+
cellDepth = d;
|
|
32
|
+
break;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
if (cellDepth < 2)
|
|
36
|
+
return null;
|
|
37
|
+
const rowDepth = cellDepth - 1;
|
|
38
|
+
const tableDepth = cellDepth - 2;
|
|
39
|
+
if (tableDepth < 1)
|
|
40
|
+
return null;
|
|
41
|
+
const table = rp.node(tableDepth);
|
|
42
|
+
if (table.type.name !== 'table')
|
|
43
|
+
return null;
|
|
44
|
+
const rowIdx = rp.index(rowDepth);
|
|
45
|
+
const colIdx = rp.index(cellDepth);
|
|
46
|
+
return {
|
|
47
|
+
tableDepth,
|
|
48
|
+
tableStart: rp.before(tableDepth),
|
|
49
|
+
table,
|
|
50
|
+
rowIdx,
|
|
51
|
+
colIdx,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
function makeEmptyCell(schema) {
|
|
55
|
+
return schema.node('table_cell', null, [schema.node('paragraph', null)]);
|
|
56
|
+
}
|
|
57
|
+
function makeEmptyRow(schema, cols) {
|
|
58
|
+
const cells = [];
|
|
59
|
+
for (let i = 0; i < cols; i++)
|
|
60
|
+
cells.push(makeEmptyCell(schema));
|
|
61
|
+
return schema.node('table_row', null, cells);
|
|
62
|
+
}
|
|
63
|
+
function rowCellCount(row) {
|
|
64
|
+
return row.content.childCount;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Insert a fresh `rows × cols` table at the cursor.
|
|
68
|
+
*
|
|
69
|
+
* If the cursor's top-level block (in its container) is an EMPTY paragraph,
|
|
70
|
+
* REPLACE that paragraph with the table. Otherwise INSERT the table as the
|
|
71
|
+
* next sibling of that block.
|
|
72
|
+
*
|
|
73
|
+
* Each cell starts with a single empty paragraph. Cursor lands inside the
|
|
74
|
+
* first cell's paragraph (parentOffset 0).
|
|
75
|
+
*
|
|
76
|
+
* Returns false if rows < 1 or cols < 1, if the schema doesn't carry
|
|
77
|
+
* table / table_row / table_cell, or if the cursor isn't inside any
|
|
78
|
+
* top-level block.
|
|
79
|
+
*/
|
|
80
|
+
export function insertTable(schema, rows, cols) {
|
|
81
|
+
return (state, dispatch) => {
|
|
82
|
+
if (rows < 1 || cols < 1)
|
|
83
|
+
return false;
|
|
84
|
+
if (!schema.nodes.table || !schema.nodes.table_row || !schema.nodes.table_cell) {
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
const rp = resolve(state.doc, state.selection.from);
|
|
88
|
+
if (rp.depth < 1)
|
|
89
|
+
return false;
|
|
90
|
+
// Build the new table.
|
|
91
|
+
const newRows = [];
|
|
92
|
+
for (let r = 0; r < rows; r++)
|
|
93
|
+
newRows.push(makeEmptyRow(schema, cols));
|
|
94
|
+
const table = schema.node('table', null, newRows);
|
|
95
|
+
// Find the cursor's containing block in its container. We want depth 1
|
|
96
|
+
// unless we're inside a cell-like container (table_cell), in which case
|
|
97
|
+
// we operate on the cell's child block. Mirrors findTopBlockDepth, but
|
|
98
|
+
// we keep the logic local here to avoid pulling helpers that could
|
|
99
|
+
// confuse the table-vs-cell case (we never insert nested tables in v0.3b
|
|
100
|
+
// but the cell case still must work for completeness).
|
|
101
|
+
let topD = 1;
|
|
102
|
+
for (let d = rp.depth; d >= 1; d--) {
|
|
103
|
+
const parent = rp.node(d - 1);
|
|
104
|
+
const parentSpec = parent.type.spec;
|
|
105
|
+
if (parent.type.name === 'doc' || parentSpec.isolating === true) {
|
|
106
|
+
topD = d;
|
|
107
|
+
break;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
const block = rp.node(topD);
|
|
111
|
+
if (!dispatch)
|
|
112
|
+
return true;
|
|
113
|
+
const isEmptyParagraph = block.type.name === 'paragraph' && block.content.size === 0;
|
|
114
|
+
let blockStart;
|
|
115
|
+
let blockEnd;
|
|
116
|
+
let slice;
|
|
117
|
+
if (isEmptyParagraph) {
|
|
118
|
+
blockStart = rp.before(topD);
|
|
119
|
+
blockEnd = rp.after(topD);
|
|
120
|
+
slice = new Slice(Fragment.from([table]), 0, 0);
|
|
121
|
+
}
|
|
122
|
+
else {
|
|
123
|
+
const sibling = rp.after(topD);
|
|
124
|
+
blockStart = sibling;
|
|
125
|
+
blockEnd = sibling;
|
|
126
|
+
slice = new Slice(Fragment.from([table]), 0, 0);
|
|
127
|
+
}
|
|
128
|
+
const tr = state.tr.step(new ReplaceStep(blockStart, blockEnd, slice));
|
|
129
|
+
// Cursor lands in first cell. The table [open] is at `blockStart`.
|
|
130
|
+
// Layout inside table: table[open]=blockStart, row[open]=blockStart+1,
|
|
131
|
+
// cell[open]=blockStart+2, paragraph[open]=blockStart+3, content=blockStart+4.
|
|
132
|
+
setOverride(tr, blockStart + 4);
|
|
133
|
+
dispatch(tr);
|
|
134
|
+
return true;
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
/** Insert a fresh empty row above the cursor's row. */
|
|
138
|
+
export function addRowAbove() {
|
|
139
|
+
return (state, dispatch) => {
|
|
140
|
+
const rp = resolve(state.doc, state.selection.from);
|
|
141
|
+
const ctx = findTableContext(rp);
|
|
142
|
+
if (!ctx)
|
|
143
|
+
return false;
|
|
144
|
+
if (!dispatch)
|
|
145
|
+
return true;
|
|
146
|
+
return spliceRow(state, dispatch, rp, ctx, ctx.rowIdx, 'above');
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
/** Insert a fresh empty row below the cursor's row. */
|
|
150
|
+
export function addRowBelow() {
|
|
151
|
+
return (state, dispatch) => {
|
|
152
|
+
const rp = resolve(state.doc, state.selection.from);
|
|
153
|
+
const ctx = findTableContext(rp);
|
|
154
|
+
if (!ctx)
|
|
155
|
+
return false;
|
|
156
|
+
if (!dispatch)
|
|
157
|
+
return true;
|
|
158
|
+
return spliceRow(state, dispatch, rp, ctx, ctx.rowIdx + 1, 'below');
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
function spliceRow(state, dispatch, _rp, ctx, insertAt, direction) {
|
|
162
|
+
const cols = rowCellCount(ctx.table.content.child(ctx.rowIdx));
|
|
163
|
+
const newRow = makeEmptyRow(state.schema, cols);
|
|
164
|
+
const newRows = [];
|
|
165
|
+
for (let i = 0; i < ctx.table.content.childCount; i++) {
|
|
166
|
+
if (i === insertAt)
|
|
167
|
+
newRows.push(newRow);
|
|
168
|
+
newRows.push(ctx.table.content.child(i));
|
|
169
|
+
}
|
|
170
|
+
if (insertAt === ctx.table.content.childCount)
|
|
171
|
+
newRows.push(newRow);
|
|
172
|
+
const newTable = state.schema.node('table', ctx.table.attrs, newRows);
|
|
173
|
+
const slice = new Slice(Fragment.from([newTable]), 0, 0);
|
|
174
|
+
const tableEnd = ctx.tableStart + ctx.table.nodeSize;
|
|
175
|
+
const tr = state.tr.step(new ReplaceStep(ctx.tableStart, tableEnd, slice));
|
|
176
|
+
// Cursor stays where it was. When inserting above, the cursor's row shifted
|
|
177
|
+
// down by one row's worth of positions; when inserting below, the cursor
|
|
178
|
+
// is unaffected.
|
|
179
|
+
let cursor = state.selection.from;
|
|
180
|
+
if (direction === 'above') {
|
|
181
|
+
cursor += newRow.nodeSize;
|
|
182
|
+
}
|
|
183
|
+
setOverride(tr, cursor);
|
|
184
|
+
dispatch(tr);
|
|
185
|
+
return true;
|
|
186
|
+
}
|
|
187
|
+
/** Insert a fresh empty cell at the cursor's column index in every row. */
|
|
188
|
+
export function addColumnLeft() {
|
|
189
|
+
return (state, dispatch) => {
|
|
190
|
+
const rp = resolve(state.doc, state.selection.from);
|
|
191
|
+
const ctx = findTableContext(rp);
|
|
192
|
+
if (!ctx)
|
|
193
|
+
return false;
|
|
194
|
+
if (!dispatch)
|
|
195
|
+
return true;
|
|
196
|
+
return spliceColumn(state, dispatch, ctx, ctx.colIdx, state.selection.from, 'left');
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
/** Insert a fresh empty cell to the right of the cursor's column in every row. */
|
|
200
|
+
export function addColumnRight() {
|
|
201
|
+
return (state, dispatch) => {
|
|
202
|
+
const rp = resolve(state.doc, state.selection.from);
|
|
203
|
+
const ctx = findTableContext(rp);
|
|
204
|
+
if (!ctx)
|
|
205
|
+
return false;
|
|
206
|
+
if (!dispatch)
|
|
207
|
+
return true;
|
|
208
|
+
return spliceColumn(state, dispatch, ctx, ctx.colIdx + 1, state.selection.from, 'right');
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
function spliceColumn(state, dispatch, ctx, insertAt, cursor, direction) {
|
|
212
|
+
const newRows = [];
|
|
213
|
+
for (let r = 0; r < ctx.table.content.childCount; r++) {
|
|
214
|
+
const row = ctx.table.content.child(r);
|
|
215
|
+
const newCells = [];
|
|
216
|
+
for (let c = 0; c < row.content.childCount; c++) {
|
|
217
|
+
if (c === insertAt)
|
|
218
|
+
newCells.push(makeEmptyCell(state.schema));
|
|
219
|
+
newCells.push(row.content.child(c));
|
|
220
|
+
}
|
|
221
|
+
if (insertAt === row.content.childCount)
|
|
222
|
+
newCells.push(makeEmptyCell(state.schema));
|
|
223
|
+
newRows.push(state.schema.node('table_row', row.attrs, newCells));
|
|
224
|
+
}
|
|
225
|
+
const newTable = state.schema.node('table', ctx.table.attrs, newRows);
|
|
226
|
+
const slice = new Slice(Fragment.from([newTable]), 0, 0);
|
|
227
|
+
const tableEnd = ctx.tableStart + ctx.table.nodeSize;
|
|
228
|
+
const tr = state.tr.step(new ReplaceStep(ctx.tableStart, tableEnd, slice));
|
|
229
|
+
// The new cell has size 4 (table_cell[open], paragraph[open], paragraph[close], cell[close]).
|
|
230
|
+
// If inserted at-or-before the cursor's column, the cursor shifts by that size.
|
|
231
|
+
const newCellSize = makeEmptyCell(state.schema).nodeSize;
|
|
232
|
+
let newCursor = cursor;
|
|
233
|
+
if (direction === 'left') {
|
|
234
|
+
// insertAt === ctx.colIdx; the new cell sits before each row's existing
|
|
235
|
+
// cell at that index, so the cursor's cell shifts by newCellSize.
|
|
236
|
+
newCursor += newCellSize;
|
|
237
|
+
}
|
|
238
|
+
setOverride(tr, newCursor);
|
|
239
|
+
dispatch(tr);
|
|
240
|
+
return true;
|
|
241
|
+
}
|
|
242
|
+
/**
|
|
243
|
+
* Remove the row containing the cursor. Cursor lands in the next or previous
|
|
244
|
+
* row's same-column cell. If it was the only row, the whole table is replaced
|
|
245
|
+
* by an empty paragraph (cursor inside).
|
|
246
|
+
*/
|
|
247
|
+
export function deleteRow() {
|
|
248
|
+
return (state, dispatch) => {
|
|
249
|
+
const rp = resolve(state.doc, state.selection.from);
|
|
250
|
+
const ctx = findTableContext(rp);
|
|
251
|
+
if (!ctx)
|
|
252
|
+
return false;
|
|
253
|
+
if (!dispatch)
|
|
254
|
+
return true;
|
|
255
|
+
// Single-row table → drop the whole table.
|
|
256
|
+
if (ctx.table.content.childCount === 1) {
|
|
257
|
+
return replaceTableWithEmptyParagraph(state, dispatch, ctx);
|
|
258
|
+
}
|
|
259
|
+
const newRows = [];
|
|
260
|
+
for (let r = 0; r < ctx.table.content.childCount; r++) {
|
|
261
|
+
if (r === ctx.rowIdx)
|
|
262
|
+
continue;
|
|
263
|
+
newRows.push(ctx.table.content.child(r));
|
|
264
|
+
}
|
|
265
|
+
const newTable = state.schema.node('table', ctx.table.attrs, newRows);
|
|
266
|
+
const slice = new Slice(Fragment.from([newTable]), 0, 0);
|
|
267
|
+
const tableEnd = ctx.tableStart + ctx.table.nodeSize;
|
|
268
|
+
const tr = state.tr.step(new ReplaceStep(ctx.tableStart, tableEnd, slice));
|
|
269
|
+
// Cursor target: same column in the next row (or previous if this was the
|
|
270
|
+
// last). Compute absolute position of that cell's first inline pos in the
|
|
271
|
+
// NEW table.
|
|
272
|
+
const targetRowIdx = ctx.rowIdx < newRows.length ? ctx.rowIdx : newRows.length - 1;
|
|
273
|
+
const cursor = firstInlineOfCellAt(ctx.tableStart, newTable, targetRowIdx, ctx.colIdx);
|
|
274
|
+
setOverride(tr, cursor);
|
|
275
|
+
dispatch(tr);
|
|
276
|
+
return true;
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
/**
|
|
280
|
+
* Remove the column containing the cursor from every row. Cursor lands in the
|
|
281
|
+
* previous column or the next if it was the first. If it was the only column,
|
|
282
|
+
* the whole table is replaced by an empty paragraph.
|
|
283
|
+
*/
|
|
284
|
+
export function deleteColumn() {
|
|
285
|
+
return (state, dispatch) => {
|
|
286
|
+
const rp = resolve(state.doc, state.selection.from);
|
|
287
|
+
const ctx = findTableContext(rp);
|
|
288
|
+
if (!ctx)
|
|
289
|
+
return false;
|
|
290
|
+
if (!dispatch)
|
|
291
|
+
return true;
|
|
292
|
+
const firstRow = ctx.table.content.child(0);
|
|
293
|
+
if (firstRow.content.childCount === 1) {
|
|
294
|
+
return replaceTableWithEmptyParagraph(state, dispatch, ctx);
|
|
295
|
+
}
|
|
296
|
+
const newRows = [];
|
|
297
|
+
for (let r = 0; r < ctx.table.content.childCount; r++) {
|
|
298
|
+
const row = ctx.table.content.child(r);
|
|
299
|
+
const cells = [];
|
|
300
|
+
for (let c = 0; c < row.content.childCount; c++) {
|
|
301
|
+
if (c === ctx.colIdx)
|
|
302
|
+
continue;
|
|
303
|
+
cells.push(row.content.child(c));
|
|
304
|
+
}
|
|
305
|
+
newRows.push(state.schema.node('table_row', row.attrs, cells));
|
|
306
|
+
}
|
|
307
|
+
const newTable = state.schema.node('table', ctx.table.attrs, newRows);
|
|
308
|
+
const slice = new Slice(Fragment.from([newTable]), 0, 0);
|
|
309
|
+
const tableEnd = ctx.tableStart + ctx.table.nodeSize;
|
|
310
|
+
const tr = state.tr.step(new ReplaceStep(ctx.tableStart, tableEnd, slice));
|
|
311
|
+
const newColCount = newRows[0].content.childCount;
|
|
312
|
+
const targetColIdx = ctx.colIdx > 0 ? ctx.colIdx - 1 : 0;
|
|
313
|
+
const clampedCol = Math.min(targetColIdx, newColCount - 1);
|
|
314
|
+
const cursor = firstInlineOfCellAt(ctx.tableStart, newTable, ctx.rowIdx, clampedCol);
|
|
315
|
+
setOverride(tr, cursor);
|
|
316
|
+
dispatch(tr);
|
|
317
|
+
return true;
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
/**
|
|
321
|
+
* Remove the whole table containing the cursor. The table is replaced by an
|
|
322
|
+
* empty paragraph; cursor lands inside.
|
|
323
|
+
*/
|
|
324
|
+
export function deleteTable() {
|
|
325
|
+
return (state, dispatch) => {
|
|
326
|
+
const rp = resolve(state.doc, state.selection.from);
|
|
327
|
+
const ctx = findTableContext(rp);
|
|
328
|
+
if (!ctx)
|
|
329
|
+
return false;
|
|
330
|
+
if (!dispatch)
|
|
331
|
+
return true;
|
|
332
|
+
return replaceTableWithEmptyParagraph(state, dispatch, ctx);
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
function replaceTableWithEmptyParagraph(state, dispatch, ctx) {
|
|
336
|
+
const paragraphType = state.schema.nodes.paragraph;
|
|
337
|
+
if (!paragraphType)
|
|
338
|
+
return false;
|
|
339
|
+
const para = state.schema.node('paragraph', null);
|
|
340
|
+
const slice = new Slice(Fragment.from([para]), 0, 0);
|
|
341
|
+
const tableEnd = ctx.tableStart + ctx.table.nodeSize;
|
|
342
|
+
const tr = state.tr.step(new ReplaceStep(ctx.tableStart, tableEnd, slice));
|
|
343
|
+
// Cursor inside the new paragraph: ctx.tableStart points at the [open] of
|
|
344
|
+
// the new paragraph; content starts one position later.
|
|
345
|
+
setOverride(tr, ctx.tableStart + 1);
|
|
346
|
+
dispatch(tr);
|
|
347
|
+
return true;
|
|
348
|
+
}
|
|
349
|
+
/**
|
|
350
|
+
* Move the selection to the first inline position of the next cell in
|
|
351
|
+
* document order. If the cursor is in the last cell of the last row, append
|
|
352
|
+
* a fresh empty row and place the cursor in its first cell — done in a
|
|
353
|
+
* single transaction so undo collapses cleanly and the selection override
|
|
354
|
+
* lines up with the new structure.
|
|
355
|
+
*
|
|
356
|
+
* Returns false when the cursor is not inside any table cell (so the keymap
|
|
357
|
+
* can fall through).
|
|
358
|
+
*/
|
|
359
|
+
export function tabToNextCell() {
|
|
360
|
+
return (state, dispatch) => {
|
|
361
|
+
const rp = resolve(state.doc, state.selection.from);
|
|
362
|
+
const ctx = findTableContext(rp);
|
|
363
|
+
if (!ctx)
|
|
364
|
+
return false;
|
|
365
|
+
const row = ctx.table.content.child(ctx.rowIdx);
|
|
366
|
+
const cols = rowCellCount(row);
|
|
367
|
+
const rowCount = ctx.table.content.childCount;
|
|
368
|
+
const isLastCellInRow = ctx.colIdx === cols - 1;
|
|
369
|
+
const isLastRow = ctx.rowIdx === rowCount - 1;
|
|
370
|
+
if (!dispatch)
|
|
371
|
+
return true;
|
|
372
|
+
if (!isLastCellInRow) {
|
|
373
|
+
// Move to next cell in the same row.
|
|
374
|
+
const target = firstInlineOfCellAt(ctx.tableStart, ctx.table, ctx.rowIdx, ctx.colIdx + 1);
|
|
375
|
+
const tr = state.tr;
|
|
376
|
+
setOverride(tr, target);
|
|
377
|
+
dispatch(tr);
|
|
378
|
+
return true;
|
|
379
|
+
}
|
|
380
|
+
if (!isLastRow) {
|
|
381
|
+
// Move to first cell of the next row.
|
|
382
|
+
const target = firstInlineOfCellAt(ctx.tableStart, ctx.table, ctx.rowIdx + 1, 0);
|
|
383
|
+
const tr = state.tr;
|
|
384
|
+
setOverride(tr, target);
|
|
385
|
+
dispatch(tr);
|
|
386
|
+
return true;
|
|
387
|
+
}
|
|
388
|
+
// Last cell of last row → append a new empty row in a single transaction
|
|
389
|
+
// and land the cursor in its first cell.
|
|
390
|
+
const newRow = makeEmptyRow(state.schema, cols);
|
|
391
|
+
const newRows = [];
|
|
392
|
+
for (let i = 0; i < ctx.table.content.childCount; i++) {
|
|
393
|
+
newRows.push(ctx.table.content.child(i));
|
|
394
|
+
}
|
|
395
|
+
newRows.push(newRow);
|
|
396
|
+
const newTable = state.schema.node('table', ctx.table.attrs, newRows);
|
|
397
|
+
const slice = new Slice(Fragment.from([newTable]), 0, 0);
|
|
398
|
+
const tableEnd = ctx.tableStart + ctx.table.nodeSize;
|
|
399
|
+
const tr = state.tr.step(new ReplaceStep(ctx.tableStart, tableEnd, slice));
|
|
400
|
+
const target = firstInlineOfCellAt(ctx.tableStart, newTable, rowCount, 0);
|
|
401
|
+
setOverride(tr, target);
|
|
402
|
+
dispatch(tr);
|
|
403
|
+
return true;
|
|
404
|
+
};
|
|
405
|
+
}
|
|
406
|
+
/**
|
|
407
|
+
* Move the selection to the first inline position of the previous cell.
|
|
408
|
+
* Returns false when the cursor is at the very first cell of the table OR
|
|
409
|
+
* not inside any cell.
|
|
410
|
+
*/
|
|
411
|
+
export function tabToPrevCell() {
|
|
412
|
+
return (state, dispatch) => {
|
|
413
|
+
const rp = resolve(state.doc, state.selection.from);
|
|
414
|
+
const ctx = findTableContext(rp);
|
|
415
|
+
if (!ctx)
|
|
416
|
+
return false;
|
|
417
|
+
const isFirstCellInRow = ctx.colIdx === 0;
|
|
418
|
+
const isFirstRow = ctx.rowIdx === 0;
|
|
419
|
+
if (isFirstCellInRow && isFirstRow)
|
|
420
|
+
return false;
|
|
421
|
+
if (!dispatch)
|
|
422
|
+
return true;
|
|
423
|
+
if (!isFirstCellInRow) {
|
|
424
|
+
const target = firstInlineOfCellAt(ctx.tableStart, ctx.table, ctx.rowIdx, ctx.colIdx - 1);
|
|
425
|
+
const tr = state.tr;
|
|
426
|
+
setOverride(tr, target);
|
|
427
|
+
dispatch(tr);
|
|
428
|
+
return true;
|
|
429
|
+
}
|
|
430
|
+
// First cell of a non-first row → last cell of previous row.
|
|
431
|
+
const prevRow = ctx.table.content.child(ctx.rowIdx - 1);
|
|
432
|
+
const target = firstInlineOfCellAt(ctx.tableStart, ctx.table, ctx.rowIdx - 1, rowCellCount(prevRow) - 1);
|
|
433
|
+
const tr = state.tr;
|
|
434
|
+
setOverride(tr, target);
|
|
435
|
+
dispatch(tr);
|
|
436
|
+
return true;
|
|
437
|
+
};
|
|
438
|
+
}
|
|
439
|
+
/**
|
|
440
|
+
* Set the `widthPx` attribute on every cell in `columnIndex` of the table
|
|
441
|
+
* whose [open] token sits at `tableStart` in the doc. Dispatches a single
|
|
442
|
+
* transaction containing one AttrStep per affected cell.
|
|
443
|
+
*
|
|
444
|
+
* Used by the column-resize pointer controller in `view/table-resize.ts`.
|
|
445
|
+
*
|
|
446
|
+
* Returns false if there's no table at `tableStart` or `columnIndex` is
|
|
447
|
+
* out of range in any row.
|
|
448
|
+
*/
|
|
449
|
+
export function setCellWidth(tableStart, columnIndex, widthPx) {
|
|
450
|
+
return (state, dispatch) => {
|
|
451
|
+
if (tableStart < 0 || tableStart >= state.doc.content.size)
|
|
452
|
+
return false;
|
|
453
|
+
if (columnIndex < 0)
|
|
454
|
+
return false;
|
|
455
|
+
// Locate the table node whose [open] sits at `tableStart`. The doc tree
|
|
456
|
+
// walk mirrors the AttrStep.apply boundary-resolution logic: we find the
|
|
457
|
+
// child of rp.parent whose offset === rp.parentOffset.
|
|
458
|
+
let rp;
|
|
459
|
+
try {
|
|
460
|
+
rp = resolve(state.doc, tableStart);
|
|
461
|
+
}
|
|
462
|
+
catch {
|
|
463
|
+
return false;
|
|
464
|
+
}
|
|
465
|
+
const parent = rp.parent;
|
|
466
|
+
let offset = 0;
|
|
467
|
+
let table = null;
|
|
468
|
+
for (let i = 0; i < parent.content.childCount; i++) {
|
|
469
|
+
const child = parent.content.child(i);
|
|
470
|
+
if (offset === rp.parentOffset) {
|
|
471
|
+
table = child;
|
|
472
|
+
break;
|
|
473
|
+
}
|
|
474
|
+
offset += child.nodeSize;
|
|
475
|
+
}
|
|
476
|
+
if (!table || table.type.name !== 'table')
|
|
477
|
+
return false;
|
|
478
|
+
// Validate columnIndex is in range for every row, and collect each cell's
|
|
479
|
+
// absolute [open] position in the doc.
|
|
480
|
+
const cellPositions = [];
|
|
481
|
+
// tableStart points at table[open]; row[open] starts at tableStart + 1.
|
|
482
|
+
let pos = tableStart + 1;
|
|
483
|
+
for (let r = 0; r < table.content.childCount; r++) {
|
|
484
|
+
const row = table.content.child(r);
|
|
485
|
+
if (columnIndex >= row.content.childCount)
|
|
486
|
+
return false;
|
|
487
|
+
// pos is row[open]; first cell[open] sits at pos + 1.
|
|
488
|
+
let cellPos = pos + 1;
|
|
489
|
+
for (let c = 0; c < columnIndex; c++) {
|
|
490
|
+
cellPos += row.content.child(c).nodeSize;
|
|
491
|
+
}
|
|
492
|
+
cellPositions.push(cellPos);
|
|
493
|
+
pos += row.nodeSize;
|
|
494
|
+
}
|
|
495
|
+
if (!dispatch)
|
|
496
|
+
return true;
|
|
497
|
+
let tr = state.tr;
|
|
498
|
+
for (const cellPos of cellPositions) {
|
|
499
|
+
tr = tr.step(new AttrStep(cellPos, 'widthPx', widthPx));
|
|
500
|
+
}
|
|
501
|
+
dispatch(tr);
|
|
502
|
+
return true;
|
|
503
|
+
};
|
|
504
|
+
}
|
|
505
|
+
/**
|
|
506
|
+
* Find the model position of the table containing the cell whose [open] sits
|
|
507
|
+
* at `cellPos`, by walking up from the resolved position. Returns the
|
|
508
|
+
* `tableStart` (absolute position of the table [open]) and the table node.
|
|
509
|
+
*/
|
|
510
|
+
function findTableForCellPos(doc, cellPos) {
|
|
511
|
+
// cellPos points at the cell's [open]; resolving cellPos + 1 lands us
|
|
512
|
+
// INSIDE the cell. From there, the table is 2 depths up (cell -> row -> table).
|
|
513
|
+
let rp;
|
|
514
|
+
try {
|
|
515
|
+
rp = resolve(doc, cellPos + 1);
|
|
516
|
+
}
|
|
517
|
+
catch {
|
|
518
|
+
return null;
|
|
519
|
+
}
|
|
520
|
+
// Walk up to find the table node.
|
|
521
|
+
for (let d = rp.depth; d >= 1; d--) {
|
|
522
|
+
if (rp.node(d).type.name === 'table') {
|
|
523
|
+
return { tableStart: rp.before(d), table: rp.node(d) };
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
return null;
|
|
527
|
+
}
|
|
528
|
+
/**
|
|
529
|
+
* Merge the cells in the current CellSelection into a single cell. The merged
|
|
530
|
+
* master is the top-left cell of the bounding rectangle; its content becomes
|
|
531
|
+
* the concatenation of every selected cell's block content (top-left first,
|
|
532
|
+
* then left-to-right, top-to-bottom). Spans are set so the master covers the
|
|
533
|
+
* original rectangle.
|
|
534
|
+
*
|
|
535
|
+
* Returns false when:
|
|
536
|
+
* - state.selection is not a CellSelection.
|
|
537
|
+
* - the bounding rectangle would orphan a spanned cell at its edge
|
|
538
|
+
* (a cell's master is inside the rect but the cell extends outside,
|
|
539
|
+
* OR a cell straddles the rect boundary).
|
|
540
|
+
* - the rectangle is 1×1 (nothing to merge).
|
|
541
|
+
*
|
|
542
|
+
* After dispatch, the model selection becomes a TextSelection at the first
|
|
543
|
+
* inline position of the merged master cell.
|
|
544
|
+
*/
|
|
545
|
+
export function mergeCells() {
|
|
546
|
+
return (state, dispatch) => {
|
|
547
|
+
const sel = state.selection;
|
|
548
|
+
if (!(sel instanceof CellSelection))
|
|
549
|
+
return false;
|
|
550
|
+
// 1) Locate the containing table via the anchor cell's position.
|
|
551
|
+
const located = findTableForCellPos(state.doc, sel.anchorCell);
|
|
552
|
+
if (!located)
|
|
553
|
+
return false;
|
|
554
|
+
const { tableStart, table } = located;
|
|
555
|
+
// 2) Build the grid.
|
|
556
|
+
const grid = tableGrid(table, tableStart);
|
|
557
|
+
// 3) Map the two corner cell positions to (row, col).
|
|
558
|
+
const anchorCell = grid.byPos(sel.anchorCell);
|
|
559
|
+
const headCell = grid.byPos(sel.headCell);
|
|
560
|
+
if (!anchorCell || !headCell)
|
|
561
|
+
return false;
|
|
562
|
+
// 4) Compute the bounding rectangle, EXTENDING it to include any spanned
|
|
563
|
+
// cells whose masters are inside the rect but whose spans push beyond.
|
|
564
|
+
let r0 = Math.min(anchorCell.row, headCell.row);
|
|
565
|
+
let c0 = Math.min(anchorCell.col, headCell.col);
|
|
566
|
+
let r1 = Math.max(anchorCell.row + anchorCell.rowspan - 1, headCell.row + headCell.rowspan - 1);
|
|
567
|
+
let c1 = Math.max(anchorCell.col + anchorCell.colspan - 1, headCell.col + headCell.colspan - 1);
|
|
568
|
+
// 4b) Iteratively widen the rect to include any master whose rectangle
|
|
569
|
+
// overlaps the current rect (covers the "extends beyond" case).
|
|
570
|
+
let changed = true;
|
|
571
|
+
while (changed) {
|
|
572
|
+
changed = false;
|
|
573
|
+
for (const c of grid.cells) {
|
|
574
|
+
const cellR1 = c.row + c.rowspan - 1;
|
|
575
|
+
const cellC1 = c.col + c.colspan - 1;
|
|
576
|
+
const overlaps = c.row <= r1 && cellR1 >= r0 && c.col <= c1 && cellC1 >= c0;
|
|
577
|
+
if (!overlaps)
|
|
578
|
+
continue;
|
|
579
|
+
const intrudes = c.row < r0 || c.col < c0 || cellR1 > r1 || cellC1 > c1;
|
|
580
|
+
if (!intrudes)
|
|
581
|
+
continue;
|
|
582
|
+
// The cell's master is either inside the rect (extending past) or
|
|
583
|
+
// outside the rect (straddling). In the second case we cannot merge
|
|
584
|
+
// without splitting the foreign cell.
|
|
585
|
+
const masterInside = c.row >= r0 && c.col >= c0 && c.row <= r1 && c.col <= c1;
|
|
586
|
+
if (!masterInside) {
|
|
587
|
+
// Straddling — would orphan a span.
|
|
588
|
+
return false;
|
|
589
|
+
}
|
|
590
|
+
// Master inside but extends past: widen the rect to swallow it.
|
|
591
|
+
if (cellR1 > r1) {
|
|
592
|
+
r1 = cellR1;
|
|
593
|
+
changed = true;
|
|
594
|
+
}
|
|
595
|
+
if (cellC1 > c1) {
|
|
596
|
+
c1 = cellC1;
|
|
597
|
+
changed = true;
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
// 5) Reject 1x1 (covers same cell or a span=1 cell).
|
|
602
|
+
if (r0 === r1 && c0 === c1) {
|
|
603
|
+
// Only reject if the rect collapses to a single grid slot. Note that a
|
|
604
|
+
// single colspan>1 master also has r0===r1 && c0!==c1 → not rejected.
|
|
605
|
+
return false;
|
|
606
|
+
}
|
|
607
|
+
if (!dispatch)
|
|
608
|
+
return true;
|
|
609
|
+
// 6) Collect cells in the rect in row-major (document) order.
|
|
610
|
+
const rectCells = [];
|
|
611
|
+
for (const c of grid.cells) {
|
|
612
|
+
if (c.row >= r0 && c.row <= r1 && c.col >= c0 && c.col <= c1) {
|
|
613
|
+
rectCells.push(c);
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
// Sort by (row, col) — should already match document order for masters.
|
|
617
|
+
rectCells.sort((a, b) => (a.row - b.row) || (a.col - b.col));
|
|
618
|
+
// 7) Build the merged master's content: concatenate every cell's block
|
|
619
|
+
// content (top-left first).
|
|
620
|
+
const mergedBlocks = [];
|
|
621
|
+
for (const c of rectCells) {
|
|
622
|
+
for (let i = 0; i < c.node.content.childCount; i++) {
|
|
623
|
+
mergedBlocks.push(c.node.content.child(i));
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
// Guarantee at least one block (cell content must be non-empty).
|
|
627
|
+
if (mergedBlocks.length === 0) {
|
|
628
|
+
mergedBlocks.push(state.schema.node('paragraph', null));
|
|
629
|
+
}
|
|
630
|
+
const masterOriginal = rectCells[0];
|
|
631
|
+
const newMaster = state.schema.node('table_cell', {
|
|
632
|
+
...masterOriginal.node.attrs,
|
|
633
|
+
colspan: c1 - c0 + 1,
|
|
634
|
+
rowspan: r1 - r0 + 1,
|
|
635
|
+
}, mergedBlocks);
|
|
636
|
+
// 8) Rebuild the table — walk rows; for each row, emit cells. In the
|
|
637
|
+
// rectangle's row range, only emit the master in its starting row;
|
|
638
|
+
// skip every cell whose master is in `rectCells` and which is not the
|
|
639
|
+
// chosen master.
|
|
640
|
+
const mergedCellPositions = new Set();
|
|
641
|
+
for (const c of rectCells)
|
|
642
|
+
mergedCellPositions.add(c.pos);
|
|
643
|
+
// `mergedRow` tracks the row in which we plant the new master.
|
|
644
|
+
const newRows = [];
|
|
645
|
+
for (let r = 0; r < table.content.childCount; r++) {
|
|
646
|
+
const row = table.content.child(r);
|
|
647
|
+
const newCells = [];
|
|
648
|
+
// Walk row's cells; we need their grid coordinates. We can recover them
|
|
649
|
+
// via the grid by matching cell positions.
|
|
650
|
+
// Build per-row cell list from grid.cells filtered by row === r.
|
|
651
|
+
const rowMasters = grid.cells.filter((c) => c.row === r);
|
|
652
|
+
// Sort by col for stable traversal.
|
|
653
|
+
rowMasters.sort((a, b) => a.col - b.col);
|
|
654
|
+
for (const c of rowMasters) {
|
|
655
|
+
if (mergedCellPositions.has(c.pos)) {
|
|
656
|
+
if (c.pos === masterOriginal.pos) {
|
|
657
|
+
// Emit the new merged master in its master row.
|
|
658
|
+
newCells.push(newMaster);
|
|
659
|
+
}
|
|
660
|
+
// else: skip — absorbed into the master.
|
|
661
|
+
continue;
|
|
662
|
+
}
|
|
663
|
+
newCells.push(c.node);
|
|
664
|
+
}
|
|
665
|
+
if (newCells.length === 0) {
|
|
666
|
+
// Row became empty (entire row absorbed by spans of merged cells).
|
|
667
|
+
// This shouldn't happen with our row-master semantics — rows whose
|
|
668
|
+
// cells are all spanned-into from elsewhere keep their non-master
|
|
669
|
+
// grid slots produced by other masters. But guard anyway: ensure
|
|
670
|
+
// table_row content (cell+) is satisfied.
|
|
671
|
+
continue;
|
|
672
|
+
}
|
|
673
|
+
newRows.push(state.schema.node('table_row', row.attrs, newCells));
|
|
674
|
+
}
|
|
675
|
+
const newTable = state.schema.node('table', table.attrs, newRows);
|
|
676
|
+
const slice = new Slice(Fragment.from([newTable]), 0, 0);
|
|
677
|
+
const tableEnd = tableStart + table.nodeSize;
|
|
678
|
+
const tr = state.tr.step(new ReplaceStep(tableStart, tableEnd, slice));
|
|
679
|
+
// 9) Cursor at the first inline of the new master.
|
|
680
|
+
// Compute the master's [open] position in the NEW table.
|
|
681
|
+
const newMasterRowIdx = newRows.findIndex((rr) => {
|
|
682
|
+
for (let i = 0; i < rr.content.childCount; i++) {
|
|
683
|
+
if (rr.content.child(i) === newMaster)
|
|
684
|
+
return true;
|
|
685
|
+
}
|
|
686
|
+
return false;
|
|
687
|
+
});
|
|
688
|
+
let masterColIdx = 0;
|
|
689
|
+
if (newMasterRowIdx >= 0) {
|
|
690
|
+
const rr = newRows[newMasterRowIdx];
|
|
691
|
+
for (let i = 0; i < rr.content.childCount; i++) {
|
|
692
|
+
if (rr.content.child(i) === newMaster) {
|
|
693
|
+
masterColIdx = i;
|
|
694
|
+
break;
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
const cursor = firstInlineOfCellAt(tableStart, newTable, Math.max(0, newMasterRowIdx), masterColIdx);
|
|
699
|
+
setOverride(tr, cursor);
|
|
700
|
+
dispatch(tr);
|
|
701
|
+
return true;
|
|
702
|
+
};
|
|
703
|
+
}
|
|
704
|
+
/**
|
|
705
|
+
* If the cursor is inside a cell with colspan > 1 or rowspan > 1, split that
|
|
706
|
+
* cell back into individual 1×1 cells. The original cell keeps its content;
|
|
707
|
+
* the freed grid positions are filled with fresh empty cells (one empty
|
|
708
|
+
* paragraph each).
|
|
709
|
+
*
|
|
710
|
+
* Returns false if cursor isn't in a cell, or the cell already has
|
|
711
|
+
* colspan === 1 AND rowspan === 1.
|
|
712
|
+
*/
|
|
713
|
+
export function splitCell() {
|
|
714
|
+
return (state, dispatch) => {
|
|
715
|
+
const rp = resolve(state.doc, state.selection.from);
|
|
716
|
+
const ctx = findTableContext(rp);
|
|
717
|
+
if (!ctx)
|
|
718
|
+
return false;
|
|
719
|
+
// Find this cell in the grid to read its true (row, col, spans).
|
|
720
|
+
const grid = tableGrid(ctx.table, ctx.tableStart);
|
|
721
|
+
// The cell at the cursor is the rowIdx'th master row in the grid row OR
|
|
722
|
+
// we can look up by walking the row's cells. Simpler: compute the cell's
|
|
723
|
+
// [open] position via the same logic as firstInlineOfCellAt, then byPos.
|
|
724
|
+
let cellOpenPos = ctx.tableStart + 1; // row[0][open]
|
|
725
|
+
for (let r = 0; r < ctx.rowIdx; r++) {
|
|
726
|
+
cellOpenPos += ctx.table.content.child(r).nodeSize;
|
|
727
|
+
}
|
|
728
|
+
cellOpenPos += 1; // step inside the row
|
|
729
|
+
const cursorRow = ctx.table.content.child(ctx.rowIdx);
|
|
730
|
+
for (let c = 0; c < ctx.colIdx; c++) {
|
|
731
|
+
cellOpenPos += cursorRow.content.child(c).nodeSize;
|
|
732
|
+
}
|
|
733
|
+
const cellInfo = grid.byPos(cellOpenPos);
|
|
734
|
+
if (!cellInfo)
|
|
735
|
+
return false;
|
|
736
|
+
if (cellInfo.colspan === 1 && cellInfo.rowspan === 1)
|
|
737
|
+
return false;
|
|
738
|
+
if (!dispatch)
|
|
739
|
+
return true;
|
|
740
|
+
// Build the new table. We need to insert empty cells into the appropriate
|
|
741
|
+
// rows to fill the (rowspan × colspan) rectangle the master used to cover,
|
|
742
|
+
// and reset the master's spans to 1.
|
|
743
|
+
const { row: masterRow, col: masterCol, rowspan, colspan } = cellInfo;
|
|
744
|
+
const r0 = masterRow;
|
|
745
|
+
const r1 = masterRow + rowspan - 1;
|
|
746
|
+
const c0 = masterCol;
|
|
747
|
+
const c1 = masterCol + colspan - 1;
|
|
748
|
+
const newMasterNode = state.schema.node('table_cell', { ...cellInfo.node.attrs, colspan: 1, rowspan: 1 },
|
|
749
|
+
// Preserve content (which must be non-empty per schema).
|
|
750
|
+
cellInfo.node.content);
|
|
751
|
+
// For each row in [r0..r1], we need to assemble a new list of cells. We
|
|
752
|
+
// walk the grid's view of that row and determine, for each column in
|
|
753
|
+
// [0..cols-1], which physical cell to emit (skipping spans that aren't
|
|
754
|
+
// the master's slot).
|
|
755
|
+
const newRows = [];
|
|
756
|
+
for (let r = 0; r < ctx.table.content.childCount; r++) {
|
|
757
|
+
const oldRow = ctx.table.content.child(r);
|
|
758
|
+
// Outside the split rectangle: keep the row untouched.
|
|
759
|
+
if (r < r0 || r > r1) {
|
|
760
|
+
newRows.push(oldRow);
|
|
761
|
+
continue;
|
|
762
|
+
}
|
|
763
|
+
// Inside the rectangle: rebuild the row.
|
|
764
|
+
// Step 1: collect every master that BEGINS in this row (existing).
|
|
765
|
+
const rowMasters = grid.cells.filter((c) => c.row === r);
|
|
766
|
+
rowMasters.sort((a, b) => a.col - b.col);
|
|
767
|
+
// Step 2: build the list of cells for this row by scanning columns.
|
|
768
|
+
const newCells = [];
|
|
769
|
+
let col = 0;
|
|
770
|
+
const cols = grid.cols;
|
|
771
|
+
while (col < cols) {
|
|
772
|
+
// What cell occupies grid slot (r, col)?
|
|
773
|
+
const occ = grid.cellAt(r, col);
|
|
774
|
+
const inSplitRect = col >= c0 && col <= c1;
|
|
775
|
+
if (inSplitRect) {
|
|
776
|
+
if (r === r0 && col === c0) {
|
|
777
|
+
// The master's slot — emit the de-spanned master node.
|
|
778
|
+
newCells.push(newMasterNode);
|
|
779
|
+
}
|
|
780
|
+
else {
|
|
781
|
+
// Freed slot — emit a fresh empty cell.
|
|
782
|
+
newCells.push(makeEmptyCell(state.schema));
|
|
783
|
+
}
|
|
784
|
+
col++;
|
|
785
|
+
continue;
|
|
786
|
+
}
|
|
787
|
+
// Outside the split rect: if a master STARTS at (r, col), emit it.
|
|
788
|
+
// Otherwise we're in a span foreign to the split — but in that case
|
|
789
|
+
// the master begins in a different row's column AND the slot is
|
|
790
|
+
// occupied by a master in this same row only if rowspan=1 already.
|
|
791
|
+
// For correctness we only emit when occ is a master beginning at
|
|
792
|
+
// (r, col).
|
|
793
|
+
if (occ && occ.row === r && occ.col === col) {
|
|
794
|
+
newCells.push(occ.node);
|
|
795
|
+
col += occ.colspan;
|
|
796
|
+
}
|
|
797
|
+
else {
|
|
798
|
+
// Slot owned by a span beginning in another row → no cell to emit
|
|
799
|
+
// in THIS row's child list (HTML row layout).
|
|
800
|
+
col++;
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
// Note: it is possible newCells is empty if every slot in this row is
|
|
804
|
+
// owned by spans from elsewhere AND the split rect spans the entire
|
|
805
|
+
// row. We always include the split rect's columns above, so this row
|
|
806
|
+
// will always emit at least the columns from c0..c1.
|
|
807
|
+
newRows.push(state.schema.node('table_row', oldRow.attrs, newCells));
|
|
808
|
+
}
|
|
809
|
+
const newTable = state.schema.node('table', ctx.table.attrs, newRows);
|
|
810
|
+
const slice = new Slice(Fragment.from([newTable]), 0, 0);
|
|
811
|
+
const tableEnd = ctx.tableStart + ctx.table.nodeSize;
|
|
812
|
+
const tr = state.tr.step(new ReplaceStep(ctx.tableStart, tableEnd, slice));
|
|
813
|
+
// Cursor: first inline of the (still in place) master cell. Find its
|
|
814
|
+
// index in its row.
|
|
815
|
+
const newMasterRow = newRows[r0];
|
|
816
|
+
let newMasterColIdx = 0;
|
|
817
|
+
for (let i = 0; i < newMasterRow.content.childCount; i++) {
|
|
818
|
+
if (newMasterRow.content.child(i) === newMasterNode) {
|
|
819
|
+
newMasterColIdx = i;
|
|
820
|
+
break;
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
const cursor = firstInlineOfCellAt(ctx.tableStart, newTable, r0, newMasterColIdx);
|
|
824
|
+
setOverride(tr, cursor);
|
|
825
|
+
dispatch(tr);
|
|
826
|
+
return true;
|
|
827
|
+
};
|
|
828
|
+
}
|
|
829
|
+
/**
|
|
830
|
+
* Toggle the `header` attr of the table row containing the cursor.
|
|
831
|
+
* Returns false when the cursor isn't inside a table row.
|
|
832
|
+
*
|
|
833
|
+
* The renderer treats header rows specially (either by emitting them
|
|
834
|
+
* inside `<thead>` OR by adding an `is-header` class to the `<tr>`).
|
|
835
|
+
* Either is fine for v0.3c — the toolbar styling on the example apps
|
|
836
|
+
* works against the class.
|
|
837
|
+
*/
|
|
838
|
+
export function toggleHeaderRow() {
|
|
839
|
+
return (state, dispatch) => {
|
|
840
|
+
const rp = resolve(state.doc, state.selection.from);
|
|
841
|
+
// Walk up to find the table_row depth.
|
|
842
|
+
let rowDepth = -1;
|
|
843
|
+
for (let d = rp.depth; d >= 1; d--) {
|
|
844
|
+
if (rp.node(d).type.name === 'table_row') {
|
|
845
|
+
rowDepth = d;
|
|
846
|
+
break;
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
if (rowDepth < 1)
|
|
850
|
+
return false;
|
|
851
|
+
const row = rp.node(rowDepth);
|
|
852
|
+
const current = row.attrs.header ?? false;
|
|
853
|
+
const rowPos = rp.before(rowDepth);
|
|
854
|
+
if (!dispatch)
|
|
855
|
+
return true;
|
|
856
|
+
const tr = state.tr.step(new AttrStep(rowPos, 'header', !current));
|
|
857
|
+
// Preserve the cursor.
|
|
858
|
+
setOverride(tr, state.selection.from);
|
|
859
|
+
dispatch(tr);
|
|
860
|
+
return true;
|
|
861
|
+
};
|
|
862
|
+
}
|
|
863
|
+
/**
|
|
864
|
+
* Compute the absolute position of the first inline position of the cell at
|
|
865
|
+
* (rowIdx, colIdx) within a freshly-constructed table whose [open] sits at
|
|
866
|
+
* `tableStart`.
|
|
867
|
+
*/
|
|
868
|
+
function firstInlineOfCellAt(tableStart, table, rowIdx, colIdx) {
|
|
869
|
+
// tableStart points at the table's [open]; content starts at tableStart+1.
|
|
870
|
+
let pos = tableStart + 1;
|
|
871
|
+
for (let r = 0; r < rowIdx; r++) {
|
|
872
|
+
pos += table.content.child(r).nodeSize;
|
|
873
|
+
}
|
|
874
|
+
// Now `pos` is the [open] of the target row; step inside.
|
|
875
|
+
pos += 1;
|
|
876
|
+
const row = table.content.child(rowIdx);
|
|
877
|
+
for (let c = 0; c < colIdx; c++) {
|
|
878
|
+
pos += row.content.child(c).nodeSize;
|
|
879
|
+
}
|
|
880
|
+
// `pos` is the [open] of the target cell. Step inside the cell, then into
|
|
881
|
+
// the first paragraph's content (one more open token).
|
|
882
|
+
return pos + 2;
|
|
883
|
+
}
|
|
884
|
+
//# sourceMappingURL=table-commands.js.map
|