@quillmark/svelte 0.21.0 → 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/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/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 +15 -10
- package/dist/core/codec/field.js +46 -17
- package/dist/core/codec/index.d.ts +1 -1
- package/dist/core/codec/index.js +2 -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 +101 -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 +20 -16
- package/dist/visual/Field.svelte.d.ts +4 -2
- package/dist/visual/FieldLabel.svelte +5 -33
- package/dist/visual/FieldLabel.svelte.d.ts +0 -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 +38 -20
- 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 +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 +10 -5
- package/dist/visual/strings.js +16 -9
- package/dist/visual/structure.d.ts +21 -31
- package/dist/visual/structure.js +54 -51
- package/package.json +3 -3
package/dist/core/codec/index.js
CHANGED
|
@@ -13,7 +13,7 @@ import './prose.css';
|
|
|
13
13
|
// selection (`visual/FormatPopover.svelte`), so it is reached off-barrel.
|
|
14
14
|
export { rangeAnchor } from './anchor.js';
|
|
15
15
|
// The prose leaf.
|
|
16
|
-
export { createField, emptyContent, heldAttributes, proseAttributes, proseLeafPlugins } from './field.js';
|
|
16
|
+
export { createField, emptyContent, heldAttributes, isEmptyContent, proseAttributes, proseLeafPlugins } from './field.js';
|
|
17
17
|
// The table island's chrome vocabulary: the visual tier's `strings` set extends it,
|
|
18
18
|
// so the island's wording is overridden beside every other key.
|
|
19
19
|
export { DEFAULT_TABLE_STRINGS } from './table-view.js';
|
|
@@ -37,7 +37,7 @@ export { pmToContent, contentEdit, lower } from './encode.js';
|
|
|
37
37
|
export { usvToPM, pmToUsv, buildLineIndex } from './positions.js';
|
|
38
38
|
// Reconciliation gate.
|
|
39
39
|
export { createReconciler, contentEqual, valueEqual } from './reconcile.js';
|
|
40
|
-
// Input rules (`createField` mounts them unless `noInputRules
|
|
40
|
+
// Input rules (`createField` mounts them unless `noInputRules`, or over a mark-free schema).
|
|
41
41
|
export { inputRulesPlugin } from './inputrules.js';
|
|
42
42
|
// The body leaf's structural keys: `createField` binds the composed chains
|
|
43
43
|
// (`bodyKeymap`); the suite drives both it and the list link directly.
|
|
@@ -58,7 +58,7 @@ export function markdownInputRules(schema) {
|
|
|
58
58
|
const rules = [];
|
|
59
59
|
const m = schema.marks;
|
|
60
60
|
// Inline marks. Non-greedy capture between matching delimiters. Built only where the
|
|
61
|
-
// schema has the mark: the inline schema omits them, and a
|
|
61
|
+
// schema has the mark: the inline schema omits them, and a `text` one declares none
|
|
62
62
|
// at all.
|
|
63
63
|
if (m.strong)
|
|
64
64
|
rules.push(markInputRule(/\*\*([^*]+)\*\*$/, m.strong, 2));
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { Schema } from 'prosemirror-model';
|
|
2
2
|
import type { Command } from 'prosemirror-state';
|
|
3
3
|
/**
|
|
4
|
-
* The list bindings for a block-schema leaf: `{}` for the
|
|
4
|
+
* The list bindings for a block-schema leaf: `{}` for the one-line and plain
|
|
5
5
|
* schemas, which declare no list nodes.
|
|
6
6
|
*
|
|
7
7
|
* `Tab` / `Shift-Tab` hold the list link of the body's Tab chain. Surfaces that own
|
|
@@ -13,7 +13,8 @@ import type { Command } from 'prosemirror-state';
|
|
|
13
13
|
* shell structural keymap (VISUAL_EDITOR §Settled and open).
|
|
14
14
|
*
|
|
15
15
|
* `Enter` is a chain in precedence order: the escape-above gesture, then
|
|
16
|
-
* `splitListItem`,
|
|
16
|
+
* `splitListItem` (`splitTaskItem`, which leaves a task item's new half an open task),
|
|
17
|
+
* then `liftListItem`. The middle link carries two behaviors of
|
|
17
18
|
* its own: it splits a non-empty item, and on an empty item it either splits the
|
|
18
19
|
* wrapping item (a nested empty item, so Enter lifts exactly one level) or bails
|
|
19
20
|
* so the third link lifts a top-level empty item out to a paragraph. `Backspace` lifts
|
package/dist/core/codec/lists.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
import { liftListItem, sinkListItem
|
|
1
|
+
import { liftListItem, sinkListItem } from 'prosemirror-schema-list';
|
|
2
2
|
import { EditorState, Selection, TextSelection } from 'prosemirror-state';
|
|
3
3
|
import { chainCommands, joinTextblockBackward, joinTextblockForward } from 'prosemirror-commands';
|
|
4
|
+
import { withinNesting } from './nesting.js';
|
|
5
|
+
import { splitTaskItem } from './tasks.js';
|
|
4
6
|
/** The caret's enclosing `list_item` when it sits at the very start of that item's
|
|
5
7
|
* first block: the position both structural keys branch on. `null` otherwise
|
|
6
8
|
* (mid-text, a non-first block of a multi-paragraph item, a non-empty selection),
|
|
@@ -187,8 +189,21 @@ function backspaceBelow(itemType, press) {
|
|
|
187
189
|
return press(EditorState.create({ doc: state.doc, selection: below }), dispatch && keepCaret);
|
|
188
190
|
};
|
|
189
191
|
}
|
|
192
|
+
/** Tab → `sinkListItem`, declined where the sink would nest past `MAX_NESTING_DEPTH`
|
|
193
|
+
* (`nesting.ts`), so Tab there leaves the body as it does on an item with nothing to
|
|
194
|
+
* sink under. */
|
|
195
|
+
function sinkWithin(itemType) {
|
|
196
|
+
const sink = sinkListItem(itemType);
|
|
197
|
+
return (state, dispatch) => {
|
|
198
|
+
let sunk;
|
|
199
|
+
if (!sink(state, (tr) => (sunk = tr)) || !sunk || !withinNesting(sunk.doc))
|
|
200
|
+
return false;
|
|
201
|
+
dispatch?.(sunk);
|
|
202
|
+
return true;
|
|
203
|
+
};
|
|
204
|
+
}
|
|
190
205
|
/**
|
|
191
|
-
* The list bindings for a block-schema leaf: `{}` for the
|
|
206
|
+
* The list bindings for a block-schema leaf: `{}` for the one-line and plain
|
|
192
207
|
* schemas, which declare no list nodes.
|
|
193
208
|
*
|
|
194
209
|
* `Tab` / `Shift-Tab` hold the list link of the body's Tab chain. Surfaces that own
|
|
@@ -200,7 +215,8 @@ function backspaceBelow(itemType, press) {
|
|
|
200
215
|
* shell structural keymap (VISUAL_EDITOR §Settled and open).
|
|
201
216
|
*
|
|
202
217
|
* `Enter` is a chain in precedence order: the escape-above gesture, then
|
|
203
|
-
* `splitListItem`,
|
|
218
|
+
* `splitListItem` (`splitTaskItem`, which leaves a task item's new half an open task),
|
|
219
|
+
* then `liftListItem`. The middle link carries two behaviors of
|
|
204
220
|
* its own: it splits a non-empty item, and on an empty item it either splits the
|
|
205
221
|
* wrapping item (a nested empty item, so Enter lifts exactly one level) or bails
|
|
206
222
|
* so the third link lifts a top-level empty item out to a paragraph. `Backspace` lifts
|
|
@@ -217,9 +233,9 @@ export function listKeymap(schema) {
|
|
|
217
233
|
return {};
|
|
218
234
|
const backspace = chainCommands(liftAtItemStart(itemType), joinOntoList(itemType));
|
|
219
235
|
return {
|
|
220
|
-
Tab:
|
|
236
|
+
Tab: sinkWithin(itemType),
|
|
221
237
|
'Shift-Tab': liftListItem(itemType),
|
|
222
|
-
Enter: chainCommands(paragraphAboveList(itemType, paragraph),
|
|
238
|
+
Enter: chainCommands(paragraphAboveList(itemType, paragraph), splitTaskItem(itemType), liftListItem(itemType)),
|
|
223
239
|
Backspace: backspace,
|
|
224
240
|
Delete: backspaceBelow(itemType, backspace)
|
|
225
241
|
};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { Node as PMNode } from 'prosemirror-model';
|
|
2
|
+
import { Plugin } from 'prosemirror-state';
|
|
3
|
+
/** The store's bound on the containers around one line: a commit nesting past it is
|
|
4
|
+
* refused, and `@quillmark/wasm` exports no constant to read it from. */
|
|
5
|
+
export declare const MAX_NESTING_DEPTH = 12;
|
|
6
|
+
/** The most containers any block in `doc` stands inside, counted as the store counts
|
|
7
|
+
* them: a `list_item` or a `blockquote` is one, the list around an item none, and no
|
|
8
|
+
* node holds an element (`decode.ts`). */
|
|
9
|
+
export declare function nestingDepth(doc: PMNode): number;
|
|
10
|
+
/** Whether `doc` nests within {@link MAX_NESTING_DEPTH}: what a command that nests asks
|
|
11
|
+
* of the doc it would dispatch. */
|
|
12
|
+
export declare function withinNesting(doc: PMNode): boolean;
|
|
13
|
+
/**
|
|
14
|
+
* Refuses a transaction leaving `doc` past {@link MAX_NESTING_DEPTH}, which the commit
|
|
15
|
+
* would lose: a paste or a drop into a deep item, whose slice the import bounded alone.
|
|
16
|
+
* The nesting commands ask first and keep their key; this is what reaches the rest, and
|
|
17
|
+
* a refused paste lands nothing. Past the bound, a transaction passes where it leaves the
|
|
18
|
+
* deepest level no deeper, so a body that arrived that deep keeps its Enter.
|
|
19
|
+
*/
|
|
20
|
+
export declare function nestingPlugin(): Plugin;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { Plugin } from 'prosemirror-state';
|
|
2
|
+
import { ReplaceAroundStep, ReplaceStep } from 'prosemirror-transform';
|
|
3
|
+
/** The store's bound on the containers around one line: a commit nesting past it is
|
|
4
|
+
* refused, and `@quillmark/wasm` exports no constant to read it from. */
|
|
5
|
+
export const MAX_NESTING_DEPTH = 12;
|
|
6
|
+
/** The most containers any block in `doc` stands inside, counted as the store counts
|
|
7
|
+
* them: a `list_item` or a `blockquote` is one, the list around an item none, and no
|
|
8
|
+
* node holds an element (`decode.ts`). */
|
|
9
|
+
export function nestingDepth(doc) {
|
|
10
|
+
const walk = (node, depth) => {
|
|
11
|
+
let deepest = depth;
|
|
12
|
+
node.forEach((child) => {
|
|
13
|
+
if (child.isTextblock || child.isAtom)
|
|
14
|
+
return;
|
|
15
|
+
const name = child.type.name;
|
|
16
|
+
const d = depth + (name === 'list_item' || name === 'blockquote' ? 1 : 0);
|
|
17
|
+
deepest = Math.max(deepest, walk(child, d));
|
|
18
|
+
});
|
|
19
|
+
return deepest;
|
|
20
|
+
};
|
|
21
|
+
return walk(doc, 0);
|
|
22
|
+
}
|
|
23
|
+
/** Whether `doc` nests within {@link MAX_NESTING_DEPTH}: what a command that nests asks
|
|
24
|
+
* of the doc it would dispatch. */
|
|
25
|
+
export function withinNesting(doc) {
|
|
26
|
+
return nestingDepth(doc) <= MAX_NESTING_DEPTH;
|
|
27
|
+
}
|
|
28
|
+
/** Whether a step of `tr` can move a block under another container: any replace but
|
|
29
|
+
* inline content inside one textblock, which is every keystroke's. Mark and attribute
|
|
30
|
+
* steps move no node. */
|
|
31
|
+
function mayNest(tr) {
|
|
32
|
+
return tr.steps.some((step, i) => {
|
|
33
|
+
if (step instanceof ReplaceAroundStep)
|
|
34
|
+
return true;
|
|
35
|
+
if (!(step instanceof ReplaceStep))
|
|
36
|
+
return false;
|
|
37
|
+
const doc = tr.docs[i];
|
|
38
|
+
const $from = doc.resolve(step.from);
|
|
39
|
+
if (!$from.parent.isTextblock || !$from.sameParent(doc.resolve(step.to)))
|
|
40
|
+
return true;
|
|
41
|
+
let block = false;
|
|
42
|
+
step.slice.content.forEach((node) => (block ||= node.isBlock));
|
|
43
|
+
return block;
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Refuses a transaction leaving `doc` past {@link MAX_NESTING_DEPTH}, which the commit
|
|
48
|
+
* would lose: a paste or a drop into a deep item, whose slice the import bounded alone.
|
|
49
|
+
* The nesting commands ask first and keep their key; this is what reaches the rest, and
|
|
50
|
+
* a refused paste lands nothing. Past the bound, a transaction passes where it leaves the
|
|
51
|
+
* deepest level no deeper, so a body that arrived that deep keeps its Enter.
|
|
52
|
+
*/
|
|
53
|
+
export function nestingPlugin() {
|
|
54
|
+
return new Plugin({
|
|
55
|
+
filterTransaction: (tr) => {
|
|
56
|
+
if (!tr.docChanged || !mayNest(tr))
|
|
57
|
+
return true;
|
|
58
|
+
const after = nestingDepth(tr.doc);
|
|
59
|
+
return after <= MAX_NESTING_DEPTH || after <= nestingDepth(tr.before);
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
}
|
|
@@ -148,6 +148,20 @@ li.ProseMirror-selectednode::after {
|
|
|
148
148
|
font-size: inherit;
|
|
149
149
|
}
|
|
150
150
|
|
|
151
|
+
/* A fence's language at the label rung in its top corner, read off the `data-lang` the
|
|
152
|
+
node writes (`schema.ts`). Generated content, so it is never text the caret reaches
|
|
153
|
+
or a copy takes; the code's lines wrap around it. Read-only: the language is changed
|
|
154
|
+
in source mode. */
|
|
155
|
+
.ProseMirror pre[data-lang]::before {
|
|
156
|
+
content: attr(data-lang);
|
|
157
|
+
float: inline-end;
|
|
158
|
+
margin-inline-start: var(--_qm-space-3);
|
|
159
|
+
font-size: var(--_qm-text-label);
|
|
160
|
+
color: var(--_qm-ink-label);
|
|
161
|
+
user-select: none;
|
|
162
|
+
pointer-events: none;
|
|
163
|
+
}
|
|
164
|
+
|
|
151
165
|
/* A link takes the surface's ink (the UA's blue is the `#8cf` defect again) and a
|
|
152
166
|
dotted underline. Dotted rather than solid because `underline` is a mark in the
|
|
153
167
|
same schema, and a solid rule renders an authored `<u>` and a link identically in a
|
|
@@ -191,6 +205,59 @@ li.ProseMirror-selectednode::after {
|
|
|
191
205
|
list-style: revert;
|
|
192
206
|
}
|
|
193
207
|
|
|
208
|
+
/* A task item's checkbox (`tasks.ts`) stands after the item's marker and ahead of its
|
|
209
|
+
first line, the text wrapping back under it, as the paper sets the box. Its face is a
|
|
210
|
+
matrix tick's: the input drawn `appearance: none` as an edged well, held at the
|
|
211
|
+
accent, the check glyph over it in the surface ink. A disabled box is a held leaf's
|
|
212
|
+
or a copy's, and takes no press. */
|
|
213
|
+
.ProseMirror .qm-task {
|
|
214
|
+
position: relative;
|
|
215
|
+
float: left;
|
|
216
|
+
display: flex;
|
|
217
|
+
align-items: center;
|
|
218
|
+
height: 1lh;
|
|
219
|
+
margin-right: var(--_qm-space-2);
|
|
220
|
+
}
|
|
221
|
+
.ProseMirror .qm-task-box {
|
|
222
|
+
appearance: none;
|
|
223
|
+
margin: 0;
|
|
224
|
+
width: var(--_qm-glyph-control);
|
|
225
|
+
height: var(--_qm-glyph-control);
|
|
226
|
+
border: var(--_qm-border-width) solid var(--_qm-border);
|
|
227
|
+
border-radius: calc(var(--_qm-radius-inner) / 2);
|
|
228
|
+
background: var(--_qm-surface-well);
|
|
229
|
+
cursor: pointer;
|
|
230
|
+
transition:
|
|
231
|
+
background-color var(--_qm-duration-fast) var(--_qm-ease-reverse),
|
|
232
|
+
border-color var(--_qm-duration-fast) var(--_qm-ease-reverse);
|
|
233
|
+
}
|
|
234
|
+
.ProseMirror .qm-task-box:checked {
|
|
235
|
+
background: var(--_qm-accent);
|
|
236
|
+
border-color: var(--_qm-accent);
|
|
237
|
+
}
|
|
238
|
+
.ProseMirror .qm-task-box:disabled {
|
|
239
|
+
cursor: default;
|
|
240
|
+
}
|
|
241
|
+
.ProseMirror .qm-task-box:focus-visible {
|
|
242
|
+
outline: var(--_qm-ring);
|
|
243
|
+
outline-offset: var(--_qm-ring-offset);
|
|
244
|
+
}
|
|
245
|
+
.ProseMirror .qm-task-mark {
|
|
246
|
+
position: absolute;
|
|
247
|
+
top: 50%;
|
|
248
|
+
left: 0;
|
|
249
|
+
width: var(--_qm-glyph-control);
|
|
250
|
+
height: var(--_qm-glyph-control);
|
|
251
|
+
translate: 0 -50%;
|
|
252
|
+
pointer-events: none;
|
|
253
|
+
color: var(--_qm-surface);
|
|
254
|
+
opacity: 0;
|
|
255
|
+
transition: opacity var(--_qm-duration-fast) var(--_qm-ease-reverse);
|
|
256
|
+
}
|
|
257
|
+
.ProseMirror .qm-task:has(:checked) .qm-task-mark {
|
|
258
|
+
opacity: 1;
|
|
259
|
+
}
|
|
260
|
+
|
|
194
261
|
/* Six levels, one weight, no size step. The quill reads no level at all, so a size
|
|
195
262
|
ramp here would be the editor predicting a hierarchy the paper does not set; the
|
|
196
263
|
level stays in the model, which is where the codec round-trips it. It is also all
|
|
@@ -516,37 +583,24 @@ li.ProseMirror-selectednode::after {
|
|
|
516
583
|
height: var(--_qm-tap-min);
|
|
517
584
|
}
|
|
518
585
|
/* The reveal is the control's own (CODEC §"The table island"): one comes up while the
|
|
519
|
-
pointer is on that control, while it
|
|
520
|
-
about — the selected line's grip, and the lifted one, which is drawn through a drag the
|
|
586
|
+
pointer is on that control, or while it is what a gesture is about — the selected line's grip, and the lifted one, which is drawn through a drag the
|
|
521
587
|
pointer has travelled off. A cell under the pointer draws its row's and its column's
|
|
522
588
|
grips (`table-view.ts` §`onHover`), one pill on each axis and none anywhere else.
|
|
523
589
|
|
|
524
590
|
Opacity rather than absence, so every control keeps its box and is hit-tested wherever
|
|
525
|
-
it sits. A grip's reach
|
|
526
|
-
focus rung: what a keyboard draws a line with is the cell the
|
|
527
|
-
(`table-view.ts` §`cellKeys`), and the wash and the selected
|
|
528
|
-
|
|
529
|
-
on them, so the rung there draws one a keyboard has reached. */
|
|
591
|
+
it sits. A grip's reach and a bar's are the pointer's alone and neither holds focus, so
|
|
592
|
+
neither carries a focus rung: what a keyboard draws a line with is the cell the
|
|
593
|
+
selection runs through (`table-view.ts` §`cellKeys`), and the wash and the selected
|
|
594
|
+
grip's tint are what say so. */
|
|
530
595
|
:is(.qm-table-grip, .qm-table-add) {
|
|
531
596
|
opacity: 0;
|
|
532
597
|
transition: opacity var(--_qm-duration-fast) var(--_qm-ease-reverse);
|
|
533
598
|
}
|
|
534
|
-
.qm-table-add:
|
|
599
|
+
.qm-table-add:hover,
|
|
535
600
|
.qm-table-grip:is([data-near], :hover, [aria-pressed='true'], .qm-table-lifted) {
|
|
536
601
|
opacity: 1;
|
|
537
602
|
}
|
|
538
603
|
|
|
539
|
-
/* The ring, on the two add bars and nowhere else in this file. A prose leaf's focus
|
|
540
|
-
indicator is its caret and a cell's is too, so the outline is dropped there; a control
|
|
541
|
-
that comes up out of nothing has the reveal, and a ring under a keyboard is what says
|
|
542
|
-
the focus is there rather than on the paper behind it. Declared here rather than
|
|
543
|
-
reached for as `.qm-focus-ring`: that recipe is the visual tier's stylesheet, and the
|
|
544
|
-
island is inside the leaf, which is the codec's. */
|
|
545
|
-
.qm-table-add:focus-visible {
|
|
546
|
-
outline: var(--_qm-ring);
|
|
547
|
-
outline-offset: var(--_qm-ring-offset);
|
|
548
|
-
}
|
|
549
|
-
|
|
550
604
|
/* A weighted table spans the leaf, laid out fixed, so the stored weights are the
|
|
551
605
|
proportions its columns draw, as the page's fractional columns span the text block
|
|
552
606
|
(`table-view.ts` §`weigh`). An auto-fit table keeps its content's width. */
|
|
@@ -610,7 +664,7 @@ li.ProseMirror-selectednode::after {
|
|
|
610
664
|
display: none;
|
|
611
665
|
}
|
|
612
666
|
/* The header toggle hangs under the first row's last cell, flush with the grid's
|
|
613
|
-
trailing edge
|
|
667
|
+
trailing edge. */
|
|
614
668
|
.qm-table-align[data-header] {
|
|
615
669
|
margin-inline: auto 0;
|
|
616
670
|
}
|
|
@@ -23,7 +23,7 @@ export declare const plaintextSchema: Schema<string, never>;
|
|
|
23
23
|
*/
|
|
24
24
|
export declare const cellSchema: Schema<"hard_break", string>;
|
|
25
25
|
/**
|
|
26
|
-
* A `
|
|
26
|
+
* A `text` field declaring `multiline: true`: paragraphs and hard breaks, no marks, no
|
|
27
27
|
* islands, no containers, which is the shape `Content::is_plain` admits. Mark-free
|
|
28
28
|
* for the reason {@link plaintextSchema} is.
|
|
29
29
|
*
|
|
@@ -38,7 +38,7 @@ export declare function leafSchema(opts: {
|
|
|
38
38
|
plaintext?: boolean;
|
|
39
39
|
inline?: boolean;
|
|
40
40
|
}): Schema;
|
|
41
|
-
/** Whether `schema` can carry formatting at all: false for the two
|
|
41
|
+
/** Whether `schema` can carry formatting at all: false for the two mark-free schemas.
|
|
42
42
|
* What a mark command asks before it offers itself. */
|
|
43
43
|
export declare function hasMarks(schema: Schema): boolean;
|
|
44
44
|
/** True for the one-line schemas, which declare no `hard_break`; decode branches on it. */
|
|
@@ -3,9 +3,9 @@
|
|
|
3
3
|
// (list_item/quote → lists/blockquote); marks mirror the content formatting set.
|
|
4
4
|
// Each set is closed upstream, so the schema names its whole vocabulary and carries
|
|
5
5
|
// nothing inert. `blockSchema` is the full field; `inlineSchema` is the constrained
|
|
6
|
-
// single-textblock form for `richtext
|
|
7
|
-
// containers, no islands) and `plaintextSchema` is that one without marks, for
|
|
8
|
-
// `
|
|
6
|
+
// single-textblock form for a one-line `richtext` (one paragraph, no block split, no
|
|
7
|
+
// containers, no islands) and `plaintextSchema` is that one without marks, for a
|
|
8
|
+
// one-line `text`; `plainSchema` is a `text` field declaring `multiline: true`,
|
|
9
9
|
// paragraphs and hard breaks and nothing else; `cellSchema` is the inline form with
|
|
10
10
|
// hard breaks, for a table cell. Same decode/lower/position machinery,
|
|
11
11
|
// narrower shapes. Anchors are not marks here (decorations).
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
// marks, a `<table>` being the block leaf's clipboard parser's (`table-paste.ts`).
|
|
19
19
|
import { Schema } from 'prosemirror-model';
|
|
20
20
|
import { islandBlockSpec, islandInlineSpec } from './islands.js';
|
|
21
|
+
import { taskBody, taskChecked, taskDOM } from './tasks.js';
|
|
21
22
|
import { rendersHref, storableUrl } from './urls.js';
|
|
22
23
|
// ── Marks spelled as style ──────────────────────────────────────────────────
|
|
23
24
|
// A word processor writes formatting as a span's inline style rather than as a tag:
|
|
@@ -193,11 +194,19 @@ const blockNodes = {
|
|
|
193
194
|
parseDOM: [{ tag: 'ul' }],
|
|
194
195
|
toDOM: () => ['ul', 0]
|
|
195
196
|
},
|
|
197
|
+
// `checked` is a task item's, `null` on every other item (`tasks.ts`).
|
|
196
198
|
list_item: {
|
|
197
199
|
content: 'block+',
|
|
198
200
|
defining: true,
|
|
199
|
-
|
|
200
|
-
|
|
201
|
+
attrs: { checked: { default: null } },
|
|
202
|
+
parseDOM: [
|
|
203
|
+
{
|
|
204
|
+
tag: 'li',
|
|
205
|
+
getAttrs: (el) => ({ checked: taskChecked(el) }),
|
|
206
|
+
contentElement: taskBody
|
|
207
|
+
}
|
|
208
|
+
],
|
|
209
|
+
toDOM: (node) => node.attrs.checked === null ? ['li', 0] : taskDOM(node.attrs.checked)
|
|
201
210
|
},
|
|
202
211
|
island_block: islandBlockSpec
|
|
203
212
|
};
|
|
@@ -254,7 +263,7 @@ export const cellSchema = new Schema({
|
|
|
254
263
|
marks
|
|
255
264
|
});
|
|
256
265
|
/**
|
|
257
|
-
* A `
|
|
266
|
+
* A `text` field declaring `multiline: true`: paragraphs and hard breaks, no marks, no
|
|
258
267
|
* islands, no containers, which is the shape `Content::is_plain` admits. Mark-free
|
|
259
268
|
* for the reason {@link plaintextSchema} is.
|
|
260
269
|
*
|
|
@@ -278,7 +287,7 @@ export function leafSchema(opts) {
|
|
|
278
287
|
return opts.inline ? plaintextSchema : plainSchema;
|
|
279
288
|
return opts.inline ? inlineSchema : blockSchema;
|
|
280
289
|
}
|
|
281
|
-
/** Whether `schema` can carry formatting at all: false for the two
|
|
290
|
+
/** Whether `schema` can carry formatting at all: false for the two mark-free schemas.
|
|
282
291
|
* What a mark command asks before it offers itself. */
|
|
283
292
|
export function hasMarks(schema) {
|
|
284
293
|
return Object.keys(schema.marks).length > 0;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { Plugin } from 'prosemirror-state';
|
|
2
|
+
import type { TableProps } from '@quillmark/wasm';
|
|
3
|
+
/** A block of cells, both corners inclusive, in the row space `table.ts` names. */
|
|
4
|
+
export interface CellRange {
|
|
5
|
+
r0: number;
|
|
6
|
+
c0: number;
|
|
7
|
+
r1: number;
|
|
8
|
+
c1: number;
|
|
9
|
+
}
|
|
10
|
+
/** The cells of `range` as a `<table>`, the header row in a `<thead>` where the range
|
|
11
|
+
* holds it and the table has one. The cells carry their marks and breaks, and nothing
|
|
12
|
+
* of the table's layout. */
|
|
13
|
+
export declare function tableElement(props: TableProps, range?: CellRange): HTMLTableElement;
|
|
14
|
+
/** The cells of `range` as tab-separated text, a line a row. */
|
|
15
|
+
export declare function tableText(props: TableProps, range?: CellRange): string;
|
|
16
|
+
/** A block leaf's copy: a table in the slice writes as a table in both spellings. */
|
|
17
|
+
export declare function tableCopyPlugin(): Plugin;
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
// A table leaves the editor as the two spellings a table arrives in (`table-paste.ts`):
|
|
2
|
+
// an HTML `<table>` and tab-separated text, what Word, Docs, Excel and Sheets each read.
|
|
3
|
+
// Two copies write them: a held rectangle's cells (`table-view.ts`), and a selection
|
|
4
|
+
// holding a whole island, whose markup keeps the island's own element around the table
|
|
5
|
+
// so a paste inside the editor reads the island back whole.
|
|
6
|
+
import { DOMSerializer } from 'prosemirror-model';
|
|
7
|
+
import { Plugin } from 'prosemirror-state';
|
|
8
|
+
import { decode } from './decode.js';
|
|
9
|
+
import { tablePropsOfNode } from './islands.js';
|
|
10
|
+
import { blockSchema, cellSchema } from './schema.js';
|
|
11
|
+
import { cellAt, cellContent, columnCount, rowCount } from './table.js';
|
|
12
|
+
const whole = (props) => ({
|
|
13
|
+
r0: 0,
|
|
14
|
+
c0: 0,
|
|
15
|
+
r1: rowCount(props) - 1,
|
|
16
|
+
c1: columnCount(props) - 1
|
|
17
|
+
});
|
|
18
|
+
const cellSerializer = DOMSerializer.fromSchema(cellSchema);
|
|
19
|
+
function cellDOM(tag, cell) {
|
|
20
|
+
const dom = document.createElement(tag);
|
|
21
|
+
const paragraph = decode(cellContent(cell), cellSchema).firstChild;
|
|
22
|
+
if (paragraph)
|
|
23
|
+
dom.append(cellSerializer.serializeFragment(paragraph.content));
|
|
24
|
+
return dom;
|
|
25
|
+
}
|
|
26
|
+
/** The cells of `range` as a `<table>`, the header row in a `<thead>` where the range
|
|
27
|
+
* holds it and the table has one. The cells carry their marks and breaks, and nothing
|
|
28
|
+
* of the table's layout. */
|
|
29
|
+
export function tableElement(props, range = whole(props)) {
|
|
30
|
+
const table = document.createElement('table');
|
|
31
|
+
let body;
|
|
32
|
+
for (let r = range.r0; r <= range.r1; r++) {
|
|
33
|
+
const head = r === 0 && !props.headless;
|
|
34
|
+
const section = head ? table.createTHead() : (body ??= table.createTBody());
|
|
35
|
+
const tr = section.insertRow();
|
|
36
|
+
for (let c = range.c0; c <= range.c1; c++)
|
|
37
|
+
tr.append(cellDOM(head ? 'th' : 'td', cellAt(props, r, c)));
|
|
38
|
+
}
|
|
39
|
+
return table;
|
|
40
|
+
}
|
|
41
|
+
/** A cell holding a tab, a line end or a quote goes in quotes with its quotes doubled,
|
|
42
|
+
* as Excel and Sheets write one. */
|
|
43
|
+
function tsvCell(text) {
|
|
44
|
+
return /[\t\n"]/.test(text) ? `"${text.replace(/"/g, '""')}"` : text;
|
|
45
|
+
}
|
|
46
|
+
/** The cells of `range` as tab-separated text, a line a row. */
|
|
47
|
+
export function tableText(props, range = whole(props)) {
|
|
48
|
+
const lines = [];
|
|
49
|
+
for (let r = range.r0; r <= range.r1; r++) {
|
|
50
|
+
const cells = [];
|
|
51
|
+
for (let c = range.c0; c <= range.c1; c++)
|
|
52
|
+
cells.push(tsvCell(cellAt(props, r, c).text));
|
|
53
|
+
lines.push(cells.join('\t'));
|
|
54
|
+
}
|
|
55
|
+
return lines.join('\n');
|
|
56
|
+
}
|
|
57
|
+
const base = DOMSerializer.fromSchema(blockSchema);
|
|
58
|
+
/** The island's own element with the table inside it: a paste in the editor matches the
|
|
59
|
+
* element and reads `props`, and every other reader finds the table. */
|
|
60
|
+
const islandSerializer = new DOMSerializer({
|
|
61
|
+
...base.nodes,
|
|
62
|
+
island_block: (node) => {
|
|
63
|
+
const props = tablePropsOfNode(node);
|
|
64
|
+
const dom = DOMSerializer.renderSpec(document, blockSchema.nodes.island_block.spec.toDOM(node)).dom;
|
|
65
|
+
if (props)
|
|
66
|
+
dom.replaceChildren(tableElement(props));
|
|
67
|
+
return dom;
|
|
68
|
+
}
|
|
69
|
+
}, base.marks);
|
|
70
|
+
function leafText(node) {
|
|
71
|
+
const props = node.type === blockSchema.nodes.island_block ? tablePropsOfNode(node) : undefined;
|
|
72
|
+
if (props)
|
|
73
|
+
return tableText(props);
|
|
74
|
+
return node.type.spec.leafText?.(node) ?? '';
|
|
75
|
+
}
|
|
76
|
+
/** A block leaf's copy: a table in the slice writes as a table in both spellings. */
|
|
77
|
+
export function tableCopyPlugin() {
|
|
78
|
+
return new Plugin({
|
|
79
|
+
props: {
|
|
80
|
+
clipboardSerializer: islandSerializer,
|
|
81
|
+
clipboardTextSerializer: (slice) => slice.content.textBetween(0, slice.content.size, '\n\n', leafText)
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
}
|
|
@@ -21,18 +21,12 @@ export interface TableChromeStrings {
|
|
|
21
21
|
/** The two trailing bars, each of which grows the table along its own axis. */
|
|
22
22
|
tableAddRow: string;
|
|
23
23
|
tableAddColumn: string;
|
|
24
|
-
/** The held column's alignment cluster, a name per toggle
|
|
25
|
-
* cluster, which takes these three and the vertical three beside them. */
|
|
24
|
+
/** The held column's alignment cluster, a name per toggle. */
|
|
26
25
|
tableAlignLeft: string;
|
|
27
26
|
tableAlignCenter: string;
|
|
28
27
|
tableAlignRight: string;
|
|
29
28
|
/** The held first row's toggle, pressed while the row draws as the header. */
|
|
30
29
|
tableHeaderToggle: string;
|
|
31
|
-
tableAlignTop: string;
|
|
32
|
-
tableAlignMiddle: string;
|
|
33
|
-
tableAlignBottom: string;
|
|
34
|
-
/** The held cells' cluster's own name. */
|
|
35
|
-
tableCellAlignment: string;
|
|
36
30
|
}
|
|
37
31
|
/**
|
|
38
32
|
* The package's English for the island chrome. It lives here, beside the surface
|