@quillmark/svelte 0.21.0 → 0.23.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/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 +1 -1
- package/dist/core/codec/blocks.js +4 -2
- package/dist/core/codec/cap.d.ts +21 -0
- package/dist/core/codec/cap.js +63 -0
- package/dist/core/codec/code.d.ts +1 -1
- package/dist/core/codec/code.js +1 -1
- 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 -10
- package/dist/core/codec/field.js +54 -18
- package/dist/core/codec/index.d.ts +3 -1
- package/dist/core/codec/index.js +3 -2
- package/dist/core/codec/inputrules.js +1 -1
- package/dist/core/codec/lists.d.ts +3 -2
- package/dist/core/codec/lists.js +21 -5
- package/dist/core/codec/nesting.d.ts +20 -0
- package/dist/core/codec/nesting.js +62 -0
- package/dist/core/codec/prose.css +74 -20
- package/dist/core/codec/schema.d.ts +2 -2
- package/dist/core/codec/schema.js +16 -7
- 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 +1 -7
- package/dist/core/codec/table-view.js +70 -119
- 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/visual/ArrayField.svelte +103 -63
- package/dist/visual/ArrayField.svelte.d.ts +0 -2
- package/dist/visual/Card.svelte +9 -3
- package/dist/visual/Card.svelte.d.ts +2 -2
- package/dist/visual/Field.svelte +33 -17
- package/dist/visual/Field.svelte.d.ts +4 -2
- package/dist/visual/FieldLabel.svelte +19 -33
- package/dist/visual/FieldLabel.svelte.d.ts +4 -2
- package/dist/visual/FormatPopover.svelte +54 -73
- package/dist/visual/MatrixField.svelte +18 -12
- package/dist/visual/NumberField.svelte +32 -3
- package/dist/visual/ObjectField.svelte +50 -22
- package/dist/visual/ObjectField.svelte.d.ts +6 -0
- package/dist/visual/ProseField.svelte +20 -14
- package/dist/visual/ProseField.svelte.d.ts +8 -2
- package/dist/visual/ProseValue.svelte +30 -19
- package/dist/visual/ProseValue.svelte.d.ts +7 -4
- package/dist/visual/TextField.svelte +3 -2
- package/dist/visual/TipsCard.svelte +5 -2
- package/dist/visual/VariantField.svelte +1 -1
- package/dist/visual/VariantField.svelte.d.ts +1 -1
- package/dist/visual/VisualEditorInner.svelte +13 -13
- package/dist/visual/controls.css +19 -1
- package/dist/visual/icons/nodes.d.ts +0 -21
- package/dist/visual/icons/nodes.js +1 -7
- package/dist/visual/leaves.d.ts +12 -9
- package/dist/visual/leaves.js +19 -0
- package/dist/visual/strings.d.ts +16 -5
- package/dist/visual/strings.js +17 -9
- package/dist/visual/structure.d.ts +26 -32
- package/dist/visual/structure.js +71 -53
- package/package.json +3 -3
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
// A task item is a `list_item` whose `checked` is a boolean (CODEC §"Task items"). Its
|
|
2
|
+
// `toDOM` draws a checkbox ahead of the item's blocks, disabled, which is the drawing a
|
|
3
|
+
// held leaf, a copy and `renderContent` take; a leaf that edits mounts the item through
|
|
4
|
+
// `taskItemView`, whose checkbox toggles. A task item is minted by source mode and by a
|
|
5
|
+
// paste alone: no shorthand, command or key makes one.
|
|
6
|
+
import { DOMSerializer } from 'prosemirror-model';
|
|
7
|
+
import { Plugin } from 'prosemirror-state';
|
|
8
|
+
import { splitListItem } from 'prosemirror-schema-list';
|
|
9
|
+
const SVG = 'http://www.w3.org/2000/svg';
|
|
10
|
+
/** The id the checkbox names its item's blocks by, fresh per drawing. */
|
|
11
|
+
let drawn = 0;
|
|
12
|
+
/** The `checked` an `<li>` states: `data-qm-checked`, which `toDOM` writes, else none. */
|
|
13
|
+
export function taskChecked(el) {
|
|
14
|
+
const stated = el.getAttribute('data-qm-checked');
|
|
15
|
+
return stated === null ? null : stated === 'true';
|
|
16
|
+
}
|
|
17
|
+
/** Where an `<li>`'s blocks are: a task item's body, past its checkbox; the item itself
|
|
18
|
+
* otherwise. */
|
|
19
|
+
export function taskBody(el) {
|
|
20
|
+
const li = el;
|
|
21
|
+
const body = li.hasAttribute('data-qm-checked')
|
|
22
|
+
? li.querySelector(':scope > [data-qm-task-body]')
|
|
23
|
+
: null;
|
|
24
|
+
return body ?? li;
|
|
25
|
+
}
|
|
26
|
+
/** A task item's `<li>`: the checkbox, unreachable by the caret, then the body holding
|
|
27
|
+
* the item's blocks, which names it. The mark is the check glyph a matrix tick draws. */
|
|
28
|
+
export function taskDOM(checked) {
|
|
29
|
+
const body = `qm-task-${++drawn}`;
|
|
30
|
+
return [
|
|
31
|
+
'li',
|
|
32
|
+
{ 'data-qm-checked': String(checked) },
|
|
33
|
+
[
|
|
34
|
+
'span',
|
|
35
|
+
{ class: 'qm-task', contenteditable: 'false' },
|
|
36
|
+
[
|
|
37
|
+
'input',
|
|
38
|
+
{
|
|
39
|
+
type: 'checkbox',
|
|
40
|
+
class: 'qm-task-box',
|
|
41
|
+
'aria-labelledby': body,
|
|
42
|
+
checked: checked ? '' : null,
|
|
43
|
+
disabled: ''
|
|
44
|
+
}
|
|
45
|
+
],
|
|
46
|
+
[
|
|
47
|
+
`${SVG} svg`,
|
|
48
|
+
{
|
|
49
|
+
class: 'qm-task-mark',
|
|
50
|
+
viewBox: '0 0 24 24',
|
|
51
|
+
fill: 'none',
|
|
52
|
+
stroke: 'currentColor',
|
|
53
|
+
'stroke-width': '2',
|
|
54
|
+
'stroke-linecap': 'round',
|
|
55
|
+
'stroke-linejoin': 'round',
|
|
56
|
+
'aria-hidden': 'true'
|
|
57
|
+
},
|
|
58
|
+
[`${SVG} path`, { d: 'M20 6 9 17l-5-5' }]
|
|
59
|
+
]
|
|
60
|
+
],
|
|
61
|
+
['div', { id: body, 'data-qm-task-body': '' }, 0]
|
|
62
|
+
];
|
|
63
|
+
}
|
|
64
|
+
const isTask = (node) => node.attrs.checked !== null;
|
|
65
|
+
/**
|
|
66
|
+
* Every `list_item` in a leaf that edits, drawn by its own `toDOM`. A task item's checkbox
|
|
67
|
+
* is enabled, and a change to it is one `checked` transaction, so one `Mod-z` takes it
|
|
68
|
+
* back; where the leaf declines the edit the box returns to the item's value. The box
|
|
69
|
+
* keeps the press from the caret: a mousedown leaves the selection and the focus where
|
|
70
|
+
* they were, and ProseMirror reads no event aimed at it.
|
|
71
|
+
*/
|
|
72
|
+
function taskItemView(node, view, getPos) {
|
|
73
|
+
const { dom, contentDOM } = DOMSerializer.renderSpec(document, node.type.spec.toDOM(node));
|
|
74
|
+
const box = isTask(node)
|
|
75
|
+
? dom.querySelector(':scope > .qm-task > input')
|
|
76
|
+
: null;
|
|
77
|
+
if (box) {
|
|
78
|
+
box.disabled = false;
|
|
79
|
+
box.addEventListener('mousedown', (e) => e.preventDefault());
|
|
80
|
+
box.addEventListener('change', () => {
|
|
81
|
+
const pos = getPos();
|
|
82
|
+
if (pos === undefined || !view.editable) {
|
|
83
|
+
box.checked = node.attrs.checked;
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
view.dispatch(view.state.tr.setNodeAttribute(pos, 'checked', box.checked));
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
return {
|
|
90
|
+
dom,
|
|
91
|
+
contentDOM,
|
|
92
|
+
update(next) {
|
|
93
|
+
if (next.type !== node.type || isTask(next) !== isTask(node))
|
|
94
|
+
return false;
|
|
95
|
+
node = next;
|
|
96
|
+
if (box) {
|
|
97
|
+
box.checked = next.attrs.checked;
|
|
98
|
+
dom.setAttribute('data-qm-checked', String(next.attrs.checked));
|
|
99
|
+
}
|
|
100
|
+
return true;
|
|
101
|
+
},
|
|
102
|
+
stopEvent: (e) => box !== null && e.target === box,
|
|
103
|
+
ignoreMutation: (m) => m.type !== 'selection' && !contentDOM.contains(m.target)
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
/** The node view a block leaf mounts its list items through. */
|
|
107
|
+
export function taskPlugin() {
|
|
108
|
+
return new Plugin({ props: { nodeViews: { list_item: taskItemView } } });
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* `splitListItem`, leaving the item that takes none of a task item's text an open task:
|
|
112
|
+
* the one below, or the one above where Enter is pressed at the head of the item's text.
|
|
113
|
+
* An empty item the split lifts out of a nested list keeps its own `checked`, as a
|
|
114
|
+
* Shift-Tab lift keeps it.
|
|
115
|
+
*/
|
|
116
|
+
export function splitTaskItem(itemType) {
|
|
117
|
+
const split = splitListItem(itemType);
|
|
118
|
+
return (state, dispatch, view) => {
|
|
119
|
+
const { $from, empty } = state.selection;
|
|
120
|
+
const item = $from.depth >= 2 ? $from.node(-1) : null;
|
|
121
|
+
if (!dispatch || item?.type !== itemType || !isTask(item))
|
|
122
|
+
return split(state, dispatch, view);
|
|
123
|
+
const lifted = $from.parent.content.size === 0 && item.childCount === 1;
|
|
124
|
+
const atHead = empty && $from.parentOffset === 0 && $from.index(-1) === 0;
|
|
125
|
+
return split(state, (tr) => {
|
|
126
|
+
const $at = tr.selection.$from;
|
|
127
|
+
const below = $at.before($at.depth - 1);
|
|
128
|
+
if (lifted)
|
|
129
|
+
tr.setNodeAttribute(below, 'checked', item.attrs.checked);
|
|
130
|
+
else if (atHead) {
|
|
131
|
+
const above = tr.doc.resolve(below).nodeBefore;
|
|
132
|
+
if (above)
|
|
133
|
+
tr.setNodeAttribute(below - above.nodeSize, 'checked', false);
|
|
134
|
+
}
|
|
135
|
+
else
|
|
136
|
+
tr.setNodeAttribute(below, 'checked', false);
|
|
137
|
+
dispatch(tr);
|
|
138
|
+
});
|
|
139
|
+
};
|
|
140
|
+
}
|
package/dist/core/errors.d.ts
CHANGED
|
@@ -11,8 +11,8 @@ export type EditorErrorCode =
|
|
|
11
11
|
* the whole projection instead, so the store is correct and this field's
|
|
12
12
|
* identity anchors were paid. */
|
|
13
13
|
| 'commit-fallback'
|
|
14
|
-
/** The overwrite fallback also failed, or the typed writer refused a `
|
|
15
|
-
*
|
|
14
|
+
/** The overwrite fallback also failed, or the typed writer refused a `text` leaf's
|
|
15
|
+
* text: the optimistic PM state stands and the store is stale for this
|
|
16
16
|
* field. The one code here that leaves damage. */
|
|
17
17
|
| 'commit-lost'
|
|
18
18
|
/** A card operation (add, move, remove, retype) that threw. The document is
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
An `array` field → an add/remove repeater. Elements commit by value: every
|
|
3
3
|
edit / add / remove / move rebuilds the whole array and hands it to the parent's typed
|
|
4
4
|
`writer.set(field, wholeArray)` (arrays are not op-addressed). Element control
|
|
5
|
-
by `items.type`: `richtext` / `
|
|
5
|
+
by `items.type`: `richtext` / `text` → a prose element
|
|
6
6
|
({@link ProseValue}), `object` → a summary row that opens onto a subform, or a
|
|
7
7
|
table's row where the array asks for one and the cells are short, everything else →
|
|
8
8
|
a text input. The add chip sits in the label header row (space-between with the field
|
|
@@ -71,9 +71,10 @@
|
|
|
71
71
|
controlKind,
|
|
72
72
|
declaredContent,
|
|
73
73
|
humanize,
|
|
74
|
-
|
|
74
|
+
literalText,
|
|
75
75
|
rowSummary,
|
|
76
76
|
schemaAt,
|
|
77
|
+
textCap,
|
|
77
78
|
type ArrayLayout
|
|
78
79
|
} from './structure.js';
|
|
79
80
|
import { splitDeep, unrouted, type DeepDiagnostic } from './diagnostics.js';
|
|
@@ -105,8 +106,6 @@
|
|
|
105
106
|
max?: number;
|
|
106
107
|
/** Accessible-name prefix for the element controls (`label` + 1-based index). */
|
|
107
108
|
label?: string;
|
|
108
|
-
/** No-default field → a persistent required `*` on the label. */
|
|
109
|
-
required?: boolean;
|
|
110
109
|
/** Schema `description`: the label's help affordance. */
|
|
111
110
|
description?: string;
|
|
112
111
|
/** The label's own DOM id. An array is a group (N inputs, no single `for`
|
|
@@ -140,7 +139,6 @@
|
|
|
140
139
|
layout = 'list',
|
|
141
140
|
max,
|
|
142
141
|
label,
|
|
143
|
-
required,
|
|
144
142
|
description,
|
|
145
143
|
labelId,
|
|
146
144
|
descriptionId,
|
|
@@ -519,9 +517,10 @@
|
|
|
519
517
|
* any cell an open row holds: Alt+↑/↓, the table island's own binding. The nearest
|
|
520
518
|
* row answers, or none does: a press inside a nested row is that row's whether it
|
|
521
519
|
* holds a reorder or not, so a key in a table's cell or a list of strings' input
|
|
522
|
-
* moves nothing, and never the record around it.
|
|
520
|
+
* moves nothing, and never the record around it. A held drag owns the row until its
|
|
521
|
+
* release. */
|
|
523
522
|
function onRowKey(e: KeyboardEvent, k: number): void {
|
|
524
|
-
if (!e.altKey || e.shiftKey || e.ctrlKey || e.metaKey || e.isComposing) return;
|
|
523
|
+
if (!e.altKey || e.shiftKey || e.ctrlKey || e.metaKey || e.isComposing || drag) return;
|
|
525
524
|
// A cell that took the key — a select opening, a date segment stepping, a prose
|
|
526
525
|
// leaf joining — owns it.
|
|
527
526
|
if (e.defaultPrevented) return;
|
|
@@ -536,42 +535,53 @@
|
|
|
536
535
|
|
|
537
536
|
// ── The drag handle ──────────────────────────────────────────────────────────
|
|
538
537
|
// Press-and-drag the handle moves its row; a press that does not travel past the dead
|
|
539
|
-
// zone does nothing. The
|
|
540
|
-
//
|
|
541
|
-
//
|
|
538
|
+
// zone does nothing. The drop is the row under the pointer, read off the rows where
|
|
539
|
+
// they stand, so a scroll by any hand re-picks it. Held in an edge band of its
|
|
540
|
+
// scrollport, the drag scrolls it toward the rows out of view, once the pointer has
|
|
541
|
+
// been outside the band: a press on a row already in the band does not scroll. Nothing
|
|
542
|
+
// reflows until the release (the lift is a tone and the drop rule is out of flow), and
|
|
543
|
+
// the release is the one splice `move` makes. A handle that loses the pointer, or
|
|
544
|
+
// leaves the DOM, ends the drag with no move.
|
|
542
545
|
const DEAD_ZONE = 3;
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
heads: { start: number; end: number }[];
|
|
549
|
-
};
|
|
546
|
+
/** The band at a scrollport's top and bottom edges that scrolls it under a held drag,
|
|
547
|
+
* and the most one frame scrolls, reached at the edge itself. */
|
|
548
|
+
const EDGE = 48;
|
|
549
|
+
const STEP = 16;
|
|
550
|
+
type RowDrag = { k: number; origin: number; y: number; engaged: boolean; drop: number };
|
|
550
551
|
let drag = $state<RowDrag | undefined>();
|
|
552
|
+
let follow: { grip: Element; port: Element; armed: boolean; frame: number } | undefined;
|
|
551
553
|
function onHandleDown(e: PointerEvent, k: number): void {
|
|
552
554
|
if (e.button !== 0) return;
|
|
553
555
|
e.preventDefault();
|
|
554
|
-
(
|
|
555
|
-
|
|
556
|
+
release();
|
|
557
|
+
const grip = e.currentTarget as HTMLElement;
|
|
558
|
+
grip.setPointerCapture?.(e.pointerId);
|
|
559
|
+
drag = { k, origin: e.clientY, y: e.clientY, engaged: false, drop: k };
|
|
560
|
+
follow = { grip, port: scrollport(grip), armed: false, frame: 0 };
|
|
556
561
|
}
|
|
557
562
|
function onHandleMove(e: PointerEvent): void {
|
|
558
563
|
if (!drag) return;
|
|
564
|
+
drag.y = e.clientY;
|
|
559
565
|
if (!drag.engaged) {
|
|
560
566
|
if (Math.abs(e.clientY - drag.origin) < DEAD_ZONE) return;
|
|
561
567
|
drag.engaged = true;
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
return { start: box?.top ?? 0, end: box?.bottom ?? 0 };
|
|
565
|
-
});
|
|
568
|
+
addEventListener('scroll', repick, { capture: true, passive: true });
|
|
569
|
+
if (follow) follow.frame = requestAnimationFrame(edgeScroll);
|
|
566
570
|
}
|
|
567
|
-
|
|
571
|
+
repick();
|
|
572
|
+
}
|
|
573
|
+
function repick(): void {
|
|
574
|
+
if (drag?.engaged) drag.drop = dropIndex(drag.k, drag.y);
|
|
568
575
|
}
|
|
576
|
+
const rowBox = (i: number): DOMRect | undefined => rowEls[ids[i]]?.getBoundingClientRect();
|
|
569
577
|
/** The row the pointer is over, or the nearest one past either end. */
|
|
570
|
-
function dropIndex(
|
|
571
|
-
let best =
|
|
578
|
+
function dropIndex(k: number, y: number): number {
|
|
579
|
+
let best = k;
|
|
572
580
|
let nearest = Infinity;
|
|
573
|
-
|
|
574
|
-
const
|
|
581
|
+
ids.forEach((_, i) => {
|
|
582
|
+
const box = rowBox(i);
|
|
583
|
+
if (!box) return;
|
|
584
|
+
const gap = y < box.top ? box.top - y : y > box.bottom ? y - box.bottom : 0;
|
|
575
585
|
if (gap < nearest) {
|
|
576
586
|
nearest = gap;
|
|
577
587
|
best = i;
|
|
@@ -579,9 +589,45 @@
|
|
|
579
589
|
});
|
|
580
590
|
return best;
|
|
581
591
|
}
|
|
592
|
+
const page = (): Element => document.scrollingElement ?? document.documentElement;
|
|
593
|
+
/** The nearest ancestor that scrolls vertically, else the page. */
|
|
594
|
+
function scrollport(from: Element): Element {
|
|
595
|
+
for (let el = from.parentElement; el; el = el.parentElement) {
|
|
596
|
+
const { overflowY } = getComputedStyle(el);
|
|
597
|
+
if (/auto|scroll/.test(overflowY) && el.scrollHeight > el.clientHeight) return el;
|
|
598
|
+
}
|
|
599
|
+
return page();
|
|
600
|
+
}
|
|
601
|
+
function edgeScroll(): void {
|
|
602
|
+
if (!follow || !drag?.engaged) return;
|
|
603
|
+
if (!follow.grip.isConnected) {
|
|
604
|
+
drag = undefined;
|
|
605
|
+
release();
|
|
606
|
+
return;
|
|
607
|
+
}
|
|
608
|
+
const { port } = follow;
|
|
609
|
+
const box = port === page() ? undefined : port.getBoundingClientRect();
|
|
610
|
+
const top = Math.max(box?.top ?? 0, 0);
|
|
611
|
+
const bottom = Math.min(box?.bottom ?? innerHeight, innerHeight);
|
|
612
|
+
const into = Math.min(drag.y - top - EDGE, 0) + Math.max(drag.y - bottom + EDGE, 0);
|
|
613
|
+
if (!into) follow.armed = true;
|
|
614
|
+
// Only toward rows still out of view: past the list's end there is nothing to reach.
|
|
615
|
+
const beyond =
|
|
616
|
+
into < 0 ? (rowBox(0)?.top ?? top) < top : (rowBox(ids.length - 1)?.bottom ?? 0) > bottom;
|
|
617
|
+
if (into && follow.armed && beyond)
|
|
618
|
+
port.scrollBy({ top: STEP * Math.max(-1, Math.min(1, into / EDGE)), behavior: 'instant' });
|
|
619
|
+
follow.frame = requestAnimationFrame(edgeScroll);
|
|
620
|
+
}
|
|
621
|
+
function release(): void {
|
|
622
|
+
if (follow) cancelAnimationFrame(follow.frame);
|
|
623
|
+
follow = undefined;
|
|
624
|
+
removeEventListener('scroll', repick, { capture: true });
|
|
625
|
+
}
|
|
626
|
+
onDestroy(release);
|
|
582
627
|
function onHandleUp(e: PointerEvent): void {
|
|
583
628
|
const done = drag;
|
|
584
629
|
drag = undefined;
|
|
630
|
+
release();
|
|
585
631
|
if (!done?.engaged || e.type !== 'pointerup') return;
|
|
586
632
|
move(done.k, done.drop);
|
|
587
633
|
}
|
|
@@ -600,14 +646,7 @@
|
|
|
600
646
|
>
|
|
601
647
|
<div class="qm-array-header">
|
|
602
648
|
{#if label != null}
|
|
603
|
-
<FieldLabel
|
|
604
|
-
{label}
|
|
605
|
-
id={labelId}
|
|
606
|
-
{descriptionId}
|
|
607
|
-
onActivate={focus}
|
|
608
|
-
{required}
|
|
609
|
-
{description}
|
|
610
|
-
/>
|
|
649
|
+
<FieldLabel {label} id={labelId} {descriptionId} onActivate={focus} {description} />
|
|
611
650
|
{:else}
|
|
612
651
|
<span></span>
|
|
613
652
|
{/if}
|
|
@@ -632,15 +671,15 @@
|
|
|
632
671
|
each block's one child: a branch inside the block would stand between them. -->
|
|
633
672
|
{#if table}
|
|
634
673
|
<div
|
|
635
|
-
class={['qm-array-rows qm-array-table', { empty: ids.length === 0 }]}
|
|
674
|
+
class={['qm-array-rows qm-array-table qm-wash', { empty: ids.length === 0 }]}
|
|
636
675
|
data-default={defaulted ? '' : undefined}
|
|
637
676
|
bind:this={rowsEl}
|
|
638
677
|
>
|
|
639
678
|
<div class="qm-array-table-scroller" style:--table-cols={columns.length}>
|
|
640
679
|
<!-- The header names the columns, so a cell carries no label of its own: its
|
|
641
680
|
accessible name is the row's and the column's composed (`ObjectField`). The
|
|
642
|
-
|
|
643
|
-
|
|
681
|
+
guidance affordance rides the header, being the column's rather than any
|
|
682
|
+
one cell's. -->
|
|
644
683
|
<div class="qm-array-table-head">
|
|
645
684
|
{#each columns as [key, sub] (key)}
|
|
646
685
|
<div
|
|
@@ -652,7 +691,6 @@
|
|
|
652
691
|
<FieldLabel
|
|
653
692
|
label={columnTitle(key, sub)}
|
|
654
693
|
descriptionId={idBase != null ? propertyDomIds(idBase, key).description : undefined}
|
|
655
|
-
required={obliged(sub)}
|
|
656
694
|
description={sub.description}
|
|
657
695
|
/>
|
|
658
696
|
</div>
|
|
@@ -660,7 +698,11 @@
|
|
|
660
698
|
<span class="qm-array-table-head-end"></span>
|
|
661
699
|
</div>
|
|
662
700
|
{#each ids as id, k (id)}
|
|
663
|
-
<div
|
|
701
|
+
<div
|
|
702
|
+
class="qm-array-row qm-array-table-row qm-wash"
|
|
703
|
+
data-qm-index={k}
|
|
704
|
+
bind:this={rowEls[id]}
|
|
705
|
+
>
|
|
664
706
|
<ObjectField
|
|
665
707
|
bare
|
|
666
708
|
bind:this={cellEls[id]}
|
|
@@ -680,7 +722,7 @@
|
|
|
680
722
|
</div>
|
|
681
723
|
{:else if control === 'object'}
|
|
682
724
|
<div
|
|
683
|
-
class={['qm-array-rows', { empty: ids.length === 0 }]}
|
|
725
|
+
class={['qm-array-rows qm-wash', { empty: ids.length === 0 }]}
|
|
684
726
|
data-default={defaulted ? '' : undefined}
|
|
685
727
|
bind:this={rowsEl}
|
|
686
728
|
>
|
|
@@ -690,10 +732,11 @@
|
|
|
690
732
|
interaction of the row's own: the row is no tab stop. -->
|
|
691
733
|
<div
|
|
692
734
|
class={[
|
|
693
|
-
'qm-array-row qm-element',
|
|
735
|
+
'qm-array-row qm-element qm-wash',
|
|
694
736
|
{ open: open.has(id), lifted: drag?.engaged && drag.k === k }
|
|
695
737
|
]}
|
|
696
738
|
data-drop={dropSide(k)}
|
|
739
|
+
data-qm-index={k}
|
|
697
740
|
bind:this={rowEls[id]}
|
|
698
741
|
animate:reorder={arm.armed}
|
|
699
742
|
onkeydown={(e) => onRowKey(e, k)}
|
|
@@ -708,6 +751,7 @@
|
|
|
708
751
|
type="button"
|
|
709
752
|
class="qm-control-box qm-focus-ring qm-element-summary"
|
|
710
753
|
aria-expanded={open.has(id)}
|
|
754
|
+
aria-keyshortcuts="Alt+ArrowUp Alt+ArrowDown"
|
|
711
755
|
onclick={() => toggleRow(id)}
|
|
712
756
|
>
|
|
713
757
|
<!-- Leading, and it rotates: trailing is the figure for pushing a new
|
|
@@ -745,18 +789,19 @@
|
|
|
745
789
|
</div>
|
|
746
790
|
{:else}
|
|
747
791
|
<div
|
|
748
|
-
class={['qm-array-rows', { empty: ids.length === 0 }]}
|
|
792
|
+
class={['qm-array-rows qm-wash', { empty: ids.length === 0 }]}
|
|
749
793
|
data-default={defaulted ? '' : undefined}
|
|
750
794
|
bind:this={rowsEl}
|
|
751
795
|
>
|
|
752
796
|
{#each ids as id, k (id)}
|
|
753
|
-
<div class="qm-array-row" bind:this={rowEls[id]}>
|
|
797
|
+
<div class="qm-array-row qm-wash" data-qm-index={k} bind:this={rowEls[id]}>
|
|
754
798
|
{#if control === 'prose'}
|
|
755
799
|
<ProseValue
|
|
756
800
|
bind:this={els[id]}
|
|
757
801
|
content={() => readAt([k])}
|
|
758
802
|
value={arr[k]}
|
|
759
|
-
plaintext={items != null &&
|
|
803
|
+
plaintext={items != null && literalText(items)}
|
|
804
|
+
cap={textCap(items)}
|
|
760
805
|
label={rowName(k)}
|
|
761
806
|
onChange={(rt) => commitElement(k, rt)}
|
|
762
807
|
onKey={(e) => onElementKey(e, k)}
|
|
@@ -794,20 +839,20 @@
|
|
|
794
839
|
{/snippet}
|
|
795
840
|
|
|
796
841
|
<!-- An `object` row's drag handle, a table grip's twin hung off the row's start edge.
|
|
797
|
-
|
|
798
|
-
|
|
842
|
+
Pointer-only and out of the accessibility tree: a press on it does nothing, so a
|
|
843
|
+
button there would be one a screen reader activates to no effect. Its twin is
|
|
844
|
+
Alt+↑/↓ from anywhere in the row, which the summary announces. -->
|
|
799
845
|
{#snippet handle(k: number)}
|
|
800
|
-
<
|
|
801
|
-
type="button"
|
|
846
|
+
<span
|
|
802
847
|
class="qm-row-handle"
|
|
803
|
-
|
|
848
|
+
aria-hidden="true"
|
|
804
849
|
title={t.strings.arrayDrag}
|
|
805
|
-
aria-label={rowAction(t.strings.arrayDrag, k)}
|
|
806
850
|
onpointerdown={(e) => onHandleDown(e, k)}
|
|
807
851
|
onpointermove={onHandleMove}
|
|
808
852
|
onpointerup={onHandleUp}
|
|
809
853
|
onpointercancel={onHandleUp}
|
|
810
|
-
|
|
854
|
+
onlostpointercapture={onHandleUp}
|
|
855
|
+
><span class="qm-row-handle-bar"><Icon name="grip-vertical" /></span></span
|
|
811
856
|
>
|
|
812
857
|
{/snippet}
|
|
813
858
|
|
|
@@ -841,14 +886,13 @@
|
|
|
841
886
|
}
|
|
842
887
|
/* The elements, in a box of their own: the arrival wash blooms here rather than over
|
|
843
888
|
the wrapper `Field` hands this component (`washBox`), which is the label's box too.
|
|
844
|
-
|
|
845
|
-
|
|
889
|
+
Rounded to the rung a row's own box draws, the way `.qm-field-control` is for every
|
|
890
|
+
other control.
|
|
846
891
|
|
|
847
892
|
`display: none` when there are no elements, so the header does not stand a gap above
|
|
848
893
|
an empty box; a wash over it then paints nothing, which is what an array with
|
|
849
894
|
nothing in it has to show. */
|
|
850
895
|
.qm-array-rows {
|
|
851
|
-
position: relative;
|
|
852
896
|
display: flex;
|
|
853
897
|
flex-direction: column;
|
|
854
898
|
gap: var(--_qm-space);
|
|
@@ -873,7 +917,6 @@
|
|
|
873
917
|
against. `minmax(0, …)` because a long unbroken value grows an `auto` track, and the
|
|
874
918
|
edge with it. */
|
|
875
919
|
.qm-array-row {
|
|
876
|
-
position: relative;
|
|
877
920
|
display: grid;
|
|
878
921
|
grid-template-columns: minmax(0, 1fr);
|
|
879
922
|
row-gap: var(--_qm-space);
|
|
@@ -953,9 +996,6 @@
|
|
|
953
996
|
display: flex;
|
|
954
997
|
align-items: center;
|
|
955
998
|
justify-content: center;
|
|
956
|
-
padding: 0;
|
|
957
|
-
border: none;
|
|
958
|
-
background: none;
|
|
959
999
|
color: var(--_qm-ink-label);
|
|
960
1000
|
cursor: grab;
|
|
961
1001
|
touch-action: none;
|
|
@@ -988,7 +1028,7 @@
|
|
|
988
1028
|
.qm-element.lifted > .qm-element-head > .qm-element-summary {
|
|
989
1029
|
opacity: var(--_qm-opacity-muted);
|
|
990
1030
|
}
|
|
991
|
-
.qm-element[data-drop]::
|
|
1031
|
+
.qm-element[data-drop]::before {
|
|
992
1032
|
content: '';
|
|
993
1033
|
position: absolute;
|
|
994
1034
|
inset-inline: 0;
|
|
@@ -997,10 +1037,10 @@
|
|
|
997
1037
|
border-radius: var(--_qm-radius-pill);
|
|
998
1038
|
pointer-events: none;
|
|
999
1039
|
}
|
|
1000
|
-
.qm-element[data-drop='before']::
|
|
1040
|
+
.qm-element[data-drop='before']::before {
|
|
1001
1041
|
top: calc(-0.5 * (var(--_qm-space) + var(--_qm-ring-width)));
|
|
1002
1042
|
}
|
|
1003
|
-
.qm-element[data-drop='after']::
|
|
1043
|
+
.qm-element[data-drop='after']::before {
|
|
1004
1044
|
bottom: calc(-0.5 * (var(--_qm-space) + var(--_qm-ring-width)));
|
|
1005
1045
|
}
|
|
1006
1046
|
/* ── An object element ──────────────────────────────────────────────────────
|
|
@@ -15,8 +15,6 @@ interface Props {
|
|
|
15
15
|
max?: number;
|
|
16
16
|
/** Accessible-name prefix for the element controls (`label` + 1-based index). */
|
|
17
17
|
label?: string;
|
|
18
|
-
/** No-default field → a persistent required `*` on the label. */
|
|
19
|
-
required?: boolean;
|
|
20
18
|
/** Schema `description`: the label's help affordance. */
|
|
21
19
|
description?: string;
|
|
22
20
|
/** The label's own DOM id. An array is a group (N inputs, no single `for`
|
package/dist/visual/Card.svelte
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
// component renders standalone too.
|
|
11
11
|
const t = wording();
|
|
12
12
|
import Icon from './icons/Icon.svelte';
|
|
13
|
-
import type { Document, Quill, Addr, Diagnostic } from '@quillmark/wasm';
|
|
13
|
+
import type { Document, Quill, Addr, Diagnostic, PathStep } from '@quillmark/wasm';
|
|
14
14
|
import type { EditorErrorHandler } from '../core/errors.js';
|
|
15
15
|
import type { LeafRegistry } from './leaves.js';
|
|
16
16
|
import type { CardModel, FieldModel } from './structure.js';
|
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
kinds: string[];
|
|
49
49
|
ops: CardOps;
|
|
50
50
|
enumDisallowed?: 'hide' | 'disable';
|
|
51
|
-
onFocus?: (addr: Addr) => void;
|
|
51
|
+
onFocus?: (addr: Addr, steps?: readonly PathStep[]) => void;
|
|
52
52
|
onCaretMove?: (addr: Addr, pos: number) => void;
|
|
53
53
|
onChange?: (addr: Addr) => void;
|
|
54
54
|
onError?: EditorErrorHandler;
|
|
@@ -279,7 +279,7 @@
|
|
|
279
279
|
<div class="qm-group-head">
|
|
280
280
|
<button
|
|
281
281
|
type="button"
|
|
282
|
-
class="qm-group-header"
|
|
282
|
+
class="qm-group-header qm-focus-ring"
|
|
283
283
|
id={headIds?.label}
|
|
284
284
|
bind:this={headers[group]}
|
|
285
285
|
aria-expanded={isOpen}
|
|
@@ -538,6 +538,12 @@
|
|
|
538
538
|
.qm-card-title:focus {
|
|
539
539
|
color: var(--_qm-ink);
|
|
540
540
|
}
|
|
541
|
+
/* The automatic title, at the label ink every placeholder takes rather than the UA's
|
|
542
|
+
grey, and steady under focus, where a typed title steps to the ink. */
|
|
543
|
+
.qm-card-title::placeholder {
|
|
544
|
+
color: var(--_qm-ink-label);
|
|
545
|
+
opacity: 1;
|
|
546
|
+
}
|
|
541
547
|
.qm-card-body {
|
|
542
548
|
display: flex;
|
|
543
549
|
flex-direction: column;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Document, Quill, Addr, Diagnostic } from '@quillmark/wasm';
|
|
1
|
+
import type { Document, Quill, Addr, Diagnostic, PathStep } from '@quillmark/wasm';
|
|
2
2
|
import type { EditorErrorHandler } from '../core/errors.js';
|
|
3
3
|
import type { LeafRegistry } from './leaves.js';
|
|
4
4
|
import type { CardModel } from './structure.js';
|
|
@@ -27,7 +27,7 @@ interface Props {
|
|
|
27
27
|
kinds: string[];
|
|
28
28
|
ops: CardOps;
|
|
29
29
|
enumDisallowed?: 'hide' | 'disable';
|
|
30
|
-
onFocus?: (addr: Addr) => void;
|
|
30
|
+
onFocus?: (addr: Addr, steps?: readonly PathStep[]) => void;
|
|
31
31
|
onCaretMove?: (addr: Addr, pos: number) => void;
|
|
32
32
|
onChange?: (addr: Addr) => void;
|
|
33
33
|
onError?: EditorErrorHandler;
|