@quillmark/svelte 0.20.1 → 0.22.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 +6 -7
- package/README.md +1 -1
- package/THEMING.md +1 -1
- package/dist/core/address.d.ts +6 -1
- package/dist/core/address.js +12 -3
- package/dist/core/bloom.d.ts +9 -4
- package/dist/core/bloom.js +18 -49
- package/dist/core/codec/atoms.d.ts +1 -1
- package/dist/core/codec/atoms.js +26 -11
- package/dist/core/codec/blocks.d.ts +5 -5
- package/dist/core/codec/blocks.js +27 -8
- package/dist/core/codec/code.d.ts +4 -4
- package/dist/core/codec/code.js +52 -22
- package/dist/core/codec/decode.d.ts +5 -5
- package/dist/core/codec/decode.js +18 -12
- package/dist/core/codec/encode.js +6 -1
- package/dist/core/codec/field.d.ts +22 -15
- package/dist/core/codec/field.js +66 -26
- package/dist/core/codec/index.d.ts +1 -1
- package/dist/core/codec/index.js +2 -2
- package/dist/core/codec/inputrules.d.ts +4 -0
- package/dist/core/codec/inputrules.js +44 -6
- package/dist/core/codec/link-paste.d.ts +11 -0
- package/dist/core/codec/link-paste.js +32 -0
- package/dist/core/codec/lists.d.ts +5 -4
- package/dist/core/codec/lists.js +87 -31
- package/dist/core/codec/nesting.d.ts +20 -0
- package/dist/core/codec/nesting.js +62 -0
- package/dist/core/codec/prose.css +145 -63
- package/dist/core/codec/schema.d.ts +2 -2
- package/dist/core/codec/schema.js +71 -18
- package/dist/core/codec/slash.js +0 -0
- package/dist/core/codec/table-copy.d.ts +17 -0
- package/dist/core/codec/table-copy.js +84 -0
- package/dist/core/codec/table-view.d.ts +6 -7
- package/dist/core/codec/table-view.js +281 -201
- package/dist/core/codec/table.d.ts +2 -9
- package/dist/core/codec/table.js +10 -19
- package/dist/core/codec/tasks.d.ts +19 -0
- package/dist/core/codec/tasks.js +140 -0
- package/dist/core/errors.d.ts +2 -2
- package/dist/core/rungs.d.ts +6 -0
- package/dist/core/rungs.js +11 -0
- package/dist/core/theme.css +24 -12
- package/dist/visual/ArrayField.svelte +236 -78
- package/dist/visual/ArrayField.svelte.d.ts +0 -2
- package/dist/visual/BooleanField.svelte +12 -1
- package/dist/visual/Card.svelte +41 -23
- package/dist/visual/Card.svelte.d.ts +3 -2
- package/dist/visual/CardControls.svelte +15 -15
- package/dist/visual/DateField.svelte +93 -64
- package/dist/visual/EnumField.svelte +9 -4
- package/dist/visual/Field.svelte +20 -16
- package/dist/visual/Field.svelte.d.ts +4 -2
- package/dist/visual/FieldHint.svelte +22 -5
- package/dist/visual/FieldLabel.svelte +5 -33
- package/dist/visual/FieldLabel.svelte.d.ts +0 -2
- package/dist/visual/FormatPopover.svelte +78 -81
- package/dist/visual/MatrixField.svelte +28 -19
- package/dist/visual/NumberField.svelte +32 -3
- package/dist/visual/ObjectField.svelte +40 -24
- package/dist/visual/ObjectField.svelte.d.ts +6 -0
- package/dist/visual/ProseField.svelte +10 -13
- package/dist/visual/ProseField.svelte.d.ts +5 -2
- package/dist/visual/ProseValue.svelte +25 -19
- package/dist/visual/ProseValue.svelte.d.ts +4 -4
- package/dist/visual/TextField.svelte +3 -2
- package/dist/visual/TipsCard.svelte +7 -7
- package/dist/visual/VariantField.svelte +1 -1
- package/dist/visual/VariantField.svelte.d.ts +1 -1
- package/dist/visual/VisualEditorInner.svelte +121 -39
- package/dist/visual/controls.css +21 -2
- package/dist/visual/icons/Icon.svelte +3 -2
- package/dist/visual/icons/Icon.svelte.d.ts +3 -2
- package/dist/visual/icons/nodes.d.ts +25 -21
- package/dist/visual/icons/nodes.js +9 -7
- package/dist/visual/leaves.d.ts +12 -9
- package/dist/visual/leaves.js +19 -0
- package/dist/visual/motion.d.ts +25 -3
- package/dist/visual/motion.js +58 -9
- package/dist/visual/strings.d.ts +15 -9
- package/dist/visual/strings.js +19 -12
- package/dist/visual/structure.d.ts +21 -31
- package/dist/visual/structure.js +54 -51
- package/package.json +3 -3
|
@@ -15,20 +15,23 @@
|
|
|
15
15
|
// is preserved, never minted), and no placeholder.
|
|
16
16
|
//
|
|
17
17
|
// The chrome is a band and a selection (CODEC §"The table island"), and the band raises
|
|
18
|
-
// nothing: what floats over the grid is
|
|
19
|
-
// control is absolutely positioned out of the grid, so none
|
|
20
|
-
// it; `codec/prose.css` draws the band, the
|
|
21
|
-
|
|
18
|
+
// nothing: what floats over the grid is a held column's alignment or the held first
|
|
19
|
+
// row's header toggle. Every control is absolutely positioned out of the grid, so none
|
|
20
|
+
// is in a row or a column of it; `codec/prose.css` draws the band, the clusters and the
|
|
21
|
+
// selection wash.
|
|
22
|
+
import { baseKeymap, chainCommands, selectAll, toggleMark } from 'prosemirror-commands';
|
|
22
23
|
import { redo, undo } from 'prosemirror-history';
|
|
23
24
|
import { keymap } from 'prosemirror-keymap';
|
|
24
25
|
import { EditorState, NodeSelection, Selection, TextSelection } from 'prosemirror-state';
|
|
25
26
|
import { EditorView } from 'prosemirror-view';
|
|
26
27
|
import { decode } from './decode.js';
|
|
27
|
-
import { inputRulesPlugin } from './inputrules.js';
|
|
28
|
+
import { inputRulesPlugin, undoRuleKeymap } from './inputrules.js';
|
|
29
|
+
import { linkPastePlugin } from './link-paste.js';
|
|
28
30
|
import { tablePropsOfNode } from './islands.js';
|
|
31
|
+
import { tableElement, tableText } from './table-copy.js';
|
|
29
32
|
import { breakKeymap } from './breaks.js';
|
|
30
33
|
import { cellSchema } from './schema.js';
|
|
31
|
-
import { ALIGNS, cellAt, cellContent, cellEqual, cellFromDoc, clearCells, columnCount, deleteColumn, deleteRow, insertColumn, insertRow, moveColumn, moveRow, normalizeTable, rowCells, rowCount, rowEmpty, setAlign, setHeadless,
|
|
34
|
+
import { ALIGNS, cellAt, cellContent, cellEqual, cellFromDoc, clearCells, columnCount, deleteColumn, deleteRow, insertColumn, insertRow, moveColumn, moveRow, normalizeTable, rowCells, rowCount, rowEmpty, setAlign, setHeadless, columnShares, setWidths, shapeEqual, withCell } from './table.js';
|
|
32
35
|
/**
|
|
33
36
|
* The package's English for the island chrome. It lives here, beside the surface
|
|
34
37
|
* that draws it, rather than in the visual tier's table: the codec mounts this
|
|
@@ -50,11 +53,7 @@ export const DEFAULT_TABLE_STRINGS = {
|
|
|
50
53
|
tableAlignLeft: 'Align left',
|
|
51
54
|
tableAlignCenter: 'Align center',
|
|
52
55
|
tableAlignRight: 'Align right',
|
|
53
|
-
tableHeaderToggle: 'Header row'
|
|
54
|
-
tableAlignTop: 'Align top',
|
|
55
|
-
tableAlignMiddle: 'Align middle',
|
|
56
|
-
tableAlignBottom: 'Align bottom',
|
|
57
|
-
tableCellAlignment: 'Cell alignment'
|
|
56
|
+
tableHeaderToggle: 'Header row'
|
|
58
57
|
};
|
|
59
58
|
/** The grip's glyph, as the path data a DOM node can carry — this chrome's own set rather
|
|
60
59
|
* than `visual/icons/nodes.ts`, this being the one place chrome is built without Svelte,
|
|
@@ -79,28 +78,6 @@ const HEADER_GLYPH = [
|
|
|
79
78
|
'M5 3h14a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2z',
|
|
80
79
|
'M3 9h18'
|
|
81
80
|
];
|
|
82
|
-
/** The held cells' vertical toggles' glyphs, off the same release. */
|
|
83
|
-
const VALIGN_GLYPH = {
|
|
84
|
-
top: [
|
|
85
|
-
'M6 6h2a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2z',
|
|
86
|
-
'M16 6h2a2 2 0 0 1 2 2v5a2 2 0 0 1-2 2h-2a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2z',
|
|
87
|
-
'M22 2H2'
|
|
88
|
-
],
|
|
89
|
-
middle: [
|
|
90
|
-
'M2 12h20',
|
|
91
|
-
'M10 16v4a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2v-4',
|
|
92
|
-
'M10 8V4a2 2 0 0 0-2-2H6a2 2 0 0 0-2 2v4',
|
|
93
|
-
'M20 16v1a2 2 0 0 1-2 2h-2a2 2 0 0 1-2-2v-1',
|
|
94
|
-
'M14 8V7c0-1.1.9-2 2-2h2a2 2 0 0 1 2 2v1'
|
|
95
|
-
],
|
|
96
|
-
bottom: [
|
|
97
|
-
'M6 2h2a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2z',
|
|
98
|
-
'M16 9h2a2 2 0 0 1 2 2v5a2 2 0 0 1-2 2h-2a2 2 0 0 1-2-2v-5a2 2 0 0 1 2-2z',
|
|
99
|
-
'M22 22H2'
|
|
100
|
-
]
|
|
101
|
-
};
|
|
102
|
-
/** The vertical alignments a cell takes, in the cluster's order. */
|
|
103
|
-
const VALIGNS = ['top', 'middle', 'bottom'];
|
|
104
81
|
/** Draw a cell's alignment on its box: its own where it holds one, its column's
|
|
105
82
|
* otherwise, and the vertical one where it holds one. */
|
|
106
83
|
function layCell(box, props, r, c) {
|
|
@@ -158,10 +135,16 @@ function chromeButton(className, label, run) {
|
|
|
158
135
|
});
|
|
159
136
|
return btn;
|
|
160
137
|
}
|
|
161
|
-
/** A cell's plugin stack: marks
|
|
162
|
-
* belongs to the field (history, anchors, the ghost). */
|
|
138
|
+
/** A cell's plugin stack: marks, the markdown shorthands and a pasted URL's link, and
|
|
139
|
+
* nothing that belongs to the field (history, anchors, the ghost). */
|
|
163
140
|
function cellPlugins(keys) {
|
|
164
|
-
return [
|
|
141
|
+
return [
|
|
142
|
+
inputRulesPlugin(cellSchema),
|
|
143
|
+
undoRuleKeymap(),
|
|
144
|
+
linkPastePlugin(cellSchema.marks.link),
|
|
145
|
+
keymap(keys),
|
|
146
|
+
keymap(baseKeymap)
|
|
147
|
+
];
|
|
165
148
|
}
|
|
166
149
|
/** The controls, each of which answers for its own press. Spelled apart from
|
|
167
150
|
* {@link owned} because the pointer router needs them before it needs the cells: a grip
|
|
@@ -181,14 +164,26 @@ const within = (rect, p) => p.x >= rect.left && p.x <= rect.right && p.y >= rect
|
|
|
181
164
|
function distance(rect, p) {
|
|
182
165
|
return Math.hypot(Math.max(rect.left - p.x, 0, p.x - rect.right), Math.max(rect.top - p.y, 0, p.y - rect.bottom));
|
|
183
166
|
}
|
|
184
|
-
const sameCells = (a, b) => a.r0 === b.r0 && a.c0 === b.c0 && a.r1 === b.r1 && a.c1 === b.c1;
|
|
185
|
-
/** The
|
|
167
|
+
const sameCells = (a, b) => a.r0 === b.r0 && a.c0 === b.c0 && a.r1 === b.r1 && a.c1 === b.c1 && a.swept === b.swept;
|
|
168
|
+
/** The block two corners span, in either order: what a sweep holds. */
|
|
186
169
|
const spanCells = (a, b) => ({
|
|
187
170
|
r0: Math.min(a.r, b.r),
|
|
188
171
|
c0: Math.min(a.c, b.c),
|
|
189
172
|
r1: Math.max(a.r, b.r),
|
|
190
|
-
c1: Math.max(a.c, b.c)
|
|
173
|
+
c1: Math.max(a.c, b.c),
|
|
174
|
+
swept: true
|
|
191
175
|
});
|
|
176
|
+
/** Each mounted island drawing a table, by its `dom`, which is what the outer view hands
|
|
177
|
+
* back for a position: how a key in the field reaches into a table ({@link tableAt}). A
|
|
178
|
+
* placeholder is absent, so it answers as the atom it draws. */
|
|
179
|
+
const mounted = new WeakMap();
|
|
180
|
+
/**
|
|
181
|
+
* Where the caret stood as each table value was written over, keyed by that value. An
|
|
182
|
+
* undo hands the node back the very `props` object the undone edit replaced, so the seat
|
|
183
|
+
* held against it is where the edit was made from: where PM's history puts the caret
|
|
184
|
+
* back in a field's own text.
|
|
185
|
+
*/
|
|
186
|
+
const departures = new WeakMap();
|
|
192
187
|
class TableIslandView {
|
|
193
188
|
node;
|
|
194
189
|
outer;
|
|
@@ -210,9 +205,6 @@ class TableIslandView {
|
|
|
210
205
|
clusters = [];
|
|
211
206
|
/** The first row's header toggle, up while that row is held. */
|
|
212
207
|
headerCluster;
|
|
213
|
-
/** The held cells' cluster, up over the first cell of a held rectangle that is no
|
|
214
|
-
* column, and built for each paint since what it presses is the cells'. */
|
|
215
|
-
cellCluster;
|
|
216
208
|
/** The grid's own box, and the containing block every out-of-flow control is placed
|
|
217
209
|
* against. Not the scroller: an absolute inside a scroll container is placed
|
|
218
210
|
* against a padding box the scroll then slides out from under, so a control at the
|
|
@@ -225,6 +217,8 @@ class TableIslandView {
|
|
|
225
217
|
resize;
|
|
226
218
|
/** A drag's trailing `click`, which would otherwise re-select the moved line. */
|
|
227
219
|
suppressClick = false;
|
|
220
|
+
/** The grips of the row and column under the pointer, drawn up. */
|
|
221
|
+
near = [];
|
|
228
222
|
constructor(node, outer, getPos, deps) {
|
|
229
223
|
this.node = node;
|
|
230
224
|
this.outer = outer;
|
|
@@ -234,6 +228,9 @@ class TableIslandView {
|
|
|
234
228
|
this.dom.setAttribute('data-qm-island', node.attrs.islandType);
|
|
235
229
|
this.dom.setAttribute('data-qm-island-id', node.attrs.id);
|
|
236
230
|
this.dom.addEventListener('mousedown', this.onPointerDown);
|
|
231
|
+
this.dom.addEventListener('mouseover', this.onHover);
|
|
232
|
+
this.dom.addEventListener('mouseleave', this.onLeave);
|
|
233
|
+
this.dom.addEventListener('copy', this.onCopy, true);
|
|
237
234
|
this.render();
|
|
238
235
|
}
|
|
239
236
|
// ── PM's NodeView contract ────────────────────────────────────────────────
|
|
@@ -256,6 +253,7 @@ class TableIslandView {
|
|
|
256
253
|
// transaction, which is every keystroke in the table, and projecting each cell
|
|
257
254
|
// costs a `mapMarks` across the WASM boundary wherever one holds an anchor.
|
|
258
255
|
this.rendered = props;
|
|
256
|
+
const departure = departures.get(props);
|
|
259
257
|
for (const mounted of this.cells) {
|
|
260
258
|
const stored = cellAt(props, mounted.r, mounted.c);
|
|
261
259
|
if (cellEqual(stored, mounted.shown))
|
|
@@ -267,13 +265,14 @@ class TableIslandView {
|
|
|
267
265
|
});
|
|
268
266
|
mounted.view.updateState(fresh);
|
|
269
267
|
mounted.shown = stored;
|
|
270
|
-
//
|
|
271
|
-
//
|
|
272
|
-
//
|
|
273
|
-
// caret somewhere the edit it undid never was.
|
|
268
|
+
// The rule a field's own re-hydrate takes: keep the offset, clamped into the
|
|
269
|
+
// text that is there now, a fresh state resolving to the cell's start. An undo
|
|
270
|
+
// outranks it below, the departure being where the edit was.
|
|
274
271
|
const at = Math.min(head, fresh.doc.content.size);
|
|
275
272
|
mounted.view.dispatch(fresh.tr.setSelection(Selection.near(fresh.doc.resolve(at))));
|
|
276
273
|
}
|
|
274
|
+
if (departure && this.focusedSeat())
|
|
275
|
+
this.focusCell(departure.r, departure.c, departure.head);
|
|
277
276
|
// A cell's alignment is no part of its text, so it is restated on every box: the
|
|
278
277
|
// reseed above compares text and marks alone.
|
|
279
278
|
for (const mounted of this.cells)
|
|
@@ -296,6 +295,9 @@ class TableIslandView {
|
|
|
296
295
|
}
|
|
297
296
|
destroy() {
|
|
298
297
|
this.dom.removeEventListener('mousedown', this.onPointerDown);
|
|
298
|
+
this.dom.removeEventListener('mouseover', this.onHover);
|
|
299
|
+
this.dom.removeEventListener('mouseleave', this.onLeave);
|
|
300
|
+
this.dom.removeEventListener('copy', this.onCopy, true);
|
|
299
301
|
this.endDrag();
|
|
300
302
|
this.endSweep();
|
|
301
303
|
this.endResize();
|
|
@@ -355,6 +357,43 @@ class TableIslandView {
|
|
|
355
357
|
else
|
|
356
358
|
this.caretBeside(point);
|
|
357
359
|
};
|
|
360
|
+
/** A cell under the pointer draws its row's and its column's grips, so a line's
|
|
361
|
+
* handle is found from the line rather than only from the band beside it. A control
|
|
362
|
+
* under the pointer is its own reveal, and draws no other. */
|
|
363
|
+
onHover = (event) => {
|
|
364
|
+
const target = event.target;
|
|
365
|
+
const box = target?.closest?.(CONTROLS) ? null : target?.closest?.('.qm-table-cell');
|
|
366
|
+
const cell = box ? this.cells.find((m) => m.box === box) : undefined;
|
|
367
|
+
const next = cell
|
|
368
|
+
? [
|
|
369
|
+
this.grips.get(lineKey({ axis: 'row', index: cell.r }))?.grip,
|
|
370
|
+
this.grips.get(lineKey({ axis: 'column', index: cell.c }))?.grip
|
|
371
|
+
].filter((grip) => !!grip)
|
|
372
|
+
: [];
|
|
373
|
+
for (const grip of this.near)
|
|
374
|
+
if (!next.includes(grip))
|
|
375
|
+
grip.removeAttribute('data-near');
|
|
376
|
+
for (const grip of next)
|
|
377
|
+
grip.setAttribute('data-near', '');
|
|
378
|
+
this.near = next;
|
|
379
|
+
};
|
|
380
|
+
onLeave = () => {
|
|
381
|
+
for (const grip of this.near)
|
|
382
|
+
grip.removeAttribute('data-near');
|
|
383
|
+
this.near = [];
|
|
384
|
+
};
|
|
385
|
+
/** A copy over a held rectangle writes its cells (`table-copy.ts`). Captured, since the
|
|
386
|
+
* caret's cell would otherwise copy its own text selection over them. */
|
|
387
|
+
onCopy = (event) => {
|
|
388
|
+
const held = this.selected;
|
|
389
|
+
if (!held || !event.clipboardData)
|
|
390
|
+
return;
|
|
391
|
+
const props = this.props();
|
|
392
|
+
event.clipboardData.setData('text/html', tableElement(props, held).outerHTML);
|
|
393
|
+
event.clipboardData.setData('text/plain', tableText(props, held));
|
|
394
|
+
event.preventDefault();
|
|
395
|
+
event.stopPropagation();
|
|
396
|
+
};
|
|
358
397
|
/** The cell a press inside the frame belongs to: the nearest by rect, which for a
|
|
359
398
|
* press on a border or a cell's own padding is the cell it is against.
|
|
360
399
|
*
|
|
@@ -399,18 +438,32 @@ class TableIslandView {
|
|
|
399
438
|
* leaf mounted without that plugin then answers "no gap" instead of dispatching
|
|
400
439
|
* a selection nothing draws. */
|
|
401
440
|
caretBeside(point) {
|
|
441
|
+
const box = this.dom.getBoundingClientRect();
|
|
442
|
+
this.leave(point.y < (box.top + box.bottom) / 2 ? -1 : 1);
|
|
443
|
+
}
|
|
444
|
+
/** Land the document's caret on one side of the island, by
|
|
445
|
+
* {@link TableIslandView.caretBeside}'s rule; false where that side holds no caret. */
|
|
446
|
+
leave(side) {
|
|
402
447
|
const pos = this.getPos();
|
|
403
448
|
if (pos == null)
|
|
404
|
-
return;
|
|
405
|
-
const
|
|
406
|
-
const before = point.y < (box.top + box.bottom) / 2;
|
|
407
|
-
const $at = this.outer.state.doc.resolve(before ? pos : pos + this.node.nodeSize);
|
|
449
|
+
return false;
|
|
450
|
+
const $at = this.outer.state.doc.resolve(side < 0 ? pos : pos + this.node.nodeSize);
|
|
408
451
|
const selection = this.outer.someProp('createSelectionBetween', (f) => f(this.outer, $at, $at)) ??
|
|
409
|
-
Selection.findFrom($at,
|
|
452
|
+
Selection.findFrom($at, side, true);
|
|
410
453
|
if (!selection)
|
|
411
|
-
return;
|
|
454
|
+
return false;
|
|
412
455
|
this.outer.focus();
|
|
413
456
|
this.outer.dispatch(this.outer.state.tr.setSelection(selection));
|
|
457
|
+
return true;
|
|
458
|
+
}
|
|
459
|
+
/** Land the caret in the table from the block beside it: its last cell's end from
|
|
460
|
+
* below, its first cell's start from above. */
|
|
461
|
+
enter(side) {
|
|
462
|
+
const props = this.props();
|
|
463
|
+
if (side < 0)
|
|
464
|
+
this.focusCell(rowCount(props) - 1, columnCount(props) - 1, 'end');
|
|
465
|
+
else
|
|
466
|
+
this.focusCell(0, 0, 'start');
|
|
414
467
|
}
|
|
415
468
|
/** The island as the selection: the Escape that climbs out of a cell or a held
|
|
416
469
|
* rectangle, which is the whole of what selects it. Backspace there deletes the
|
|
@@ -497,8 +550,8 @@ class TableIslandView {
|
|
|
497
550
|
* revalidates the whole table. */
|
|
498
551
|
lineCells(line, props = this.props()) {
|
|
499
552
|
return line.axis === 'row'
|
|
500
|
-
? { r0: line.index, c0: 0, r1: line.index, c1: columnCount(props) - 1 }
|
|
501
|
-
: { r0: 0, c0: line.index, r1: rowCount(props) - 1, c1: line.index };
|
|
553
|
+
? { r0: line.index, c0: 0, r1: line.index, c1: columnCount(props) - 1, swept: false }
|
|
554
|
+
: { r0: 0, c0: line.index, r1: rowCount(props) - 1, c1: line.index, swept: false };
|
|
502
555
|
}
|
|
503
556
|
/** The index space an axis allows, which is the whole of it on both. The header is
|
|
504
557
|
* row 0 rather than a line above the floor: a walk reaches it, a drag lands on it,
|
|
@@ -528,14 +581,12 @@ class TableIslandView {
|
|
|
528
581
|
else if (held?.c !== line.index)
|
|
529
582
|
this.focusCell(held?.r ?? 0, line.index);
|
|
530
583
|
}
|
|
531
|
-
/** Which line the held rectangle
|
|
532
|
-
* column spanning every row, and nothing
|
|
533
|
-
*
|
|
534
|
-
* rectangle answers by what it covers, never by the gesture that drew it, so a row
|
|
535
|
-
* swept cell by cell moves exactly as the row a grip named does. */
|
|
584
|
+
/** Which line the held rectangle names, on one axis: a row spanning every column, a
|
|
585
|
+
* column spanning every row, and nothing for a block that spans neither or for cells
|
|
586
|
+
* a sweep holds, which are text however many lines they cross. */
|
|
536
587
|
lineOn(axis) {
|
|
537
588
|
const held = this.selected;
|
|
538
|
-
if (!held)
|
|
589
|
+
if (!held || held.swept)
|
|
539
590
|
return undefined;
|
|
540
591
|
const props = this.props();
|
|
541
592
|
if (axis === 'row')
|
|
@@ -554,18 +605,24 @@ class TableIslandView {
|
|
|
554
605
|
// One subject at a time: a cell selection retires an island one, or the surface
|
|
555
606
|
// paints a washed row inside an outlined table and the next Backspace has two
|
|
556
607
|
// honest readings.
|
|
557
|
-
|
|
558
|
-
const outer = this.outer.state.selection;
|
|
559
|
-
if (pos != null && outer instanceof NodeSelection && outer.from === pos) {
|
|
560
|
-
const $at = this.outer.state.doc.resolve(pos);
|
|
561
|
-
const beside = this.outer.someProp('createSelectionBetween', (f) => f(this.outer, $at, $at)) ??
|
|
562
|
-
Selection.findFrom($at, -1, true);
|
|
563
|
-
if (beside)
|
|
564
|
-
this.outer.dispatch(this.outer.state.tr.setSelection(beside));
|
|
565
|
-
}
|
|
608
|
+
this.retireIsland();
|
|
566
609
|
this.selected = cells;
|
|
567
610
|
this.paintSelection();
|
|
568
611
|
}
|
|
612
|
+
/** Move the outer selection off this island when it holds it: a caret or a rectangle
|
|
613
|
+
* inside the grid is the subject from then on, and an outline left on the table says
|
|
614
|
+
* the next Backspace deletes it. */
|
|
615
|
+
retireIsland() {
|
|
616
|
+
const pos = this.getPos();
|
|
617
|
+
const outer = this.outer.state.selection;
|
|
618
|
+
if (pos == null || !(outer instanceof NodeSelection) || outer.from !== pos)
|
|
619
|
+
return;
|
|
620
|
+
const $at = this.outer.state.doc.resolve(pos);
|
|
621
|
+
const beside = this.outer.someProp('createSelectionBetween', (f) => f(this.outer, $at, $at)) ??
|
|
622
|
+
Selection.findFrom($at, -1, true);
|
|
623
|
+
if (beside)
|
|
624
|
+
this.outer.dispatch(this.outer.state.tr.setSelection(beside));
|
|
625
|
+
}
|
|
569
626
|
clearSelection() {
|
|
570
627
|
if (!this.selected)
|
|
571
628
|
return;
|
|
@@ -588,24 +645,25 @@ class TableIslandView {
|
|
|
588
645
|
if (line.axis === 'row' && line.index === 0 && this.headerCluster)
|
|
589
646
|
this.headerCluster.hidden = !named;
|
|
590
647
|
}
|
|
591
|
-
this.paintCellCluster(props);
|
|
592
648
|
for (const cell of this.cells)
|
|
593
649
|
cell.box.toggleAttribute('data-selected', !!held && cell.r >= held.r0 && cell.r <= held.r1 && cell.c >= held.c0 && cell.c <= held.c1);
|
|
594
650
|
}
|
|
595
651
|
/**
|
|
596
|
-
* What Backspace means over the selection
|
|
597
|
-
*
|
|
598
|
-
*
|
|
599
|
-
*
|
|
600
|
-
* going at once leaves no table for a rank rule to have produced. It is the whole of
|
|
601
|
-
* how a pointer deletes a table, and on a one-column or one-row table a single grip
|
|
602
|
-
* draws it.
|
|
652
|
+
* What Backspace means over the selection (CODEC §"The table island"). Swept cells
|
|
653
|
+
* clear and keep their lines. Named lines go, read off their extent: every rank on
|
|
654
|
+
* both axes is the table, which goes as the island, and on a one-column or one-row
|
|
655
|
+
* table a single grip draws that.
|
|
603
656
|
*/
|
|
604
657
|
deleteSelection() {
|
|
605
658
|
const held = this.selected;
|
|
606
659
|
if (!held)
|
|
607
660
|
return;
|
|
608
661
|
const props = this.props();
|
|
662
|
+
if (held.swept) {
|
|
663
|
+
if (allCells(props, held).some((cell) => cell.text !== ''))
|
|
664
|
+
this.write(clearCells(props, held.r0, held.c0, held.r1, held.c1));
|
|
665
|
+
return;
|
|
666
|
+
}
|
|
609
667
|
const wide = held.c0 === 0 && held.c1 === columnCount(props) - 1;
|
|
610
668
|
const tall = held.r0 === 0 && held.r1 === rowCount(props) - 1;
|
|
611
669
|
if (tall && wide)
|
|
@@ -663,73 +721,17 @@ class TableIslandView {
|
|
|
663
721
|
this.write(line.axis === 'row' ? moveRow(props, line.index, by) : moveColumn(props, line.index, by));
|
|
664
722
|
this.selectLine({ axis: line.axis, index: to });
|
|
665
723
|
}
|
|
666
|
-
/** Set column `c`'s alignment
|
|
667
|
-
* the
|
|
668
|
-
* column already
|
|
724
|
+
/** Set column `c`'s alignment, clear its cells' own, which would outrank it, and keep
|
|
725
|
+
* the column held: a changed alignment rebuilds the views, which retires the selection
|
|
726
|
+
* as a move's rebuild does. One `set` for both, and a column already at `to` with no
|
|
727
|
+
* cell of its own writes nothing. */
|
|
669
728
|
align(c, to) {
|
|
670
729
|
const props = this.props();
|
|
671
|
-
|
|
730
|
+
const own = Array.from({ length: rowCount(props) }, (_, r) => cellAt(props, r, c)).some((cell) => cell.align !== undefined);
|
|
731
|
+
if (props.aligns[c] !== to || own)
|
|
672
732
|
this.write(setAlign(props, c, to));
|
|
673
733
|
this.selectLine({ axis: 'column', index: c });
|
|
674
734
|
}
|
|
675
|
-
/** The held cells' cluster: up over the first cell of a held rectangle, where no
|
|
676
|
-
* column's own cluster is, each toggle pressed where every cell in the rectangle
|
|
677
|
-
* holds it. It floats over the cell under that one, as the column's floats over the
|
|
678
|
-
* first row, and is out of the tab order for the reason a grip is, its keyboard twin
|
|
679
|
-
* being `Mod-Shift-l`, `-e` and `-r` (§{@link TableIslandView.cellKeys}). */
|
|
680
|
-
paintCellCluster(props) {
|
|
681
|
-
this.cellCluster?.remove();
|
|
682
|
-
this.cellCluster = undefined;
|
|
683
|
-
const held = this.selected;
|
|
684
|
-
if (!held || this.lineOn('column') !== undefined)
|
|
685
|
-
return;
|
|
686
|
-
const first = this.cells.find((m) => m.r === held.r0 && m.c === held.c0);
|
|
687
|
-
if (!first)
|
|
688
|
-
return;
|
|
689
|
-
const s = this.deps.strings();
|
|
690
|
-
const covered = allCells(props, held);
|
|
691
|
-
const holds = (key, value) => covered.every((cell) => cell[key] === value);
|
|
692
|
-
const cluster = el('div', 'qm-table-align');
|
|
693
|
-
cluster.setAttribute('role', 'group');
|
|
694
|
-
cluster.setAttribute('aria-label', s.tableCellAlignment);
|
|
695
|
-
cluster.setAttribute('data-cells', '');
|
|
696
|
-
cluster.addEventListener('mousedown', (e) => e.preventDefault());
|
|
697
|
-
const toggle = (label, glyph, pressed, run) => {
|
|
698
|
-
const btn = chromeButton('qm-table-align-option', label, run);
|
|
699
|
-
btn.tabIndex = -1;
|
|
700
|
-
btn.setAttribute('aria-pressed', String(pressed));
|
|
701
|
-
btn.appendChild(svg(glyph, 2));
|
|
702
|
-
cluster.appendChild(btn);
|
|
703
|
-
return btn;
|
|
704
|
-
};
|
|
705
|
-
const names = {
|
|
706
|
-
left: s.tableAlignLeft,
|
|
707
|
-
center: s.tableAlignCenter,
|
|
708
|
-
right: s.tableAlignRight
|
|
709
|
-
};
|
|
710
|
-
for (const align of SETTABLE)
|
|
711
|
-
toggle(names[align], ALIGN_GLYPH[align], holds('align', align), () => this.layCells('align', align)).setAttribute('data-align', align);
|
|
712
|
-
const vnames = {
|
|
713
|
-
top: s.tableAlignTop,
|
|
714
|
-
middle: s.tableAlignMiddle,
|
|
715
|
-
bottom: s.tableAlignBottom
|
|
716
|
-
};
|
|
717
|
-
for (const valign of VALIGNS)
|
|
718
|
-
toggle(vnames[valign], VALIGN_GLYPH[valign], holds('valign', valign), () => this.layCells('valign', valign)).setAttribute('data-valign', valign);
|
|
719
|
-
first.box.appendChild(cluster);
|
|
720
|
-
this.cellCluster = cluster;
|
|
721
|
-
}
|
|
722
|
-
/** Lay every held cell, or the caret's where none is held, at `value`, and clear the
|
|
723
|
-
* key from them where every one already holds it: the press that set an alignment
|
|
724
|
-
* takes it back to the column's. One `set`, and the rectangle stays held. */
|
|
725
|
-
layCells(key, value, at) {
|
|
726
|
-
const held = this.selected ?? (at && { r0: at.r, c0: at.c, r1: at.r, c1: at.c });
|
|
727
|
-
if (!held)
|
|
728
|
-
return;
|
|
729
|
-
const props = this.props();
|
|
730
|
-
const every = allCells(props, held).every((cell) => cell[key] === value);
|
|
731
|
-
this.write(setCellLayout(props, held.r0, held.c0, held.r1, held.c1, key, every ? undefined : value));
|
|
732
|
-
}
|
|
733
735
|
// ── Drag to reorder ───────────────────────────────────────────────────────
|
|
734
736
|
/**
|
|
735
737
|
* Press-and-drag a grip moves its line. The press still selects: the dead zone is
|
|
@@ -1013,11 +1015,17 @@ class TableIslandView {
|
|
|
1013
1015
|
this.endResize();
|
|
1014
1016
|
this.teardownCells();
|
|
1015
1017
|
this.grips.clear();
|
|
1018
|
+
this.near = [];
|
|
1016
1019
|
this.clusters = [];
|
|
1017
1020
|
this.headerCluster = undefined;
|
|
1018
1021
|
this.dom.textContent = '';
|
|
1019
1022
|
const props = tablePropsOfNode(this.node);
|
|
1023
|
+
const departure = props && departures.get(props);
|
|
1020
1024
|
this.rendered = props;
|
|
1025
|
+
if (props)
|
|
1026
|
+
mounted.set(this.dom, this);
|
|
1027
|
+
else
|
|
1028
|
+
mounted.delete(this.dom);
|
|
1021
1029
|
if (!props) {
|
|
1022
1030
|
// Any other island type: the literal placeholder `toDOM` draws (islands.ts).
|
|
1023
1031
|
this.dom.appendChild(document.createTextNode(`[${this.node.attrs.islandType || 'island'}]`));
|
|
@@ -1059,7 +1067,7 @@ class TableIslandView {
|
|
|
1059
1067
|
scroller.appendChild(frame);
|
|
1060
1068
|
this.dom.appendChild(scroller);
|
|
1061
1069
|
this.paintSelection();
|
|
1062
|
-
this.reseat(seat);
|
|
1070
|
+
this.reseat(seat, departure);
|
|
1063
1071
|
}
|
|
1064
1072
|
/**
|
|
1065
1073
|
* Where the focus sits, in terms a rebuild can restore it by: the caret's cell by
|
|
@@ -1074,14 +1082,19 @@ class TableIslandView {
|
|
|
1074
1082
|
*/
|
|
1075
1083
|
focusedSeat() {
|
|
1076
1084
|
const held = this.cells.find((m) => m.view.hasFocus());
|
|
1077
|
-
return held && { r: held.r, c: held.c };
|
|
1085
|
+
return held && { r: held.r, c: held.c, head: held.view.state.selection.head };
|
|
1078
1086
|
}
|
|
1079
|
-
/** Put the
|
|
1080
|
-
*
|
|
1081
|
-
*
|
|
1082
|
-
*
|
|
1083
|
-
|
|
1084
|
-
|
|
1087
|
+
/** Put the focus back after a rebuild: at the departure where the table returns to a
|
|
1088
|
+
* value an edit was made from, which is an undo, else at the end of the cell
|
|
1089
|
+
* {@link TableIslandView.focusedSeat} found, clamped by `focusCell` into the
|
|
1090
|
+
* rectangle that is there now, the seat being perhaps a cell the rebuild removed. A
|
|
1091
|
+
* caller that has a landing of its own overrides this by running after. */
|
|
1092
|
+
reseat(seat, departure) {
|
|
1093
|
+
if (!seat)
|
|
1094
|
+
return;
|
|
1095
|
+
if (departure)
|
|
1096
|
+
this.focusCell(departure.r, departure.c, departure.head);
|
|
1097
|
+
else
|
|
1085
1098
|
this.focusCell(seat.r, seat.c);
|
|
1086
1099
|
}
|
|
1087
1100
|
/** One table row: its cells, each carrying whatever chrome hangs off it. */
|
|
@@ -1146,11 +1159,11 @@ class TableIslandView {
|
|
|
1146
1159
|
return btn;
|
|
1147
1160
|
}
|
|
1148
1161
|
/** A column's alignment cluster: a toggle per alignment, pressed on the one the column
|
|
1149
|
-
* holds. Up only while
|
|
1162
|
+
* holds. Up only while this column is the named line
|
|
1150
1163
|
* ({@link TableIslandView.paintSelection}), and absent rather than transparent the
|
|
1151
1164
|
* rest of the time: it floats over the first row, and a box hit-tested there at rest
|
|
1152
1165
|
* would take that cell's presses. Out of the tab order for the reason a grip is, its
|
|
1153
|
-
* keyboard
|
|
1166
|
+
* keyboard twins being Shift+Left and Shift+Right (§{@link TableIslandView.cellKeys}). */
|
|
1154
1167
|
alignCluster(c, held, s) {
|
|
1155
1168
|
const cluster = el('div', 'qm-table-align');
|
|
1156
1169
|
cluster.setAttribute('role', 'group');
|
|
@@ -1201,12 +1214,10 @@ class TableIslandView {
|
|
|
1201
1214
|
* the pointer out past the last line is the claim — so its name carries the verb for
|
|
1202
1215
|
* everything that does not read position.
|
|
1203
1216
|
*
|
|
1204
|
-
*
|
|
1205
|
-
*
|
|
1206
|
-
*
|
|
1207
|
-
* cell
|
|
1208
|
-
* declines off the last cell lands on them, and the focus rung draws the one it lands
|
|
1209
|
-
* on (`codec/prose.css`). */
|
|
1217
|
+
* Out of the tab order, as a grip is: the Tab that declines off the last cell leaves
|
|
1218
|
+
* the island rather than landing on two controls drawn only under the pointer. A row
|
|
1219
|
+
* is the keyboard's from inside a cell, by Enter at the last row and Tab past the
|
|
1220
|
+
* last cell, and a column is the pointer's alone. */
|
|
1210
1221
|
addBar(axis, label) {
|
|
1211
1222
|
const btn = chromeButton('qm-table-add', label, () => {
|
|
1212
1223
|
const props = this.props();
|
|
@@ -1216,6 +1227,7 @@ class TableIslandView {
|
|
|
1216
1227
|
this.write(insertColumn(props, columnCount(props) - 1), { r: 0, c: columnCount(props) });
|
|
1217
1228
|
});
|
|
1218
1229
|
btn.setAttribute('data-axis', axis);
|
|
1230
|
+
btn.tabIndex = -1;
|
|
1219
1231
|
btn.appendChild(el('span', 'qm-table-add-bar'));
|
|
1220
1232
|
return btn;
|
|
1221
1233
|
}
|
|
@@ -1236,6 +1248,7 @@ class TableIslandView {
|
|
|
1236
1248
|
scrollThreshold: this.deps.clearance,
|
|
1237
1249
|
scrollMargin: this.deps.clearance,
|
|
1238
1250
|
dispatchTransaction: (tr) => {
|
|
1251
|
+
const from = { r, c, head: view.state.selection.head };
|
|
1239
1252
|
const next = view.state.apply(tr);
|
|
1240
1253
|
view.updateState(next);
|
|
1241
1254
|
if (!tr.docChanged)
|
|
@@ -1248,11 +1261,12 @@ class TableIslandView {
|
|
|
1248
1261
|
const now = this.props();
|
|
1249
1262
|
const cell = cellFromDoc(next.doc, cellAt(now, r, c));
|
|
1250
1263
|
mounted.shown = cell;
|
|
1251
|
-
this.write(withCell(now, r, c, cell));
|
|
1264
|
+
this.write(withCell(now, r, c, cell), undefined, from);
|
|
1252
1265
|
},
|
|
1253
1266
|
handleDOMEvents: {
|
|
1254
1267
|
focus: () => {
|
|
1255
1268
|
this.clearSelection();
|
|
1269
|
+
this.retireIsland();
|
|
1256
1270
|
this.deps.onCellFocus();
|
|
1257
1271
|
return false;
|
|
1258
1272
|
}
|
|
@@ -1283,13 +1297,16 @@ class TableIslandView {
|
|
|
1283
1297
|
* op is one commit and one undo step, which is why every op writes a whole
|
|
1284
1298
|
* table rather than mutating the props in place.
|
|
1285
1299
|
*/
|
|
1286
|
-
write(next, focus) {
|
|
1300
|
+
write(next, focus, from = this.focusedSeat()) {
|
|
1287
1301
|
const pos = this.getPos();
|
|
1288
1302
|
if (pos == null)
|
|
1289
1303
|
return;
|
|
1290
1304
|
const node = this.outer.state.doc.nodeAt(pos);
|
|
1291
1305
|
if (!node || node.type !== this.node.type)
|
|
1292
1306
|
return;
|
|
1307
|
+
const left = tablePropsOfNode(node);
|
|
1308
|
+
if (left && from)
|
|
1309
|
+
departures.set(left, from);
|
|
1293
1310
|
this.outer.dispatch(this.outer.state.tr.setNodeMarkup(pos, undefined, {
|
|
1294
1311
|
...node.attrs,
|
|
1295
1312
|
props: normalizeTable(next)
|
|
@@ -1297,9 +1314,10 @@ class TableIslandView {
|
|
|
1297
1314
|
if (focus)
|
|
1298
1315
|
this.focusCell(focus.r, focus.c);
|
|
1299
1316
|
}
|
|
1300
|
-
/** Land the caret at the end of a cell,
|
|
1301
|
-
*
|
|
1302
|
-
|
|
1317
|
+
/** Land the caret at the end of a cell, at its start, or at an offset clamped into its
|
|
1318
|
+
* text, the cell itself clamped into the rectangle: where a row or column op puts it,
|
|
1319
|
+
* since the op rebuilt the views the caret was in. */
|
|
1320
|
+
focusCell(r, c, edge = 'end') {
|
|
1303
1321
|
const props = this.props();
|
|
1304
1322
|
const row = Math.max(0, Math.min(r, rowCount(props) - 1));
|
|
1305
1323
|
const col = Math.max(0, Math.min(c, columnCount(props) - 1));
|
|
@@ -1307,12 +1325,29 @@ class TableIslandView {
|
|
|
1307
1325
|
if (!mounted)
|
|
1308
1326
|
return;
|
|
1309
1327
|
const { view } = mounted;
|
|
1328
|
+
const { doc } = view.state;
|
|
1329
|
+
const at = edge === 'start'
|
|
1330
|
+
? Selection.atStart(doc)
|
|
1331
|
+
: edge === 'end'
|
|
1332
|
+
? Selection.atEnd(doc)
|
|
1333
|
+
: Selection.near(doc.resolve(Math.min(edge, doc.content.size)));
|
|
1310
1334
|
// Flagged, for the reason the leaf's own landing is (`field.ts`, `setCaret`): PM
|
|
1311
1335
|
// focuses with `preventScroll`, so an unflagged dispatch lands the caret where no
|
|
1312
1336
|
// scroller has moved to — a Tab past the right edge of the horizontal scroller a
|
|
1313
1337
|
// wide table lives in, or the row an Enter on the last one appends below the fold.
|
|
1314
1338
|
view.focus();
|
|
1315
|
-
view.dispatch(view.state.tr.setSelection(
|
|
1339
|
+
view.dispatch(view.state.tr.setSelection(at).scrollIntoView());
|
|
1340
|
+
}
|
|
1341
|
+
/** The cell beside `(r, c)` in reading order, a row's end wrapping to the next row's
|
|
1342
|
+
* start; past the first or last cell its row is outside the table. */
|
|
1343
|
+
reading(r, c, dir) {
|
|
1344
|
+
const cols = columnCount(this.props());
|
|
1345
|
+
const next = c + dir;
|
|
1346
|
+
if (next >= cols)
|
|
1347
|
+
return { r: r + 1, c: 0 };
|
|
1348
|
+
if (next < 0)
|
|
1349
|
+
return { r: r - 1, c: cols - 1 };
|
|
1350
|
+
return { r, c: next };
|
|
1316
1351
|
}
|
|
1317
1352
|
/**
|
|
1318
1353
|
* A cell's keys. Traversal is the island's link in the leaf's chain
|
|
@@ -1330,22 +1365,35 @@ class TableIslandView {
|
|
|
1330
1365
|
marks['Mod-i'] = toggleMark(cellSchema.marks.em);
|
|
1331
1366
|
if (cellSchema.marks.underline)
|
|
1332
1367
|
marks['Mod-u'] = toggleMark(cellSchema.marks.underline);
|
|
1333
|
-
//
|
|
1334
|
-
//
|
|
1335
|
-
//
|
|
1336
|
-
//
|
|
1337
|
-
//
|
|
1368
|
+
// The arrows are the grid's own walk, each leaving a cell only from the text's edge
|
|
1369
|
+
// on its side. Up and down reach the cell above or below, `focusCell` clamping at
|
|
1370
|
+
// the top and bottom. Left and right reach the neighbour in reading order, landing
|
|
1371
|
+
// at the edge they entered by and stopping at the first and last cells. No arrow
|
|
1372
|
+
// grows the table, growth being Tab's and Enter's: on a caret key it would make a
|
|
1373
|
+
// walk a growth affordance.
|
|
1374
|
+
const caretAtEdge = (view, dir) => {
|
|
1375
|
+
const { selection } = view?.state ?? {};
|
|
1376
|
+
return (!!view && selection instanceof TextSelection && selection.empty && view.endOfTextblock(dir));
|
|
1377
|
+
};
|
|
1338
1378
|
const walk = (dir) => {
|
|
1339
1379
|
return (_state, _dispatch, view) => {
|
|
1340
|
-
|
|
1341
|
-
if (!view || !(selection instanceof TextSelection) || !selection.empty)
|
|
1342
|
-
return false;
|
|
1343
|
-
if (!view.endOfTextblock(dir))
|
|
1380
|
+
if (!caretAtEdge(view, dir))
|
|
1344
1381
|
return false;
|
|
1345
1382
|
this.focusCell(dir === 'up' ? r - 1 : r + 1, c);
|
|
1346
1383
|
return true;
|
|
1347
1384
|
};
|
|
1348
1385
|
};
|
|
1386
|
+
const cross = (dir) => {
|
|
1387
|
+
return (_state, _dispatch, view) => {
|
|
1388
|
+
if (!caretAtEdge(view, dir < 0 ? 'left' : 'right'))
|
|
1389
|
+
return false;
|
|
1390
|
+
const to = this.reading(r, c, dir);
|
|
1391
|
+
if (to.r < 0 || to.r >= rowCount(this.props()))
|
|
1392
|
+
return false;
|
|
1393
|
+
this.focusCell(to.r, to.c, dir < 0 ? 'end' : 'start');
|
|
1394
|
+
return true;
|
|
1395
|
+
};
|
|
1396
|
+
};
|
|
1349
1397
|
// A block selection outranks the caret it was swept from: the origin cell still
|
|
1350
1398
|
// holds the focus, so its own view is where the block's Backspace lands. Declining
|
|
1351
1399
|
// when no block is held is what leaves an ordinary Backspace to `baseKeymap`.
|
|
@@ -1357,17 +1405,22 @@ class TableIslandView {
|
|
|
1357
1405
|
};
|
|
1358
1406
|
// The line verbs, over the held rectangle and nothing else: with none held every
|
|
1359
1407
|
// one of these declines and the key is the caret's, which is what lets them sit on
|
|
1360
|
-
// keys a cell is already typing under. An arrow
|
|
1361
|
-
//
|
|
1362
|
-
//
|
|
1363
|
-
//
|
|
1364
|
-
//
|
|
1365
|
-
//
|
|
1366
|
-
//
|
|
1408
|
+
// keys a cell is already typing under. An arrow over swept cells lets go of them,
|
|
1409
|
+
// leaving the caret where it is, as an arrow collapses a text selection. Over named
|
|
1410
|
+
// lines it names the line on its own axis through the caret — the one already held
|
|
1411
|
+
// steps to its neighbour and carries the caret, and any other turns into it, which
|
|
1412
|
+
// is how a row becomes the column the caret is in. Alt moves the line instead, and
|
|
1413
|
+
// over a rectangle that is not one it does nothing but keep the key: while a
|
|
1414
|
+
// rectangle is held every arrow is its own, and an Alt+arrow handed back is the
|
|
1415
|
+
// browser's Back on two platforms, mid-gesture.
|
|
1367
1416
|
const line = (axis, by, move) => {
|
|
1368
1417
|
return () => {
|
|
1369
1418
|
if (!this.selected)
|
|
1370
1419
|
return false;
|
|
1420
|
+
if (this.selected.swept && !move) {
|
|
1421
|
+
this.clearSelection();
|
|
1422
|
+
return true;
|
|
1423
|
+
}
|
|
1371
1424
|
const on = this.lineOn(axis);
|
|
1372
1425
|
if (move) {
|
|
1373
1426
|
if (on !== undefined)
|
|
@@ -1399,6 +1452,34 @@ class TableIslandView {
|
|
|
1399
1452
|
return true;
|
|
1400
1453
|
};
|
|
1401
1454
|
};
|
|
1455
|
+
// Select-all grows a rung a press: the cell's text, which the base keymap's
|
|
1456
|
+
// `selectAll` takes, then the caret's row, then every cell, then the document. Any
|
|
1457
|
+
// held rectangle short of every cell grows to every cell.
|
|
1458
|
+
const grow = (state) => {
|
|
1459
|
+
const props = this.props();
|
|
1460
|
+
const every = {
|
|
1461
|
+
r0: 0,
|
|
1462
|
+
c0: 0,
|
|
1463
|
+
r1: rowCount(props) - 1,
|
|
1464
|
+
c1: columnCount(props) - 1,
|
|
1465
|
+
swept: false
|
|
1466
|
+
};
|
|
1467
|
+
const held = this.selected;
|
|
1468
|
+
if (held && sameCells(held, every)) {
|
|
1469
|
+
this.clearSelection();
|
|
1470
|
+
this.outer.focus();
|
|
1471
|
+
return selectAll(this.outer.state, this.outer.dispatch);
|
|
1472
|
+
}
|
|
1473
|
+
if (held)
|
|
1474
|
+
this.select(every);
|
|
1475
|
+
else {
|
|
1476
|
+
const { from, to } = state.selection;
|
|
1477
|
+
if (from > Selection.atStart(state.doc).from || to < Selection.atEnd(state.doc).to)
|
|
1478
|
+
return false;
|
|
1479
|
+
this.selectLine({ axis: 'row', index: r });
|
|
1480
|
+
}
|
|
1481
|
+
return true;
|
|
1482
|
+
};
|
|
1402
1483
|
return {
|
|
1403
1484
|
...marks,
|
|
1404
1485
|
...breakKeymap(cellSchema),
|
|
@@ -1411,17 +1492,15 @@ class TableIslandView {
|
|
|
1411
1492
|
Delete: erase,
|
|
1412
1493
|
ArrowUp: chainCommands(line('row', -1, false), walk('up')),
|
|
1413
1494
|
ArrowDown: chainCommands(line('row', 1, false), walk('down')),
|
|
1414
|
-
ArrowLeft: line('column', -1, false),
|
|
1415
|
-
ArrowRight: line('column', 1, false),
|
|
1495
|
+
ArrowLeft: chainCommands(line('column', -1, false), cross(-1)),
|
|
1496
|
+
ArrowRight: chainCommands(line('column', 1, false), cross(1)),
|
|
1416
1497
|
'Alt-ArrowUp': line('row', -1, true),
|
|
1417
1498
|
'Alt-ArrowDown': line('row', 1, true),
|
|
1418
1499
|
'Alt-ArrowLeft': line('column', -1, true),
|
|
1419
1500
|
'Alt-ArrowRight': line('column', 1, true),
|
|
1420
1501
|
'Shift-ArrowLeft': slide(-1),
|
|
1421
1502
|
'Shift-ArrowRight': slide(1),
|
|
1422
|
-
'
|
|
1423
|
-
'Shift-Mod-e': () => (this.layCells('align', 'center', { r, c }), true),
|
|
1424
|
-
'Shift-Mod-r': () => (this.layCells('align', 'right', { r, c }), true),
|
|
1503
|
+
'Mod-a': grow,
|
|
1425
1504
|
Tab: () => this.step(r, c, 1),
|
|
1426
1505
|
'Shift-Tab': () => this.step(r, c, -1),
|
|
1427
1506
|
Enter: () => {
|
|
@@ -1431,11 +1510,17 @@ class TableIslandView {
|
|
|
1431
1510
|
this.clearSelection();
|
|
1432
1511
|
return true;
|
|
1433
1512
|
}
|
|
1513
|
+
// Past the last row it appends one, and on an empty trailing row it refuses the
|
|
1514
|
+
// row on offer, as Tab's decline does and an empty item's Enter leaves a list:
|
|
1515
|
+
// the row goes and the caret lands past the table. Row 1 is never the one
|
|
1516
|
+
// refused, so the table keeps a row under its first.
|
|
1434
1517
|
const props = this.props();
|
|
1435
|
-
if (r
|
|
1436
|
-
this.write(insertRow(props, r), { r: r + 1, c });
|
|
1437
|
-
else
|
|
1518
|
+
if (r < rowCount(props) - 1)
|
|
1438
1519
|
this.focusCell(r + 1, c);
|
|
1520
|
+
else if (r > 1 && rowEmpty(props, r) && this.leave(1))
|
|
1521
|
+
this.write(deleteRow(props, r));
|
|
1522
|
+
else
|
|
1523
|
+
this.write(insertRow(props, r), { r: r + 1, c });
|
|
1439
1524
|
return true;
|
|
1440
1525
|
},
|
|
1441
1526
|
// Escape climbs a rung a press: the caret's own row, then the island. The row
|
|
@@ -1467,30 +1552,25 @@ class TableIslandView {
|
|
|
1467
1552
|
*/
|
|
1468
1553
|
step(r, c, dir) {
|
|
1469
1554
|
const props = this.props();
|
|
1470
|
-
const
|
|
1471
|
-
|
|
1472
|
-
let nr = r;
|
|
1473
|
-
let nc = c + dir;
|
|
1474
|
-
if (nc >= cols) {
|
|
1475
|
-
nr = r + 1;
|
|
1476
|
-
nc = 0;
|
|
1477
|
-
}
|
|
1478
|
-
else if (nc < 0) {
|
|
1479
|
-
nr = r - 1;
|
|
1480
|
-
nc = cols - 1;
|
|
1481
|
-
}
|
|
1482
|
-
if (nr < 0)
|
|
1555
|
+
const to = this.reading(r, c, dir);
|
|
1556
|
+
if (to.r < 0)
|
|
1483
1557
|
return false;
|
|
1484
|
-
if (
|
|
1558
|
+
if (to.r >= rowCount(props)) {
|
|
1485
1559
|
if (rowEmpty(props, r))
|
|
1486
1560
|
return false;
|
|
1487
|
-
this.write(insertRow(props, r),
|
|
1561
|
+
this.write(insertRow(props, r), to);
|
|
1488
1562
|
}
|
|
1489
1563
|
else
|
|
1490
|
-
this.focusCell(
|
|
1564
|
+
this.focusCell(to.r, to.c);
|
|
1491
1565
|
return true;
|
|
1492
1566
|
}
|
|
1493
1567
|
}
|
|
1568
|
+
/** The table island mounted at `pos` in `view`, which a caret can step into; `undefined`
|
|
1569
|
+
* for any other node, or where no view is mounted. */
|
|
1570
|
+
export function tableAt(view, pos) {
|
|
1571
|
+
const dom = view.nodeDOM(pos);
|
|
1572
|
+
return dom ? mounted.get(dom) : undefined;
|
|
1573
|
+
}
|
|
1494
1574
|
/** The `island_block` node view: a table island's editing surface, and the literal
|
|
1495
1575
|
* placeholder for every other island type. */
|
|
1496
1576
|
export function tableNodeView(deps) {
|