@quillmark/svelte 0.18.0 → 0.20.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 -2
- package/dist/core/codec/blocks.d.ts +11 -2
- package/dist/core/codec/blocks.js +45 -14
- package/dist/core/codec/breaks.d.ts +3 -2
- package/dist/core/codec/breaks.js +3 -2
- package/dist/core/codec/decode.d.ts +7 -3
- package/dist/core/codec/decode.js +34 -8
- package/dist/core/codec/field.d.ts +11 -5
- package/dist/core/codec/field.js +11 -8
- package/dist/core/codec/index.d.ts +1 -1
- package/dist/core/codec/index.js +1 -1
- package/dist/core/codec/islands.d.ts +3 -1
- package/dist/core/codec/islands.js +8 -5
- package/dist/core/codec/markdown-paste.d.ts +9 -0
- package/dist/core/codec/markdown-paste.js +49 -0
- package/dist/core/codec/prose.css +109 -1
- package/dist/core/codec/schema.d.ts +8 -2
- package/dist/core/codec/schema.js +16 -5
- package/dist/core/codec/slash.d.ts +9 -6
- package/dist/core/codec/slash.js +0 -0
- package/dist/core/codec/table-paste.d.ts +7 -0
- package/dist/core/codec/table-paste.js +132 -0
- package/dist/core/codec/table-view.d.ts +12 -0
- package/dist/core/codec/table-view.js +401 -57
- package/dist/core/codec/table.d.ts +38 -16
- package/dist/core/codec/table.js +106 -35
- package/dist/core/theme.css +1 -1
- package/dist/visual/ArrayField.svelte +17 -6
- package/dist/visual/ArrayField.svelte.d.ts +1 -1
- package/dist/visual/Field.svelte +9 -3
- package/dist/visual/Field.svelte.d.ts +1 -1
- package/dist/visual/FieldHint.svelte +62 -10
- package/dist/visual/FormatPopover.svelte +27 -31
- package/dist/visual/FormatPopover.svelte.d.ts +2 -2
- package/dist/visual/MatrixAdd.svelte +13 -132
- package/dist/visual/MatrixAdd.svelte.d.ts +2 -5
- package/dist/visual/MatrixField.svelte +5 -63
- package/dist/visual/MatrixField.svelte.d.ts +0 -3
- package/dist/visual/ObjectField.svelte +8 -4
- package/dist/visual/ObjectField.svelte.d.ts +4 -2
- package/dist/visual/ProseField.svelte +2 -1
- package/dist/visual/ProseValue.svelte +113 -40
- package/dist/visual/ProseValue.svelte.d.ts +8 -4
- package/dist/visual/VisualEditorInner.svelte +22 -22
- package/dist/visual/VisualEditorInner.svelte.d.ts +2 -2
- package/dist/visual/checklists.d.ts +0 -79
- package/dist/visual/checklists.js +0 -164
- package/dist/visual/guidance.d.ts +15 -0
- package/dist/visual/guidance.js +37 -0
- package/dist/visual/leaves.d.ts +27 -1
- package/dist/visual/leaves.js +46 -0
- package/dist/visual/strings.d.ts +3 -8
- package/dist/visual/strings.js +0 -3
- package/dist/visual/structure.js +1 -1
- package/dist/visual/tips.js +1 -1
- package/package.json +2 -2
|
@@ -25,10 +25,11 @@ export declare function cellAt(props: TableProps, r: number, c: number): TableCe
|
|
|
25
25
|
/** The table's column count: the one number `header`, every row and `aligns` share. */
|
|
26
26
|
export declare function columnCount(props: TableProps): number;
|
|
27
27
|
/**
|
|
28
|
-
* The rectangularizing constructor: one column count across `header`, every row
|
|
29
|
-
* `aligns`, padding with empty cells
|
|
30
|
-
* thing present, so a caller that appended to one axis alone still
|
|
31
|
-
* back, and a table is never narrower than one column.
|
|
28
|
+
* The rectangularizing constructor: one column count across `header`, every row,
|
|
29
|
+
* `aligns` and `widths`, padding with empty cells, `none` and auto-fit. The column
|
|
30
|
+
* count is the widest thing present, so a caller that appended to one axis alone still
|
|
31
|
+
* gets a rectangle back, and a table is never narrower than one column. The layout
|
|
32
|
+
* keys ride through, each absent at its default as the store writes it.
|
|
32
33
|
*/
|
|
33
34
|
export declare function normalizeTable(props: TableProps): TableProps;
|
|
34
35
|
/**
|
|
@@ -58,9 +59,9 @@ export declare function deleteColumn(props: TableProps, c: number): TableProps;
|
|
|
58
59
|
* last row. The header travels like any other row, and so does the row that displaces
|
|
59
60
|
* it: being the header is holding index 0, which a move is free to hand over. */
|
|
60
61
|
export declare function moveRow(props: TableProps, r: number, by: number): TableProps;
|
|
61
|
-
/** Column `c` moved by `by` places, clamped. `aligns`
|
|
62
|
-
*
|
|
63
|
-
* column that took the index. */
|
|
62
|
+
/** Column `c` moved by `by` places, clamped. `aligns` and `widths` travel with the
|
|
63
|
+
* column: each is the column's property, so a move that left it behind would retint
|
|
64
|
+
* or resize the column that took the index. */
|
|
64
65
|
export declare function moveColumn(props: TableProps, c: number, by: number): TableProps;
|
|
65
66
|
/**
|
|
66
67
|
* Every cell in the inclusive rectangle emptied, alignment kept: what a delete gesture
|
|
@@ -68,17 +69,38 @@ export declare function moveColumn(props: TableProps, c: number, by: number): Ta
|
|
|
68
69
|
* covers ranks and deletes them, and one spanning both is the table itself, so what
|
|
69
70
|
* reaches here is a proper sub-rectangle and the shape it leaves is the shape it found.
|
|
70
71
|
*
|
|
71
|
-
*
|
|
72
|
+
* A cell keeps its own alignment, and a column its alignment and width: what a clear
|
|
73
|
+
* empties is text.
|
|
72
74
|
*/
|
|
73
75
|
export declare function clearCells(props: TableProps, r0: number, c0: number, r1: number, c1: number): TableProps;
|
|
74
|
-
/** Set column `c`'s alignment: the
|
|
75
|
-
*
|
|
76
|
+
/** Set column `c`'s alignment: what the held column's cluster and Shift+arrow write
|
|
77
|
+
* (`table-view.ts`). */
|
|
76
78
|
export declare function setAlign(props: TableProps, c: number, align: TableAlign): TableProps;
|
|
79
|
+
/** Draw the first row as a header or as a body row: what the header toggle writes
|
|
80
|
+
* (`table-view.ts`). */
|
|
81
|
+
export declare function setHeadless(props: TableProps, headless: boolean): TableProps;
|
|
82
|
+
/** A cell's horizontal alignment, and its vertical one: the keys a cell holds beside
|
|
83
|
+
* its text, each absent at the default the column or the quill supplies. */
|
|
84
|
+
export type CellAlign = NonNullable<TableCell['align']>;
|
|
85
|
+
export type CellValign = NonNullable<TableCell['valign']>;
|
|
86
|
+
/** Every cell in the inclusive rectangle given `key`'s `value`, or cleared of it where
|
|
87
|
+
* `value` is absent, which returns the cell to its column's alignment or the quill's. */
|
|
88
|
+
export declare function setCellLayout<K extends 'align' | 'valign'>(props: TableProps, r0: number, c0: number, r1: number, c1: number, key: K, value: TableCell[K] | undefined): TableProps;
|
|
89
|
+
/** Set or clear the column weights: what a border drag and its double-click write
|
|
90
|
+
* (`table-view.ts`). `undefined` returns every column to auto-fit. */
|
|
91
|
+
export declare function setWidths(props: TableProps, widths: (number | null)[] | undefined): TableProps;
|
|
77
92
|
/**
|
|
78
|
-
*
|
|
79
|
-
*
|
|
80
|
-
*
|
|
81
|
-
*
|
|
93
|
+
* Each column's share of the table's width, summing to 1, or `undefined` for a table
|
|
94
|
+
* that auto-fits throughout. An auto-fit column beside weighted ones takes the mean
|
|
95
|
+
* weight: the page sizes it to its content, which the editor's grid, laid out fixed so
|
|
96
|
+
* the weights hold, cannot ask.
|
|
97
|
+
*/
|
|
98
|
+
export declare function columnShares(props: TableProps): number[] | undefined;
|
|
99
|
+
/**
|
|
100
|
+
* A cell as a one-paragraph `Content`: what `decode` takes under `cellSchema`. Each
|
|
101
|
+
* `\n` in the text opens a `continues` line, which is the hard break it decodes to and
|
|
102
|
+
* the one USV `pmToContent` projects a break back to. The cell's marks are that
|
|
103
|
+
* content's marks, both being offsets into the same text.
|
|
82
104
|
*/
|
|
83
105
|
export declare function cellContent(cell: TableCell): Content;
|
|
84
106
|
/**
|
|
@@ -98,6 +120,6 @@ export declare function cellFromDoc(doc: PMNode, prior: TableCell): TableCell;
|
|
|
98
120
|
* view just produced) from an external one (an undo, a re-hydrate) without a flag.
|
|
99
121
|
* The marks structurally, for the reason `reconcile.ts` states. */
|
|
100
122
|
export declare function cellEqual(a: TableCell, b: TableCell): boolean;
|
|
101
|
-
/** Whether two tables have the same rectangle and
|
|
102
|
-
* a rebuild rather than a per-cell reseed. */
|
|
123
|
+
/** Whether two tables have the same rectangle, alignment, header and widths: the
|
|
124
|
+
* change that forces a rebuild rather than a per-cell reseed. */
|
|
103
125
|
export declare function shapeEqual(a: TableProps, b: TableProps): boolean;
|
package/dist/core/codec/table.js
CHANGED
|
@@ -36,18 +36,23 @@ export function columnCount(props) {
|
|
|
36
36
|
return props.aligns.length;
|
|
37
37
|
}
|
|
38
38
|
/**
|
|
39
|
-
* The rectangularizing constructor: one column count across `header`, every row
|
|
40
|
-
* `aligns`, padding with empty cells
|
|
41
|
-
* thing present, so a caller that appended to one axis alone still
|
|
42
|
-
* back, and a table is never narrower than one column.
|
|
39
|
+
* The rectangularizing constructor: one column count across `header`, every row,
|
|
40
|
+
* `aligns` and `widths`, padding with empty cells, `none` and auto-fit. The column
|
|
41
|
+
* count is the widest thing present, so a caller that appended to one axis alone still
|
|
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.
|
|
43
44
|
*/
|
|
44
45
|
export function normalizeTable(props) {
|
|
45
46
|
const cols = Math.max(1, props.header.length, props.aligns.length, ...props.rows.map((row) => row.length));
|
|
46
47
|
const fit = (cells) => Array.from({ length: cols }, (_, c) => cells[c] ?? emptyCell());
|
|
48
|
+
const widths = Array.from({ length: cols }, (_, c) => props.widths?.[c] ?? null);
|
|
47
49
|
return {
|
|
48
50
|
header: fit(props.header),
|
|
49
51
|
rows: props.rows.map(fit),
|
|
50
|
-
aligns: Array.from({ length: cols }, (_, c) => props.aligns[c] ?? 'none')
|
|
52
|
+
aligns: Array.from({ length: cols }, (_, c) => props.aligns[c] ?? 'none'),
|
|
53
|
+
...(widths.some((w) => w !== null) && { widths }),
|
|
54
|
+
...(props.align && { align: props.align }),
|
|
55
|
+
...(props.headless && { headless: true })
|
|
51
56
|
};
|
|
52
57
|
}
|
|
53
58
|
/**
|
|
@@ -62,9 +67,9 @@ export function allRows(props) {
|
|
|
62
67
|
/** A flat list back into the stored shape: whatever sits at index 0 is the header, by
|
|
63
68
|
* position rather than by having been one. No caller passes an empty list — every op
|
|
64
69
|
* that could reach one refuses the last row first. */
|
|
65
|
-
function fromRows(
|
|
70
|
+
function fromRows(props, rows) {
|
|
66
71
|
const [header = [], ...rest] = rows;
|
|
67
|
-
return normalizeTable({ header, rows: rest
|
|
72
|
+
return normalizeTable({ ...props, header, rows: rest });
|
|
68
73
|
}
|
|
69
74
|
/** A fresh table: a header plus `bodyRows` empty rows, every column unaligned.
|
|
70
75
|
* Growth is cheap (Tab at the last cell appends a row), so the default is small. */
|
|
@@ -79,9 +84,9 @@ export function newTable(cols = 3, bodyRows = 2) {
|
|
|
79
84
|
export function withCell(props, r, c, cell) {
|
|
80
85
|
const replace = (cells) => cells.map((old, i) => (i === c ? cell : old));
|
|
81
86
|
return normalizeTable({
|
|
87
|
+
...props,
|
|
82
88
|
header: r === 0 ? replace(props.header) : props.header,
|
|
83
|
-
rows: props.rows.map((row, i) => (i === r - 1 ? replace(row) : row))
|
|
84
|
-
aligns: props.aligns
|
|
89
|
+
rows: props.rows.map((row, i) => (i === r - 1 ? replace(row) : row))
|
|
85
90
|
});
|
|
86
91
|
}
|
|
87
92
|
/** A new empty body row below row `r` (row 0, the header, opens the first body row). */
|
|
@@ -100,16 +105,18 @@ export function deleteRow(props, r) {
|
|
|
100
105
|
const rows = allRows(props);
|
|
101
106
|
if (rows.length <= 1 || r < 0 || r >= rows.length)
|
|
102
107
|
return normalizeTable(props);
|
|
103
|
-
return fromRows(rows.filter((_, i) => i !== r)
|
|
108
|
+
return fromRows(props, rows.filter((_, i) => i !== r));
|
|
104
109
|
}
|
|
105
110
|
/** A new empty column after column `c`. */
|
|
106
111
|
export function insertColumn(props, c) {
|
|
107
112
|
const at = Math.max(0, Math.min(c + 1, columnCount(props)));
|
|
108
|
-
const splice = (
|
|
113
|
+
const splice = (xs, fresh) => [...xs.slice(0, at), fresh, ...xs.slice(at)];
|
|
109
114
|
return normalizeTable({
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
115
|
+
...props,
|
|
116
|
+
header: splice(props.header, emptyCell()),
|
|
117
|
+
rows: props.rows.map((row) => splice(row, emptyCell())),
|
|
118
|
+
aligns: splice(props.aligns, 'none'),
|
|
119
|
+
widths: props.widths && splice(props.widths, null)
|
|
113
120
|
});
|
|
114
121
|
}
|
|
115
122
|
/** Drop column `c`; a no-op at one column (a table has at least one). */
|
|
@@ -118,9 +125,11 @@ export function deleteColumn(props, c) {
|
|
|
118
125
|
return normalizeTable(props);
|
|
119
126
|
const drop = (xs) => xs.filter((_, i) => i !== c);
|
|
120
127
|
return normalizeTable({
|
|
128
|
+
...props,
|
|
121
129
|
header: drop(props.header),
|
|
122
130
|
rows: props.rows.map(drop),
|
|
123
|
-
aligns: drop(props.aligns)
|
|
131
|
+
aligns: drop(props.aligns),
|
|
132
|
+
widths: props.widths && drop(props.widths)
|
|
124
133
|
});
|
|
125
134
|
}
|
|
126
135
|
/** One element moved within an array, out of place. The whole of what both move ops
|
|
@@ -140,20 +149,22 @@ export function moveRow(props, r, by) {
|
|
|
140
149
|
const to = Math.max(0, Math.min(r + by, rows.length - 1));
|
|
141
150
|
if (r === to || r < 0 || r >= rows.length)
|
|
142
151
|
return normalizeTable(props);
|
|
143
|
-
return fromRows(move(rows, r, to)
|
|
152
|
+
return fromRows(props, move(rows, r, to));
|
|
144
153
|
}
|
|
145
|
-
/** Column `c` moved by `by` places, clamped. `aligns`
|
|
146
|
-
*
|
|
147
|
-
* column that took the index. */
|
|
154
|
+
/** Column `c` moved by `by` places, clamped. `aligns` and `widths` travel with the
|
|
155
|
+
* column: each is the column's property, so a move that left it behind would retint
|
|
156
|
+
* or resize the column that took the index. */
|
|
148
157
|
export function moveColumn(props, c, by) {
|
|
149
158
|
const to = Math.max(0, Math.min(c + by, columnCount(props) - 1));
|
|
150
159
|
if (c === to || c < 0 || c >= columnCount(props))
|
|
151
160
|
return normalizeTable(props);
|
|
152
161
|
const shift = (xs) => move(xs, c, to);
|
|
153
162
|
return normalizeTable({
|
|
163
|
+
...props,
|
|
154
164
|
header: shift(props.header),
|
|
155
165
|
rows: props.rows.map(shift),
|
|
156
|
-
aligns: shift(props.aligns)
|
|
166
|
+
aligns: shift(props.aligns),
|
|
167
|
+
widths: props.widths && shift(props.widths)
|
|
157
168
|
});
|
|
158
169
|
}
|
|
159
170
|
/**
|
|
@@ -162,35 +173,86 @@ export function moveColumn(props, c, by) {
|
|
|
162
173
|
* covers ranks and deletes them, and one spanning both is the table itself, so what
|
|
163
174
|
* reaches here is a proper sub-rectangle and the shape it leaves is the shape it found.
|
|
164
175
|
*
|
|
165
|
-
*
|
|
176
|
+
* A cell keeps its own alignment, and a column its alignment and width: what a clear
|
|
177
|
+
* empties is text.
|
|
166
178
|
*/
|
|
167
179
|
export function clearCells(props, r0, c0, r1, c1) {
|
|
168
|
-
const blank = (cells, r) => cells.map((cell, c) =>
|
|
180
|
+
const blank = (cells, r) => cells.map((cell, c) => r >= r0 && r <= r1 && c >= c0 && c <= c1 ? { ...cell, text: '', marks: [] } : cell);
|
|
169
181
|
return normalizeTable({
|
|
182
|
+
...props,
|
|
170
183
|
header: blank(props.header, 0),
|
|
171
|
-
rows: props.rows.map((row, i) => blank(row, i + 1))
|
|
172
|
-
aligns: props.aligns
|
|
184
|
+
rows: props.rows.map((row, i) => blank(row, i + 1))
|
|
173
185
|
});
|
|
174
186
|
}
|
|
175
|
-
/** Set column `c`'s alignment: the
|
|
176
|
-
*
|
|
187
|
+
/** Set column `c`'s alignment: what the held column's cluster and Shift+arrow write
|
|
188
|
+
* (`table-view.ts`). */
|
|
177
189
|
export function setAlign(props, c, align) {
|
|
178
190
|
return normalizeTable({
|
|
179
191
|
...props,
|
|
180
192
|
aligns: props.aligns.map((a, i) => (i === c ? align : a))
|
|
181
193
|
});
|
|
182
194
|
}
|
|
195
|
+
/** Draw the first row as a header or as a body row: what the header toggle writes
|
|
196
|
+
* (`table-view.ts`). */
|
|
197
|
+
export function setHeadless(props, headless) {
|
|
198
|
+
return normalizeTable({ ...props, headless: headless || undefined });
|
|
199
|
+
}
|
|
200
|
+
/** Every cell in the inclusive rectangle given `key`'s `value`, or cleared of it where
|
|
201
|
+
* `value` is absent, which returns the cell to its column's alignment or the quill's. */
|
|
202
|
+
export function setCellLayout(props, r0, c0, r1, c1, key, value) {
|
|
203
|
+
const lay = (cells, r) => cells.map((cell, c) => {
|
|
204
|
+
if (r < r0 || r > r1 || c < c0 || c > c1)
|
|
205
|
+
return cell;
|
|
206
|
+
const next = { ...cell, [key]: value };
|
|
207
|
+
if (value === undefined)
|
|
208
|
+
delete next[key];
|
|
209
|
+
return next;
|
|
210
|
+
});
|
|
211
|
+
return normalizeTable({
|
|
212
|
+
...props,
|
|
213
|
+
header: lay(props.header, 0),
|
|
214
|
+
rows: props.rows.map((row, i) => lay(row, i + 1))
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
/** Set or clear the column weights: what a border drag and its double-click write
|
|
218
|
+
* (`table-view.ts`). `undefined` returns every column to auto-fit. */
|
|
219
|
+
export function setWidths(props, widths) {
|
|
220
|
+
return normalizeTable({ ...props, widths });
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* Each column's share of the table's width, summing to 1, or `undefined` for a table
|
|
224
|
+
* that auto-fits throughout. An auto-fit column beside weighted ones takes the mean
|
|
225
|
+
* weight: the page sizes it to its content, which the editor's grid, laid out fixed so
|
|
226
|
+
* the weights hold, cannot ask.
|
|
227
|
+
*/
|
|
228
|
+
export function columnShares(props) {
|
|
229
|
+
const set = props.widths?.filter((w) => w !== null);
|
|
230
|
+
if (!props.widths || !set?.length)
|
|
231
|
+
return undefined;
|
|
232
|
+
const mean = set.reduce((a, b) => a + b, 0) / set.length;
|
|
233
|
+
const weights = props.widths.map((w) => w ?? mean);
|
|
234
|
+
const total = weights.reduce((a, b) => a + b, 0);
|
|
235
|
+
return weights.map((w) => w / total);
|
|
236
|
+
}
|
|
183
237
|
// ── The cell codec ──────────────────────────────────────────────────────────
|
|
184
238
|
/**
|
|
185
|
-
* A cell as a one-
|
|
186
|
-
*
|
|
187
|
-
*
|
|
188
|
-
*
|
|
239
|
+
* A cell as a one-paragraph `Content`: what `decode` takes under `cellSchema`. Each
|
|
240
|
+
* `\n` in the text opens a `continues` line, which is the hard break it decodes to and
|
|
241
|
+
* the one USV `pmToContent` projects a break back to. The cell's marks are that
|
|
242
|
+
* content's marks, both being offsets into the same text.
|
|
189
243
|
*/
|
|
190
244
|
export function cellContent(cell) {
|
|
245
|
+
const breaks = cell.text.split('\n').length - 1;
|
|
191
246
|
return {
|
|
192
247
|
text: cell.text,
|
|
193
|
-
lines: [
|
|
248
|
+
lines: [
|
|
249
|
+
{ containers: [], kind: 'para' },
|
|
250
|
+
...Array.from({ length: breaks }, () => ({
|
|
251
|
+
containers: [],
|
|
252
|
+
kind: 'para',
|
|
253
|
+
continues: true
|
|
254
|
+
}))
|
|
255
|
+
],
|
|
194
256
|
marks: cell.marks,
|
|
195
257
|
islands: []
|
|
196
258
|
};
|
|
@@ -210,29 +272,38 @@ export function cellContent(cell) {
|
|
|
210
272
|
export function cellFromDoc(doc, prior) {
|
|
211
273
|
const projected = pmToContent(doc);
|
|
212
274
|
const text = projected.text;
|
|
275
|
+
const layout = cellLayout(prior);
|
|
213
276
|
const anchors = prior.marks.filter((m) => m.type === 'anchor');
|
|
214
277
|
if (!anchors.length)
|
|
215
|
-
return { text, marks: projected.marks };
|
|
278
|
+
return { ...layout, text, marks: projected.marks };
|
|
216
279
|
const before = cellContent(prior);
|
|
217
280
|
const delta = contentEdit(before, cellContent({ text, marks: [] })).delta;
|
|
218
281
|
const rebased = core()
|
|
219
282
|
.mapMarks(before, delta ? { delta } : {})
|
|
220
283
|
.filter((m) => m.type === 'anchor');
|
|
221
284
|
return {
|
|
285
|
+
...layout,
|
|
222
286
|
text,
|
|
223
287
|
marks: [...projected.marks, ...rebased].sort((a, b) => a.start - b.start || a.end - b.end)
|
|
224
288
|
};
|
|
225
289
|
}
|
|
290
|
+
/** A cell's keys beside its text and marks: what no edit of the text touches. */
|
|
291
|
+
function cellLayout(cell) {
|
|
292
|
+
const { text: _text, marks: _marks, ...layout } = cell;
|
|
293
|
+
return layout;
|
|
294
|
+
}
|
|
226
295
|
/** Whether two cells are the same value: what tells an own edit (the projection the
|
|
227
296
|
* view just produced) from an external one (an undo, a re-hydrate) without a flag.
|
|
228
297
|
* The marks structurally, for the reason `reconcile.ts` states. */
|
|
229
298
|
export function cellEqual(a, b) {
|
|
230
299
|
return a.text === b.text && valueEqual(a.marks, b.marks);
|
|
231
300
|
}
|
|
232
|
-
/** Whether two tables have the same rectangle and
|
|
233
|
-
* a rebuild rather than a per-cell reseed. */
|
|
301
|
+
/** Whether two tables have the same rectangle, alignment, header and widths: the
|
|
302
|
+
* change that forces a rebuild rather than a per-cell reseed. */
|
|
234
303
|
export function shapeEqual(a, b) {
|
|
235
304
|
return (a.rows.length === b.rows.length &&
|
|
236
305
|
a.aligns.length === b.aligns.length &&
|
|
237
|
-
a.aligns.every((x, i) => x === b.aligns[i])
|
|
306
|
+
a.aligns.every((x, i) => x === b.aligns[i]) &&
|
|
307
|
+
!a.headless === !b.headless &&
|
|
308
|
+
valueEqual(a.widths, b.widths));
|
|
238
309
|
}
|
package/dist/core/theme.css
CHANGED
|
@@ -235,7 +235,7 @@
|
|
|
235
235
|
Offset-free is the whole of it: an offset states where the light is and no pole
|
|
236
236
|
carries that, where a halo states occlusion. Toward black at both poles for the
|
|
237
237
|
same reason — a crevice is darker than what surrounds it in a dark room too — so
|
|
238
|
-
the cue means one thing whichever way the scale runs. Spent only on what
|
|
238
|
+
the cue means one thing whichever way the scale runs. Spent only on what floats,
|
|
239
239
|
and the one shadow any surface casts, which is review's to hold. */
|
|
240
240
|
--_qm-lift: 0 0 12px color-mix(in oklab, #000 16%, transparent);
|
|
241
241
|
/* The recede ladder: a hover-revealed or disabled affordance at rest, and a
|
|
@@ -76,6 +76,7 @@
|
|
|
76
76
|
type ArrayLayout
|
|
77
77
|
} from './structure.js';
|
|
78
78
|
import { splitDeep, unrouted, type DeepDiagnostic } from './diagnostics.js';
|
|
79
|
+
import { propertyDomIds } from './domid.js';
|
|
79
80
|
import type { LandingBox } from './leaves.js';
|
|
80
81
|
import { reorder, reorderArm } from './motion.js';
|
|
81
82
|
import Icon from './icons/Icon.svelte';
|
|
@@ -91,7 +92,7 @@
|
|
|
91
92
|
const CHEVRON = 16;
|
|
92
93
|
|
|
93
94
|
interface Props {
|
|
94
|
-
value: unknown
|
|
95
|
+
value: unknown;
|
|
95
96
|
/** The declared `default:`: the rows an unset array draws, which a gesture takes. */
|
|
96
97
|
fallback?: unknown[];
|
|
97
98
|
items: QuillFieldSchema | undefined;
|
|
@@ -156,11 +157,18 @@
|
|
|
156
157
|
const table = $derived(layout === 'table' && control === 'object');
|
|
157
158
|
const columns = $derived(Object.entries(items?.properties ?? {}));
|
|
158
159
|
const defaulted = $derived(value == null && (fallback?.length ?? 0) > 0);
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
160
|
+
// A bare value is the one-element list the render floor wraps it into (canon
|
|
161
|
+
// `SCHEMAS.md` §"Type coercion"), and the reader answers no element of it.
|
|
162
|
+
const bare = $derived(value != null && !Array.isArray(value));
|
|
163
|
+
const arr = $derived(rowsOf(value));
|
|
164
|
+
function rowsOf(v: unknown): unknown[] {
|
|
165
|
+
if (v == null) return fallback ?? [];
|
|
166
|
+
return Array.isArray(v) ? v : [v];
|
|
167
|
+
}
|
|
168
|
+
/** The rows' content read: the boundary's, or, while the rows are the default or a
|
|
169
|
+
* bare value, the literal's own leaf at the codec its declared type names. */
|
|
162
170
|
function readAt(path: PathStep[]): Content | undefined {
|
|
163
|
-
if (!defaulted) return contentAt(path);
|
|
171
|
+
if (!defaulted && !bare) return contentAt(path);
|
|
164
172
|
const leaf = schemaAt({ type: 'array', items }, path);
|
|
165
173
|
let v: unknown = arr;
|
|
166
174
|
for (const step of path) v = (v as Record<string | number, unknown> | undefined)?.[step];
|
|
@@ -177,7 +185,7 @@
|
|
|
177
185
|
// an effect-only seed mounts every element editor in a second render.
|
|
178
186
|
const seq = new IdSeq();
|
|
179
187
|
// svelte-ignore state_referenced_locally
|
|
180
|
-
let ids = $state<string[]>(seq.take((value
|
|
188
|
+
let ids = $state<string[]>(seq.take(rowsOf(value).length));
|
|
181
189
|
// Length reconcile (defend against an out-of-band length change);
|
|
182
190
|
// order is maintained by the mutators, not here.
|
|
183
191
|
$effect(() => {
|
|
@@ -593,6 +601,7 @@
|
|
|
593
601
|
>
|
|
594
602
|
<FieldLabel
|
|
595
603
|
label={columnTitle(key, sub)}
|
|
604
|
+
descriptionId={idBase != null ? propertyDomIds(idBase, key).description : undefined}
|
|
596
605
|
required={obliged(sub)}
|
|
597
606
|
description={sub.description}
|
|
598
607
|
/>
|
|
@@ -605,6 +614,7 @@
|
|
|
605
614
|
<ObjectField
|
|
606
615
|
bare
|
|
607
616
|
bind:this={cellEls[id]}
|
|
617
|
+
{idBase}
|
|
608
618
|
value={(arr[k] ?? {}) as Record<string, unknown>}
|
|
609
619
|
properties={items?.properties}
|
|
610
620
|
label={rowName(k)}
|
|
@@ -690,6 +700,7 @@
|
|
|
690
700
|
<ProseValue
|
|
691
701
|
bind:this={els[id]}
|
|
692
702
|
content={() => readAt([k])}
|
|
703
|
+
value={arr[k]}
|
|
693
704
|
plaintext={items != null && baseType(items) === 'plaintext'}
|
|
694
705
|
label={rowName(k)}
|
|
695
706
|
onChange={(rt) => commitElement(k, rt)}
|
|
@@ -3,7 +3,7 @@ import { type ArrayLayout } from './structure.js';
|
|
|
3
3
|
import { type DeepDiagnostic } from './diagnostics.js';
|
|
4
4
|
import type { LandingBox } from './leaves.js';
|
|
5
5
|
interface Props {
|
|
6
|
-
value: unknown
|
|
6
|
+
value: unknown;
|
|
7
7
|
/** The declared `default:`: the rows an unset array draws, which a gesture takes. */
|
|
8
8
|
fallback?: unknown[];
|
|
9
9
|
items: QuillFieldSchema | undefined;
|
package/dist/visual/Field.svelte
CHANGED
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
ResolvedField
|
|
28
28
|
} from '@quillmark/wasm';
|
|
29
29
|
import type { EditorErrorHandler } from '../core/errors.js';
|
|
30
|
-
import type
|
|
30
|
+
import { seatValueLeaves, type LandingBox, type LeafRegistry } from './leaves.js';
|
|
31
31
|
import type { FieldModel, FieldSpan } from './structure.js';
|
|
32
32
|
import {
|
|
33
33
|
VARIANT_DISCRIMINANT,
|
|
@@ -250,6 +250,13 @@
|
|
|
250
250
|
return () => registry.unregisterControl(key);
|
|
251
251
|
}
|
|
252
252
|
|
|
253
|
+
// A by-value prose leaf anywhere inside this field's containers is reached under
|
|
254
|
+
// this field's key: the active address names the field, never the element or cell.
|
|
255
|
+
seatValueLeaves({
|
|
256
|
+
focus: (leaf) => leaves?.focusValue(leafKey, leaf),
|
|
257
|
+
release: (leaf) => leaves?.releaseValue(leafKey, leaf)
|
|
258
|
+
});
|
|
259
|
+
|
|
253
260
|
/**
|
|
254
261
|
* A form control has no controller to report its focus through, so the wrapper
|
|
255
262
|
* reports: `focusin` bubbles, so one handler covers a plain input, an array's N
|
|
@@ -361,7 +368,7 @@
|
|
|
361
368
|
{:else if field.control === 'array'}
|
|
362
369
|
<ArrayField
|
|
363
370
|
bind:this={arrayEl}
|
|
364
|
-
|
|
371
|
+
{value}
|
|
365
372
|
fallback={Array.isArray(field.schema.default) ? field.schema.default : undefined}
|
|
366
373
|
items={field.schema.items}
|
|
367
374
|
layout={arrayLayout(field.schema)}
|
|
@@ -400,7 +407,6 @@
|
|
|
400
407
|
labelId={domIds.label}
|
|
401
408
|
descriptionId={domIds.description}
|
|
402
409
|
idBase={domIds.control}
|
|
403
|
-
listKey={leafKey}
|
|
404
410
|
{contentAt}
|
|
405
411
|
onCommit={onCommitScalar}
|
|
406
412
|
diagnostics={deep}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { Document, Quill, Addr, Diagnostic, ResolvedField } from '@quillmark/wasm';
|
|
2
2
|
import type { EditorErrorHandler } from '../core/errors.js';
|
|
3
|
-
import type
|
|
3
|
+
import { type LeafRegistry } from './leaves.js';
|
|
4
4
|
import type { FieldModel, FieldSpan } from './structure.js';
|
|
5
5
|
import type { FieldDomIds } from './domid.js';
|
|
6
6
|
interface Props {
|
|
@@ -20,9 +20,13 @@
|
|
|
20
20
|
{@link FieldLabel} renders and the `aria-describedby` the control carries, which
|
|
21
21
|
holds whether the surface is open or not. So no reader depends on a hover.
|
|
22
22
|
|
|
23
|
-
Three ways in, one per input modality: hover (pointer),
|
|
24
|
-
(
|
|
25
|
-
on `pointerType === 'touch'` and closes on
|
|
23
|
+
Three ways in, one per input modality: hover (pointer), tap (touch), and an
|
|
24
|
+
activation with no pointer behind it (keyboard), wired here rather than taken from
|
|
25
|
+
`Tooltip`, whose trigger returns early on `pointerType === 'touch'` and closes on
|
|
26
|
+
click. The keyboard's activation is `Mod-/` on the control the description describes
|
|
27
|
+
(`guidance.ts`), not a stop on this glyph: a stop here would double every described
|
|
28
|
+
field's Tab stops, so the glyph is out of the tab order. A screen reader activating
|
|
29
|
+
it lands on the same toggle.
|
|
26
30
|
-->
|
|
27
31
|
<script lang="ts">
|
|
28
32
|
import { Popover } from 'bits-ui';
|
|
@@ -81,10 +85,51 @@
|
|
|
81
85
|
return lines === 1;
|
|
82
86
|
}
|
|
83
87
|
|
|
84
|
-
|
|
88
|
+
/** How far the trigger's target reaches past its glyph on the axis the surface stands
|
|
89
|
+
* on, which makes the side offset clearance from the target rather than from the
|
|
90
|
+
* glyph. The target is `.qm-tap-floor`'s box (controls.css), centred on the glyph and
|
|
91
|
+
* read off it rather than restated. A surface landing on the target takes a resting
|
|
92
|
+
* pointer off the trigger, which closes the surface, and hands the pointer back as it
|
|
93
|
+
* goes: a loop. A DOM with no layout reads none. */
|
|
94
|
+
let reach = $state(0);
|
|
95
|
+
|
|
96
|
+
function targetReach(): number {
|
|
97
|
+
if (triggerEl === undefined) return 0;
|
|
98
|
+
const glyph = triggerEl.getBoundingClientRect();
|
|
99
|
+
const target = getComputedStyle(triggerEl, '::after');
|
|
100
|
+
const [across, own] = beside ? [target.width, glyph.width] : [target.height, glyph.height];
|
|
101
|
+
return Math.max(0, (parseFloat(across) - own) / 2) || 0;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** What a keyboard raise was asked from: the described control, holding focus while
|
|
105
|
+
* the trigger never does, so its leaving and its Escape are what lower the surface. */
|
|
106
|
+
let from = $state<HTMLElement | undefined>(undefined);
|
|
107
|
+
|
|
108
|
+
function raise(origin?: HTMLElement): void {
|
|
109
|
+
from = origin;
|
|
85
110
|
beside = ridesTheRung();
|
|
111
|
+
reach = targetReach();
|
|
86
112
|
open = true;
|
|
87
113
|
}
|
|
114
|
+
|
|
115
|
+
$effect(() => {
|
|
116
|
+
if (!open || !from) return;
|
|
117
|
+
const owner = from;
|
|
118
|
+
const leave = (e: FocusEvent) => {
|
|
119
|
+
if (!(e.relatedTarget instanceof Node && owner.contains(e.relatedTarget))) open = false;
|
|
120
|
+
};
|
|
121
|
+
const escape = (e: KeyboardEvent) => {
|
|
122
|
+
if (e.key !== 'Escape' || e.defaultPrevented) return;
|
|
123
|
+
e.stopPropagation();
|
|
124
|
+
open = false;
|
|
125
|
+
};
|
|
126
|
+
owner.addEventListener('focusout', leave);
|
|
127
|
+
owner.addEventListener('keydown', escape);
|
|
128
|
+
return () => {
|
|
129
|
+
owner.removeEventListener('focusout', leave);
|
|
130
|
+
owner.removeEventListener('keydown', escape);
|
|
131
|
+
};
|
|
132
|
+
});
|
|
88
133
|
</script>
|
|
89
134
|
|
|
90
135
|
<!-- The trigger is ours, not `Popover.Trigger`, and the surface anchors to it:
|
|
@@ -96,6 +141,7 @@
|
|
|
96
141
|
bind:this={triggerEl}
|
|
97
142
|
type="button"
|
|
98
143
|
class="qm-field-hint qm-icon-btn qm-focus-ring qm-tap-floor"
|
|
144
|
+
tabindex="-1"
|
|
99
145
|
aria-label="{label} guidance"
|
|
100
146
|
aria-expanded={open}
|
|
101
147
|
aria-describedby={describedBy}
|
|
@@ -114,11 +160,17 @@
|
|
|
114
160
|
else raise();
|
|
115
161
|
}
|
|
116
162
|
}}
|
|
117
|
-
|
|
118
|
-
//
|
|
119
|
-
//
|
|
120
|
-
//
|
|
121
|
-
if (e.
|
|
163
|
+
onclick={(e) => {
|
|
164
|
+
// No pointer behind it (`detail` 0): the guidance key or a screen reader. A
|
|
165
|
+
// mouse click falls through to hover and a tap to `pointerup`, as above. The
|
|
166
|
+
// owner is whatever holds focus and is described by this trigger's node.
|
|
167
|
+
if (e.detail !== 0) return;
|
|
168
|
+
if (open) open = false;
|
|
169
|
+
else
|
|
170
|
+
raise(
|
|
171
|
+
document.activeElement?.closest<HTMLElement>(`[aria-describedby~="${describedBy}"]`) ??
|
|
172
|
+
undefined
|
|
173
|
+
);
|
|
122
174
|
}}
|
|
123
175
|
onblur={() => (open = false)}
|
|
124
176
|
onkeydown={(e) => {
|
|
@@ -153,7 +205,7 @@
|
|
|
153
205
|
collisionBoundary={bounds}
|
|
154
206
|
side={beside ? 'right' : 'top'}
|
|
155
207
|
align={beside ? 'center' : 'start'}
|
|
156
|
-
sideOffset={6}
|
|
208
|
+
sideOffset={6 + reach}
|
|
157
209
|
trapFocus={false}
|
|
158
210
|
onOpenAutoFocus={(e: Event) => e.preventDefault()}
|
|
159
211
|
onCloseAutoFocus={(e: Event) => e.preventDefault()}
|