@quillmark/svelte 0.27.0 → 0.28.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/NOTICE +4 -4
- package/README.md +26 -8
- package/THEMING.md +10 -8
- package/dist/core/address.d.ts +35 -64
- package/dist/core/address.js +35 -55
- package/dist/core/bloom.d.ts +4 -10
- package/dist/core/bloom.js +14 -27
- package/dist/core/codec/blocks.d.ts +41 -43
- package/dist/core/codec/blocks.js +97 -130
- package/dist/core/codec/code.d.ts +7 -10
- package/dist/core/codec/code.js +37 -46
- package/dist/core/codec/decode.d.ts +19 -23
- package/dist/core/codec/decode.js +76 -111
- package/dist/core/codec/encode.d.ts +11 -15
- package/dist/core/codec/encode.js +60 -67
- package/dist/core/codec/field.d.ts +62 -95
- package/dist/core/codec/field.js +107 -179
- package/dist/core/codec/islands.d.ts +33 -41
- package/dist/core/codec/islands.js +76 -86
- package/dist/core/codec/lists.d.ts +17 -26
- package/dist/core/codec/lists.js +117 -157
- package/dist/core/codec/marks.d.ts +4 -3
- package/dist/core/codec/marks.js +8 -3
- package/dist/core/codec/prose.css +84 -46
- package/dist/core/codec/schema.d.ts +18 -23
- package/dist/core/codec/schema.js +84 -76
- package/dist/core/codec/slash.d.ts +42 -48
- package/dist/core/codec/slash.js +0 -0
- package/dist/core/codec/table-copy.d.ts +4 -3
- package/dist/core/codec/table-copy.js +25 -7
- package/dist/core/codec/table-view.d.ts +12 -12
- package/dist/core/codec/table-view.js +460 -383
- package/dist/core/codec/table.d.ts +94 -61
- package/dist/core/codec/table.js +331 -83
- package/dist/core/codec/tasks.d.ts +6 -0
- package/dist/core/codec/tasks.js +30 -3
- package/dist/core/errors.d.ts +15 -30
- package/dist/core/errors.js +2 -6
- package/dist/core/index.js +7 -12
- package/dist/core/lifecycle.d.ts +7 -16
- package/dist/core/lifecycle.js +10 -21
- package/dist/core/rebind.svelte.d.ts +10 -20
- package/dist/core/rebind.svelte.js +15 -27
- package/dist/core/rungs.d.ts +14 -11
- package/dist/core/rungs.js +17 -19
- package/dist/core/teardown.d.ts +4 -6
- package/dist/core/teardown.js +7 -10
- package/dist/core/theme.css +74 -54
- package/dist/index.js +3 -8
- package/dist/preset/app.css +3 -1
- package/dist/preset/index.css +6 -8
- package/dist/preset/recipes.css +12 -15
- package/dist/preset/scale.css +18 -17
- package/dist/preset/shell.css +10 -0
- package/dist/preview/bridge.d.ts +18 -22
- package/dist/preview/bridge.js +29 -58
- package/dist/preview/controller.d.ts +21 -29
- package/dist/preview/controller.js +14 -39
- package/dist/preview/geometry.d.ts +13 -34
- package/dist/preview/geometry.js +13 -34
- package/dist/preview/index.js +4 -16
- package/dist/preview/paint.d.ts +3 -3
- package/dist/preview/paint.js +28 -68
- package/dist/preview/strings.d.ts +1 -1
- package/dist/preview/strings.js +2 -14
- package/dist/preview/zoom.d.ts +18 -23
- package/dist/preview/zoom.js +28 -27
- package/dist/visual/ArrayField.svelte +77 -16
- package/dist/visual/BooleanField.svelte +27 -14
- package/dist/visual/Card.svelte +61 -48
- package/dist/visual/CardControls.svelte +5 -11
- package/dist/visual/DateField.svelte +20 -4
- package/dist/visual/EnumField.svelte +3 -0
- package/dist/visual/FieldHint.svelte +2 -1
- package/dist/visual/FormatPopover.svelte +82 -15
- package/dist/visual/FormatPopover.svelte.d.ts +4 -0
- package/dist/visual/MatrixAdd.svelte +30 -12
- package/dist/visual/MatrixField.svelte +24 -16
- package/dist/visual/NumberField.svelte +7 -0
- package/dist/visual/ObjectField.svelte +4 -4
- package/dist/visual/ProseField.svelte +0 -5
- package/dist/visual/TipsCard.svelte +11 -10
- package/dist/visual/VariantField.svelte +3 -1
- package/dist/visual/VisualEditorInner.svelte +19 -3
- package/dist/visual/controls.css +7 -2
- package/dist/visual/fonts.d.ts +18 -0
- package/dist/visual/fonts.js +59 -0
- package/dist/visual/icons/nodes.d.ts +0 -5
- package/dist/visual/icons/nodes.js +0 -4
- package/dist/visual/props.d.ts +14 -1
- package/dist/visual/strings.d.ts +7 -0
- package/dist/visual/strings.js +5 -2
- package/dist/visual/structure.d.ts +43 -87
- package/dist/visual/structure.js +42 -83
- package/package.json +2 -2
package/dist/core/codec/table.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { core } from '../lifecycle.js';
|
|
2
|
+
import { usvLength } from './decode.js';
|
|
2
3
|
import { contentEdit, pmToContent } from './encode.js';
|
|
3
4
|
import { valueEqual } from './reconcile.js';
|
|
4
5
|
/** The alignments a column can take: the four the content declares, `none` first
|
|
@@ -9,11 +10,9 @@ export function emptyCell() {
|
|
|
9
10
|
return { text: '', marks: [] };
|
|
10
11
|
}
|
|
11
12
|
/**
|
|
12
|
-
* The row index space the chrome and the ops share: **0 is the header**, 1…n the body
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
* so index 0 moves and deletes like any other. The one row the model refuses is the
|
|
16
|
-
* last, there being no table under it.
|
|
13
|
+
* The row index space the chrome and the ops share: **0 is the header**, 1…n the body rows. A row
|
|
14
|
+
* op works {@link allRows}, and whichever row lands at index 0 is the header afterwards. The one
|
|
15
|
+
* row the model refuses is the last, there being no table under it.
|
|
17
16
|
*/
|
|
18
17
|
export function rowCount(props) {
|
|
19
18
|
return props.rows.length + 1;
|
|
@@ -22,8 +21,10 @@ export function rowCount(props) {
|
|
|
22
21
|
export function rowCells(props, r) {
|
|
23
22
|
return r === 0 ? props.header : (props.rows[r - 1] ?? []);
|
|
24
23
|
}
|
|
25
|
-
/**
|
|
26
|
-
*
|
|
24
|
+
/**
|
|
25
|
+
* Whether a row holds no text, telling a row on offer from one taken up (`table-view.ts` §`step`).
|
|
26
|
+
* Text alone: a mark over none is invisible.
|
|
27
|
+
*/
|
|
27
28
|
export function rowEmpty(props, r) {
|
|
28
29
|
return rowCells(props, r).every((cell) => cell.text === '');
|
|
29
30
|
}
|
|
@@ -36,43 +37,144 @@ export function columnCount(props) {
|
|
|
36
37
|
return props.aligns.length;
|
|
37
38
|
}
|
|
38
39
|
/**
|
|
39
|
-
* The rectangularizing constructor: one column count
|
|
40
|
-
* `aligns` and `widths`, padding with empty cells, `none` and auto-fit.
|
|
41
|
-
*
|
|
42
|
-
* gets a rectangle back, and a table is never narrower than one column. The layout
|
|
43
|
-
* keys ride through, each absent at its default as the store writes it.
|
|
40
|
+
* The rectangularizing constructor: one column count (the widest thing present, at least one)
|
|
41
|
+
* across `header`, every row, `aligns` and `widths`, padding with empty cells, `none` and auto-fit.
|
|
42
|
+
* The layout keys ride through, each absent at its default as the store writes it.
|
|
44
43
|
*/
|
|
45
44
|
export function normalizeTable(props) {
|
|
46
45
|
const cols = Math.max(1, props.header.length, props.aligns.length, ...props.rows.map((row) => row.length));
|
|
47
46
|
const fit = (cells) => Array.from({ length: cols }, (_, c) => cells[c] ?? emptyCell());
|
|
48
47
|
const widths = Array.from({ length: cols }, (_, c) => props.widths?.[c] ?? null);
|
|
48
|
+
const [header = [], ...rows] = settleSpans([fit(props.header), ...props.rows.map(fit)], props.head);
|
|
49
49
|
return {
|
|
50
|
-
header
|
|
51
|
-
rows
|
|
50
|
+
header,
|
|
51
|
+
rows,
|
|
52
52
|
aligns: Array.from({ length: cols }, (_, c) => props.aligns[c] ?? 'none'),
|
|
53
53
|
...(widths.some((w) => w !== null) && { widths }),
|
|
54
54
|
...(props.align && { align: props.align }),
|
|
55
|
-
...(props.
|
|
55
|
+
...(props.head !== undefined && props.head !== 1 && { head: props.head })
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
/** How many rows from the top draw as header rows, absent being one. */
|
|
59
|
+
export function headRows(props) {
|
|
60
|
+
return props.head ?? 1;
|
|
61
|
+
}
|
|
62
|
+
const SPAN_KEYS = ['colspan', 'rowspan'];
|
|
63
|
+
/** A span key as the cell holds it: a whole number from 1, else 1. */
|
|
64
|
+
function spanKey(cell, key) {
|
|
65
|
+
const n = cell[key];
|
|
66
|
+
return typeof n === 'number' && Number.isInteger(n) && n >= 1 ? n : 1;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Per row of {@link allRows} and per cell, the extent a drawn cell spans, or `null` for a slot
|
|
70
|
+
* another's span covers: the rule upstream draws and stores by. Cells claim slots in reading order,
|
|
71
|
+
* so an earlier span wins. A span takes the columns to its right, then the rows below, while each
|
|
72
|
+
* slot it would cover holds a cell with no text and no mark that no other span covers, stopping at
|
|
73
|
+
* the grid's edge and at the end of its own section (header rows or body).
|
|
74
|
+
*/
|
|
75
|
+
function spansOf(rows, head) {
|
|
76
|
+
const top = Math.min(head, rows.length);
|
|
77
|
+
const covered = rows.map((row) => row.map(() => false));
|
|
78
|
+
const spans = rows.map((row) => row.map(() => null));
|
|
79
|
+
const free = (r, c) => {
|
|
80
|
+
const cell = rows[r]?.[c];
|
|
81
|
+
return !!cell && cell.text === '' && cell.marks.length === 0 && !covered[r][c];
|
|
82
|
+
};
|
|
83
|
+
for (let r = 0; r < rows.length; r++) {
|
|
84
|
+
const end = r < top ? top : rows.length;
|
|
85
|
+
for (let c = 0; c < rows[r].length; c++) {
|
|
86
|
+
if (covered[r][c])
|
|
87
|
+
continue;
|
|
88
|
+
const cell = rows[r][c];
|
|
89
|
+
let w = 1;
|
|
90
|
+
while (w < spanKey(cell, 'colspan') && free(r, c + w))
|
|
91
|
+
w++;
|
|
92
|
+
let h = 1;
|
|
93
|
+
const rowFree = (y) => Array.from({ length: w }, (_, x) => c + x).every((x) => free(y, x));
|
|
94
|
+
while (h < spanKey(cell, 'rowspan') && r + h < end && rowFree(r + h))
|
|
95
|
+
h++;
|
|
96
|
+
for (let y = r; y < r + h; y++)
|
|
97
|
+
for (let x = c; x < c + w; x++)
|
|
98
|
+
covered[y][x] = true;
|
|
99
|
+
spans[r][c] = { colspan: w, rowspan: h };
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return spans;
|
|
103
|
+
}
|
|
104
|
+
/** {@link spansOf} over a table's rows, header first. */
|
|
105
|
+
export function spanGrid(props) {
|
|
106
|
+
return spansOf(allRows(props), headRows(props));
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* The drawn cell whose span covers `(r, c)`; the slot itself where nothing covers it or it lies
|
|
110
|
+
* outside the grid.
|
|
111
|
+
*/
|
|
112
|
+
export function anchorAt(grid, r, c) {
|
|
113
|
+
for (let y = r; y >= 0; y--)
|
|
114
|
+
for (let x = c; x >= 0; x--) {
|
|
115
|
+
const span = grid[y]?.[x];
|
|
116
|
+
if (span && y + span.rowspan > r && x + span.colspan > c)
|
|
117
|
+
return { r: y, c: x, span };
|
|
118
|
+
}
|
|
119
|
+
return { r, c, span: { colspan: 1, rowspan: 1 } };
|
|
120
|
+
}
|
|
121
|
+
/** `cell` holding `span`, each key absent at 1. */
|
|
122
|
+
function withSpan(cell, span) {
|
|
123
|
+
const { colspan: _colspan, rowspan: _rowspan, ...rest } = cell;
|
|
124
|
+
return {
|
|
125
|
+
...rest,
|
|
126
|
+
...(span.colspan > 1 && { colspan: span.colspan }),
|
|
127
|
+
...(span.rowspan > 1 && { rowspan: span.rowspan })
|
|
56
128
|
};
|
|
57
129
|
}
|
|
58
130
|
/**
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
|
|
131
|
+
* Each span written as the cell can cover it, absent at 1, and a covered cell's span keys dropped,
|
|
132
|
+
* its alignment kept: what the store settles a table to. A key holding no whole number from 1 stays
|
|
133
|
+
* as written and reads as 1.
|
|
134
|
+
*/
|
|
135
|
+
function settleSpans(rows, head = 1) {
|
|
136
|
+
const spans = spansOf(rows, head);
|
|
137
|
+
return rows.map((row, r) => row.map((cell, c) => {
|
|
138
|
+
const span = spans[r][c];
|
|
139
|
+
if (!span)
|
|
140
|
+
return withSpan(cell, { colspan: 1, rowspan: 1 });
|
|
141
|
+
const settled = { ...cell };
|
|
142
|
+
for (const key of SPAN_KEYS) {
|
|
143
|
+
if (spanKey(cell, key) === 1 && cell[key] !== 1)
|
|
144
|
+
continue;
|
|
145
|
+
if (span[key] > 1)
|
|
146
|
+
settled[key] = span[key];
|
|
147
|
+
else
|
|
148
|
+
delete settled[key];
|
|
149
|
+
}
|
|
150
|
+
return settled;
|
|
151
|
+
}));
|
|
152
|
+
}
|
|
153
|
+
/** Every span in the grid, by its drawn cell. */
|
|
154
|
+
function spanList(props) {
|
|
155
|
+
return spanGrid(props).flatMap((row, r) => row.flatMap((span, c) => span && (span.colspan > 1 || span.rowspan > 1) ? [{ r, c, span }] : []));
|
|
156
|
+
}
|
|
157
|
+
/** `rows` with the span of the cell at `(r, c)` set to `span`. */
|
|
158
|
+
function respan(rows, r, c, span) {
|
|
159
|
+
return rows.map((row, y) => y === r ? row.map((cell, x) => (x === c ? withSpan(cell, span) : cell)) : row);
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* The rows as one list, header first: the space a row op works in, so move and delete treat every
|
|
163
|
+
* row alike. `header` is a separate field because a table always has one; {@link fromRows} splits
|
|
164
|
+
* it back.
|
|
63
165
|
*/
|
|
64
166
|
export function allRows(props) {
|
|
65
167
|
return [props.header, ...props.rows];
|
|
66
168
|
}
|
|
67
|
-
/**
|
|
68
|
-
*
|
|
69
|
-
*
|
|
169
|
+
/**
|
|
170
|
+
* A flat list back into the stored shape: whatever sits at index 0 is the header. No caller passes
|
|
171
|
+
* an empty list; every op that could reach one refuses the last row first.
|
|
172
|
+
*/
|
|
70
173
|
function fromRows(props, rows) {
|
|
71
174
|
const [header = [], ...rest] = rows;
|
|
72
175
|
return normalizeTable({ ...props, header, rows: rest });
|
|
73
176
|
}
|
|
74
|
-
/** A fresh table: a header plus `bodyRows` empty rows, every column unaligned.
|
|
75
|
-
* Growth is cheap (Tab at the last cell appends a row), so the default is small. */
|
|
177
|
+
/** A fresh table: a header plus `bodyRows` empty rows, every column unaligned. */
|
|
76
178
|
export function newTable(cols = 3, bodyRows = 2) {
|
|
77
179
|
return normalizeTable({
|
|
78
180
|
header: Array.from({ length: cols }, emptyCell),
|
|
@@ -93,28 +195,48 @@ export function withCell(props, r, c, cell) {
|
|
|
93
195
|
export function insertRow(props, r) {
|
|
94
196
|
const at = Math.max(0, Math.min(r, props.rows.length));
|
|
95
197
|
const fresh = Array.from({ length: columnCount(props) }, emptyCell);
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
198
|
+
let rows = allRows(props);
|
|
199
|
+
for (const { r: y, c, span } of spanList(props))
|
|
200
|
+
if (y <= at && y + span.rowspan - 1 > at)
|
|
201
|
+
rows = respan(rows, y, c, { ...span, rowspan: span.rowspan + 1 });
|
|
202
|
+
return fromRows(props, [...rows.slice(0, at + 1), fresh, ...rows.slice(at + 1)]);
|
|
100
203
|
}
|
|
101
|
-
/**
|
|
102
|
-
*
|
|
103
|
-
*
|
|
204
|
+
/**
|
|
205
|
+
* Drop row `r`, the header included: the row left at index 0 becomes the header. A no-op at the
|
|
206
|
+
* last row, which is the removal of the table and the island's to answer.
|
|
207
|
+
*/
|
|
104
208
|
export function deleteRow(props, r) {
|
|
105
|
-
|
|
209
|
+
let rows = allRows(props);
|
|
106
210
|
if (rows.length <= 1 || r < 0 || r >= rows.length)
|
|
107
211
|
return normalizeTable(props);
|
|
212
|
+
for (const { r: y, c, span } of spanList(props)) {
|
|
213
|
+
if (y > r || y + span.rowspan <= r)
|
|
214
|
+
continue;
|
|
215
|
+
const shorter = { ...span, rowspan: span.rowspan - 1 };
|
|
216
|
+
if (y < r)
|
|
217
|
+
rows = respan(rows, y, c, shorter);
|
|
218
|
+
else if (span.rowspan > 1) {
|
|
219
|
+
// The drawn cell goes with its row, so the one under it takes its text and the rest of
|
|
220
|
+
// the span.
|
|
221
|
+
const moved = withSpan(rows[y][c], shorter);
|
|
222
|
+
rows = rows.map((row, i) => i === y + 1 ? row.map((cell, x) => (x === c ? moved : cell)) : row);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
108
225
|
return fromRows(props, rows.filter((_, i) => i !== r));
|
|
109
226
|
}
|
|
110
227
|
/** A new empty column after column `c`. */
|
|
111
228
|
export function insertColumn(props, c) {
|
|
112
229
|
const at = Math.max(0, Math.min(c + 1, columnCount(props)));
|
|
113
230
|
const splice = (xs, fresh) => [...xs.slice(0, at), fresh, ...xs.slice(at)];
|
|
231
|
+
let rows = allRows(props);
|
|
232
|
+
for (const { r, c: x, span } of spanList(props))
|
|
233
|
+
if (x < at && x + span.colspan > at)
|
|
234
|
+
rows = respan(rows, r, x, { ...span, colspan: span.colspan + 1 });
|
|
235
|
+
const [header = [], ...body] = rows;
|
|
114
236
|
return normalizeTable({
|
|
115
237
|
...props,
|
|
116
|
-
header: splice(
|
|
117
|
-
rows:
|
|
238
|
+
header: splice(header, emptyCell()),
|
|
239
|
+
rows: body.map((row) => splice(row, emptyCell())),
|
|
118
240
|
aligns: splice(props.aligns, 'none'),
|
|
119
241
|
widths: props.widths && splice(props.widths, null)
|
|
120
242
|
});
|
|
@@ -124,57 +246,109 @@ export function deleteColumn(props, c) {
|
|
|
124
246
|
if (columnCount(props) <= 1)
|
|
125
247
|
return normalizeTable(props);
|
|
126
248
|
const drop = (xs) => xs.filter((_, i) => i !== c);
|
|
249
|
+
let rows = allRows(props);
|
|
250
|
+
for (const { r, c: x, span } of spanList(props)) {
|
|
251
|
+
if (x > c || x + span.colspan <= c)
|
|
252
|
+
continue;
|
|
253
|
+
const narrower = { ...span, colspan: span.colspan - 1 };
|
|
254
|
+
if (x < c)
|
|
255
|
+
rows = respan(rows, r, x, narrower);
|
|
256
|
+
else if (span.colspan > 1) {
|
|
257
|
+
const moved = withSpan(rows[r][x], narrower);
|
|
258
|
+
rows = rows.map((row, i) => i === r ? row.map((cell, y) => (y === x + 1 ? moved : cell)) : row);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
const [header = [], ...body] = rows;
|
|
127
262
|
return normalizeTable({
|
|
128
263
|
...props,
|
|
129
|
-
header: drop(
|
|
130
|
-
rows:
|
|
264
|
+
header: drop(header),
|
|
265
|
+
rows: body.map(drop),
|
|
131
266
|
aligns: drop(props.aligns),
|
|
132
267
|
widths: props.widths && drop(props.widths)
|
|
133
268
|
});
|
|
134
269
|
}
|
|
135
|
-
/**
|
|
136
|
-
|
|
270
|
+
/** `xs` read down its columns. */
|
|
271
|
+
function transpose(xs) {
|
|
272
|
+
return Array.from({ length: xs[0]?.length ?? 0 }, (_, c) => xs.map((row) => row[c]));
|
|
273
|
+
}
|
|
274
|
+
/**
|
|
275
|
+
* `lines` with a span released wherever moving line `from` to `to` would leave the lines it covers
|
|
276
|
+
* out of order or apart. Each span names its drawn cell by `line` and its place `at` along it, and
|
|
277
|
+
* the `length` it runs across the lines under `key`; a span whose lines keep their order and stay
|
|
278
|
+
* together keeps it.
|
|
279
|
+
*/
|
|
280
|
+
function unspan(lines, spans, key, from, to) {
|
|
281
|
+
const order = move(lines.map((_, i) => i), from, to);
|
|
282
|
+
const place = (i) => order.indexOf(i);
|
|
283
|
+
let out = lines;
|
|
284
|
+
for (const { line, at, length } of spans) {
|
|
285
|
+
const together = Array.from({ length: length - 1 }, (_, k) => line + k).every((i) => place(i + 1) === place(i) + 1);
|
|
286
|
+
if (together)
|
|
287
|
+
continue;
|
|
288
|
+
out = out.map((cells, i) => i === line
|
|
289
|
+
? cells.map((cell, x) => {
|
|
290
|
+
if (x !== at)
|
|
291
|
+
return cell;
|
|
292
|
+
const { [key]: _span, ...rest } = cell;
|
|
293
|
+
return rest;
|
|
294
|
+
})
|
|
295
|
+
: cells);
|
|
296
|
+
}
|
|
297
|
+
return out;
|
|
298
|
+
}
|
|
299
|
+
/**
|
|
300
|
+
* One element moved within an array, out of place: the whole of what both move ops do to their
|
|
301
|
+
* axis.
|
|
302
|
+
*/
|
|
137
303
|
function move(xs, from, to) {
|
|
138
304
|
const next = [...xs];
|
|
139
305
|
const [moved] = next.splice(from, 1);
|
|
140
306
|
next.splice(to, 0, moved);
|
|
141
307
|
return next;
|
|
142
308
|
}
|
|
143
|
-
/**
|
|
144
|
-
*
|
|
145
|
-
*
|
|
146
|
-
|
|
309
|
+
/**
|
|
310
|
+
* Row `r` moved by `by` places, clamped: a drag past the rectangle is a no-op, not an error. The
|
|
311
|
+
* header travels like any other row; being the header is holding index 0.
|
|
312
|
+
*/
|
|
147
313
|
export function moveRow(props, r, by) {
|
|
148
314
|
const rows = allRows(props);
|
|
149
315
|
const to = Math.max(0, Math.min(r + by, rows.length - 1));
|
|
150
316
|
if (r === to || r < 0 || r >= rows.length)
|
|
151
317
|
return normalizeTable(props);
|
|
152
|
-
|
|
318
|
+
const spans = spanList(props).map(({ r: line, c: at, span }) => ({
|
|
319
|
+
line,
|
|
320
|
+
at,
|
|
321
|
+
length: span.rowspan
|
|
322
|
+
}));
|
|
323
|
+
return fromRows(props, move(unspan(rows, spans, 'rowspan', r, to), r, to));
|
|
153
324
|
}
|
|
154
|
-
/**
|
|
155
|
-
*
|
|
156
|
-
*
|
|
325
|
+
/**
|
|
326
|
+
* Column `c` moved by `by` places, clamped. `aligns` and `widths` travel with the column, being its
|
|
327
|
+
* properties.
|
|
328
|
+
*/
|
|
157
329
|
export function moveColumn(props, c, by) {
|
|
158
330
|
const to = Math.max(0, Math.min(c + by, columnCount(props) - 1));
|
|
159
331
|
if (c === to || c < 0 || c >= columnCount(props))
|
|
160
332
|
return normalizeTable(props);
|
|
161
333
|
const shift = (xs) => move(xs, c, to);
|
|
334
|
+
const spans = spanList(props).map(({ r: at, c: line, span }) => ({
|
|
335
|
+
line,
|
|
336
|
+
at,
|
|
337
|
+
length: span.colspan
|
|
338
|
+
}));
|
|
339
|
+
const [header = [], ...body] = transpose(unspan(transpose(allRows(props)), spans, 'colspan', c, to));
|
|
162
340
|
return normalizeTable({
|
|
163
341
|
...props,
|
|
164
|
-
header: shift(
|
|
165
|
-
rows:
|
|
342
|
+
header: shift(header),
|
|
343
|
+
rows: body.map(shift),
|
|
166
344
|
aligns: shift(props.aligns),
|
|
167
345
|
widths: props.widths && shift(props.widths)
|
|
168
346
|
});
|
|
169
347
|
}
|
|
170
348
|
/**
|
|
171
|
-
* Every cell in the inclusive rectangle emptied, alignment kept
|
|
172
|
-
*
|
|
173
|
-
*
|
|
174
|
-
* reaches here is a proper sub-rectangle and the shape it leaves is the shape it found.
|
|
175
|
-
*
|
|
176
|
-
* A cell keeps its own alignment, and a column its alignment and width: what a clear
|
|
177
|
-
* empties is text.
|
|
349
|
+
* Every cell in the inclusive rectangle emptied, alignment kept (a cell its own, a column its
|
|
350
|
+
* alignment and width): a delete over a block covering no whole rank. A rectangle spanning one axis
|
|
351
|
+
* deletes ranks and one spanning both is the table, so what reaches here is a proper sub-rectangle.
|
|
178
352
|
*/
|
|
179
353
|
export function clearCells(props, r0, c0, r1, c1) {
|
|
180
354
|
const blank = (cells, r) => cells.map((cell, c) => r >= r0 && r <= r1 && c >= c0 && c <= c1 ? { ...cell, text: '', marks: [] } : cell);
|
|
@@ -184,8 +358,10 @@ export function clearCells(props, r0, c0, r1, c1) {
|
|
|
184
358
|
rows: props.rows.map((row, i) => blank(row, i + 1))
|
|
185
359
|
});
|
|
186
360
|
}
|
|
187
|
-
/**
|
|
188
|
-
*
|
|
361
|
+
/**
|
|
362
|
+
* Set column `c`'s alignment and clear its cells' own `align`, which would outrank it
|
|
363
|
+
* (`table-view.ts`).
|
|
364
|
+
*/
|
|
189
365
|
export function setAlign(props, c, align) {
|
|
190
366
|
const release = (cells) => cells.map((cell, i) => {
|
|
191
367
|
if (i !== c || cell.align === undefined)
|
|
@@ -200,21 +376,96 @@ export function setAlign(props, c, align) {
|
|
|
200
376
|
aligns: props.aligns.map((a, i) => (i === c ? align : a))
|
|
201
377
|
});
|
|
202
378
|
}
|
|
203
|
-
/** Draw the
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
return normalizeTable({ ...props, headless: headless || undefined });
|
|
379
|
+
/** Draw the top `head` rows as header rows (`table-view.ts`). */
|
|
380
|
+
export function setHead(props, head) {
|
|
381
|
+
return normalizeTable({ ...props, head });
|
|
207
382
|
}
|
|
208
|
-
/**
|
|
209
|
-
|
|
383
|
+
/** `block` grown until every drawn cell it touches lies wholly inside it. */
|
|
384
|
+
export function closeBlock(props, block) {
|
|
385
|
+
const grid = spanGrid(props);
|
|
386
|
+
let { r0, c0, r1, c1 } = block;
|
|
387
|
+
for (let grown = true; grown;) {
|
|
388
|
+
grown = false;
|
|
389
|
+
for (let r = r0; r <= r1; r++)
|
|
390
|
+
for (let c = c0; c <= c1; c++) {
|
|
391
|
+
const a = anchorAt(grid, r, c);
|
|
392
|
+
const next = {
|
|
393
|
+
r0: Math.min(r0, a.r),
|
|
394
|
+
c0: Math.min(c0, a.c),
|
|
395
|
+
r1: Math.max(r1, a.r + a.span.rowspan - 1),
|
|
396
|
+
c1: Math.max(c1, a.c + a.span.colspan - 1)
|
|
397
|
+
};
|
|
398
|
+
if (next.r0 !== r0 || next.c0 !== c0 || next.r1 !== r1 || next.c1 !== c1) {
|
|
399
|
+
({ r0, c0, r1, c1 } = next);
|
|
400
|
+
grown = true;
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
return { r0, c0, r1, c1 };
|
|
405
|
+
}
|
|
406
|
+
/**
|
|
407
|
+
* Whether `block` can be one cell: more than one drawn cell, inside the header rows or the body.
|
|
408
|
+
*/
|
|
409
|
+
export function canMerge(props, block) {
|
|
410
|
+
const { r0, c0, r1, c1 } = closeBlock(props, block);
|
|
411
|
+
const head = headRows(props);
|
|
412
|
+
const across = r0 < head && r1 >= head;
|
|
413
|
+
const { span } = anchorAt(spanGrid(props), r0, c0);
|
|
414
|
+
return !across && span.colspan * span.rowspan < (r1 - r0 + 1) * (c1 - c0 + 1);
|
|
415
|
+
}
|
|
416
|
+
/**
|
|
417
|
+
* The block's cells as one, at its top-left: the text of each drawn cell holding any, in reading
|
|
418
|
+
* order, a line apiece, every other slot an empty one the span covers, alignment kept. The top-left
|
|
419
|
+
* cell's alignment is the merged cell's. A block {@link canMerge} refuses comes back as it was.
|
|
420
|
+
*/
|
|
421
|
+
export function mergeCells(props, block) {
|
|
422
|
+
if (!canMerge(props, block))
|
|
423
|
+
return normalizeTable(props);
|
|
424
|
+
const { r0, c0, r1, c1 } = closeBlock(props, block);
|
|
425
|
+
const grid = spanGrid(props);
|
|
426
|
+
const rows = allRows(props);
|
|
427
|
+
let text = '';
|
|
428
|
+
const marks = [];
|
|
429
|
+
for (let r = r0; r <= r1; r++)
|
|
430
|
+
for (let c = c0; c <= c1; c++) {
|
|
431
|
+
const cell = rows[r][c];
|
|
432
|
+
if (!grid[r][c] || cell.text === '')
|
|
433
|
+
continue;
|
|
434
|
+
if (text)
|
|
435
|
+
text += '\n';
|
|
436
|
+
const at = usvLength(text);
|
|
437
|
+
text += cell.text;
|
|
438
|
+
for (const m of cell.marks)
|
|
439
|
+
marks.push({ ...m, start: m.start + at, end: m.end + at });
|
|
440
|
+
}
|
|
441
|
+
const merged = rows.map((row, r) => row.map((cell, c) => {
|
|
442
|
+
if (r < r0 || r > r1 || c < c0 || c > c1)
|
|
443
|
+
return cell;
|
|
444
|
+
const layout = withSpan({ ...cell, text: '', marks: [] }, { colspan: 1, rowspan: 1 });
|
|
445
|
+
if (r !== r0 || c !== c0)
|
|
446
|
+
return layout;
|
|
447
|
+
return withSpan({ ...layout, text, marks }, { colspan: c1 - c0 + 1, rowspan: r1 - r0 + 1 });
|
|
448
|
+
}));
|
|
449
|
+
return fromRows(props, merged);
|
|
450
|
+
}
|
|
451
|
+
/**
|
|
452
|
+
* The merged cell covering `(r, c)` back to the cells under it: its text stays top-left, the rest
|
|
453
|
+
* are the empty cells it covered.
|
|
454
|
+
*/
|
|
455
|
+
export function splitCell(props, r, c) {
|
|
456
|
+
const a = anchorAt(spanGrid(props), r, c);
|
|
457
|
+
return fromRows(props, respan(allRows(props), a.r, a.c, { colspan: 1, rowspan: 1 }));
|
|
458
|
+
}
|
|
459
|
+
/**
|
|
460
|
+
* Set or clear the column weights (`table-view.ts`); `undefined` returns every column to auto-fit.
|
|
461
|
+
*/
|
|
210
462
|
export function setWidths(props, widths) {
|
|
211
463
|
return normalizeTable({ ...props, widths });
|
|
212
464
|
}
|
|
213
465
|
/**
|
|
214
|
-
* Each column's share of the table's width, summing to 1, or `undefined` for a table
|
|
215
|
-
*
|
|
216
|
-
*
|
|
217
|
-
* the weights hold, cannot ask.
|
|
466
|
+
* Each column's share of the table's width, summing to 1, or `undefined` for a table that auto-fits
|
|
467
|
+
* throughout. An auto-fit column beside weighted ones takes the mean weight, since the editor's
|
|
468
|
+
* fixed-layout grid cannot size it to content as the page does.
|
|
218
469
|
*/
|
|
219
470
|
export function columnShares(props) {
|
|
220
471
|
const set = props.widths?.filter((w) => w !== null);
|
|
@@ -227,10 +478,8 @@ export function columnShares(props) {
|
|
|
227
478
|
}
|
|
228
479
|
// ── The cell codec ──────────────────────────────────────────────────────────
|
|
229
480
|
/**
|
|
230
|
-
* A cell as a one-paragraph `Content
|
|
231
|
-
*
|
|
232
|
-
* the one USV `pmToContent` projects a break back to. The cell's marks are that
|
|
233
|
-
* content's marks, both being offsets into the same text.
|
|
481
|
+
* A cell as a one-paragraph `Content` for `decode` under `cellSchema`: each `\n` opens a
|
|
482
|
+
* `continues` line, the hard break it decodes to. The cell's marks are that content's marks.
|
|
234
483
|
*/
|
|
235
484
|
export function cellContent(cell) {
|
|
236
485
|
const breaks = cell.text.split('\n').length - 1;
|
|
@@ -251,14 +500,11 @@ export function cellContent(cell) {
|
|
|
251
500
|
/**
|
|
252
501
|
* The cell a nested PM doc projects, carrying `prior`'s identity anchors through.
|
|
253
502
|
*
|
|
254
|
-
* Anchors inside a cell are
|
|
255
|
-
*
|
|
256
|
-
*
|
|
257
|
-
*
|
|
258
|
-
*
|
|
259
|
-
* anchor through the cell's own delta by the rule the store applies to a field's,
|
|
260
|
-
* and a mark the splice deleted through lands where the splice left it rather than
|
|
261
|
-
* pointing past the text.
|
|
503
|
+
* Anchors inside a cell are preserved verbatim, never minted: they are not in the field plugin's
|
|
504
|
+
* coordinate space (its position map holds one `atom` run for the whole table), so the popover
|
|
505
|
+
* withholds its `anchor` button in a cell. Preserving still means rebasing: `mapMarks` carries each
|
|
506
|
+
* held anchor through the cell's delta by the store's rule, and a mark the splice deleted through
|
|
507
|
+
* lands where the splice left it.
|
|
262
508
|
*/
|
|
263
509
|
export function cellFromDoc(doc, prior) {
|
|
264
510
|
const projected = pmToContent(doc);
|
|
@@ -283,9 +529,10 @@ function cellLayout(cell) {
|
|
|
283
529
|
const { text: _text, marks: _marks, ...layout } = cell;
|
|
284
530
|
return layout;
|
|
285
531
|
}
|
|
286
|
-
/**
|
|
287
|
-
*
|
|
288
|
-
*
|
|
532
|
+
/**
|
|
533
|
+
* Whether two cells are the same value, telling an own edit from an external one (an undo, a
|
|
534
|
+
* re-hydrate) without a flag. Marks compare structurally, as `reconcile.ts` states.
|
|
535
|
+
*/
|
|
289
536
|
export function cellEqual(a, b) {
|
|
290
537
|
return a.text === b.text && valueEqual(a.marks, b.marks);
|
|
291
538
|
}
|
|
@@ -295,6 +542,7 @@ export function shapeEqual(a, b) {
|
|
|
295
542
|
return (a.rows.length === b.rows.length &&
|
|
296
543
|
a.aligns.length === b.aligns.length &&
|
|
297
544
|
a.aligns.every((x, i) => x === b.aligns[i]) &&
|
|
298
|
-
|
|
545
|
+
headRows(a) === headRows(b) &&
|
|
546
|
+
valueEqual(spanGrid(a), spanGrid(b)) &&
|
|
299
547
|
valueEqual(a.widths, b.widths));
|
|
300
548
|
}
|
|
@@ -17,3 +17,9 @@ export declare function taskPlugin(): Plugin;
|
|
|
17
17
|
* Shift-Tab lift keeps it.
|
|
18
18
|
*/
|
|
19
19
|
export declare function splitTaskItem(itemType: NodeType): Command;
|
|
20
|
+
/**
|
|
21
|
+
* `checked` flipped on the nearest task item around the caret, or on a selected task item,
|
|
22
|
+
* as one transaction. It declines outside a task item and inside a fence, where `baseKeymap`'s `exitCode` is the
|
|
23
|
+
* nearer answer to `Mod-Enter`.
|
|
24
|
+
*/
|
|
25
|
+
export declare function toggleTask(itemType: NodeType): Command;
|
package/dist/core/codec/tasks.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
// A task item is a `list_item` whose `checked` is a boolean (CODEC §"Task items"). Its
|
|
2
2
|
// `toDOM` draws a checkbox ahead of the item's blocks, disabled, which is the drawing a
|
|
3
3
|
// held leaf, a copy and `renderContent` take; a leaf that edits mounts the item through
|
|
4
|
-
// `taskItemView`, whose checkbox toggles. A task item
|
|
5
|
-
// paste alone: no shorthand, command or key makes one.
|
|
4
|
+
// `taskItemView`, whose checkbox toggles, as `Mod-Enter` does (`toggleTask`). A task item
|
|
5
|
+
// is minted by source mode and by a paste alone: no shorthand, command or key makes one.
|
|
6
6
|
import { DOMSerializer } from 'prosemirror-model';
|
|
7
|
-
import { Plugin } from 'prosemirror-state';
|
|
7
|
+
import { NodeSelection, Plugin } from 'prosemirror-state';
|
|
8
8
|
import { splitListItem } from 'prosemirror-schema-list';
|
|
9
9
|
const SVG = 'http://www.w3.org/2000/svg';
|
|
10
10
|
/** The id the checkbox names its item's blocks by, fresh per drawing. */
|
|
@@ -138,3 +138,30 @@ export function splitTaskItem(itemType) {
|
|
|
138
138
|
});
|
|
139
139
|
};
|
|
140
140
|
}
|
|
141
|
+
/**
|
|
142
|
+
* `checked` flipped on the nearest task item around the caret, or on a selected task item,
|
|
143
|
+
* as one transaction. It declines outside a task item and inside a fence, where `baseKeymap`'s `exitCode` is the
|
|
144
|
+
* nearer answer to `Mod-Enter`.
|
|
145
|
+
*/
|
|
146
|
+
export function toggleTask(itemType) {
|
|
147
|
+
return (state, dispatch) => {
|
|
148
|
+
const { selection } = state;
|
|
149
|
+
const { $from } = selection;
|
|
150
|
+
if (selection instanceof NodeSelection &&
|
|
151
|
+
selection.node.type === itemType &&
|
|
152
|
+
isTask(selection.node)) {
|
|
153
|
+
dispatch?.(state.tr.setNodeAttribute(selection.from, 'checked', !selection.node.attrs.checked));
|
|
154
|
+
return true;
|
|
155
|
+
}
|
|
156
|
+
if ($from.parent.type.spec.code)
|
|
157
|
+
return false;
|
|
158
|
+
for (let d = $from.depth; d > 0; d--) {
|
|
159
|
+
const node = $from.node(d);
|
|
160
|
+
if (node.type !== itemType || !isTask(node))
|
|
161
|
+
continue;
|
|
162
|
+
dispatch?.(state.tr.setNodeAttribute($from.before(d), 'checked', !node.attrs.checked));
|
|
163
|
+
return true;
|
|
164
|
+
}
|
|
165
|
+
return false;
|
|
166
|
+
};
|
|
167
|
+
}
|