@quillmark/svelte 0.0.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/LICENSE +201 -0
- package/NOTICE +58 -0
- package/README.md +241 -0
- package/THEMING.md +153 -0
- package/dist/core/address.d.ts +56 -0
- package/dist/core/address.js +95 -0
- package/dist/core/bloom.d.ts +37 -0
- package/dist/core/bloom.js +116 -0
- package/dist/core/codec/anchor.d.ts +19 -0
- package/dist/core/codec/anchor.js +36 -0
- package/dist/core/codec/code.d.ts +12 -0
- package/dist/core/codec/code.js +118 -0
- package/dist/core/codec/decode.d.ts +24 -0
- package/dist/core/codec/decode.js +270 -0
- package/dist/core/codec/encode.d.ts +71 -0
- package/dist/core/codec/encode.js +480 -0
- package/dist/core/codec/field.d.ts +171 -0
- package/dist/core/codec/field.js +493 -0
- package/dist/core/codec/index.d.ts +22 -0
- package/dist/core/codec/index.js +43 -0
- package/dist/core/codec/inputrules.d.ts +7 -0
- package/dist/core/codec/inputrules.js +168 -0
- package/dist/core/codec/islands.d.ts +36 -0
- package/dist/core/codec/islands.js +82 -0
- package/dist/core/codec/keymap.d.ts +13 -0
- package/dist/core/codec/keymap.js +29 -0
- package/dist/core/codec/lists.d.ts +22 -0
- package/dist/core/codec/lists.js +104 -0
- package/dist/core/codec/marks.d.ts +37 -0
- package/dist/core/codec/marks.js +74 -0
- package/dist/core/codec/positions.d.ts +17 -0
- package/dist/core/codec/positions.js +108 -0
- package/dist/core/codec/prose.css +458 -0
- package/dist/core/codec/reconcile.d.ts +26 -0
- package/dist/core/codec/reconcile.js +64 -0
- package/dist/core/codec/schema.d.ts +13 -0
- package/dist/core/codec/schema.js +155 -0
- package/dist/core/codec/slash.d.ts +79 -0
- package/dist/core/codec/slash.js +0 -0
- package/dist/core/codec/table-view.d.ts +50 -0
- package/dist/core/codec/table-view.js +928 -0
- package/dist/core/codec/table.d.ts +85 -0
- package/dist/core/codec/table.js +224 -0
- package/dist/core/errors.d.ts +73 -0
- package/dist/core/errors.js +24 -0
- package/dist/core/index.d.ts +5 -0
- package/dist/core/index.js +19 -0
- package/dist/core/lifecycle.d.ts +7 -0
- package/dist/core/lifecycle.js +21 -0
- package/dist/core/rebind.svelte.d.ts +18 -0
- package/dist/core/rebind.svelte.js +44 -0
- package/dist/core/teardown.d.ts +20 -0
- package/dist/core/teardown.js +37 -0
- package/dist/core/theme.css +276 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +9 -0
- package/dist/preset/index.css +10 -0
- package/dist/preset/recipes.css +188 -0
- package/dist/preset/scale.css +97 -0
- package/dist/preview/Preview.svelte +118 -0
- package/dist/preview/Preview.svelte.d.ts +39 -0
- package/dist/preview/bridge.d.ts +13 -0
- package/dist/preview/bridge.js +88 -0
- package/dist/preview/controller.d.ts +40 -0
- package/dist/preview/controller.js +141 -0
- package/dist/preview/geometry.d.ts +37 -0
- package/dist/preview/geometry.js +45 -0
- package/dist/preview/index.d.ts +5 -0
- package/dist/preview/index.js +14 -0
- package/dist/preview/overlay.d.ts +10 -0
- package/dist/preview/overlay.js +82 -0
- package/dist/preview/paint.d.ts +17 -0
- package/dist/preview/paint.js +262 -0
- package/dist/preview/strings.d.ts +14 -0
- package/dist/preview/strings.js +22 -0
- package/dist/visual/ArrayField.svelte +362 -0
- package/dist/visual/ArrayField.svelte.d.ts +24 -0
- package/dist/visual/BooleanField.svelte +88 -0
- package/dist/visual/BooleanField.svelte.d.ts +20 -0
- package/dist/visual/Card.svelte +849 -0
- package/dist/visual/Card.svelte.d.ts +43 -0
- package/dist/visual/CardControls.svelte +92 -0
- package/dist/visual/CardControls.svelte.d.ts +11 -0
- package/dist/visual/DateField.svelte +203 -0
- package/dist/visual/DateField.svelte.d.ts +28 -0
- package/dist/visual/DiagnosticList.svelte +50 -0
- package/dist/visual/DiagnosticList.svelte.d.ts +7 -0
- package/dist/visual/EnumField.svelte +169 -0
- package/dist/visual/EnumField.svelte.d.ts +25 -0
- package/dist/visual/Field.svelte +270 -0
- package/dist/visual/Field.svelte.d.ts +37 -0
- package/dist/visual/FieldHint.svelte +185 -0
- package/dist/visual/FieldHint.svelte.d.ts +13 -0
- package/dist/visual/FieldLabel.svelte +141 -0
- package/dist/visual/FieldLabel.svelte.d.ts +19 -0
- package/dist/visual/FormatPopover.svelte +396 -0
- package/dist/visual/FormatPopover.svelte.d.ts +9 -0
- package/dist/visual/NumberField.svelte +72 -0
- package/dist/visual/NumberField.svelte.d.ts +19 -0
- package/dist/visual/ObjectField.svelte +131 -0
- package/dist/visual/ObjectField.svelte.d.ts +17 -0
- package/dist/visual/ProseArrayElement.svelte +98 -0
- package/dist/visual/ProseArrayElement.svelte.d.ts +19 -0
- package/dist/visual/ProseField.svelte +213 -0
- package/dist/visual/ProseField.svelte.d.ts +50 -0
- package/dist/visual/SlashMenu.svelte +126 -0
- package/dist/visual/SlashMenu.svelte.d.ts +14 -0
- package/dist/visual/TextField.svelte +63 -0
- package/dist/visual/TextField.svelte.d.ts +23 -0
- package/dist/visual/TipsCard.svelte +191 -0
- package/dist/visual/TipsCard.svelte.d.ts +13 -0
- package/dist/visual/VisualEditor.svelte +96 -0
- package/dist/visual/VisualEditor.svelte.d.ts +14 -0
- package/dist/visual/VisualEditorInner.svelte +1032 -0
- package/dist/visual/VisualEditorInner.svelte.d.ts +17 -0
- package/dist/visual/controls.css +272 -0
- package/dist/visual/diagnostics.d.ts +52 -0
- package/dist/visual/diagnostics.js +83 -0
- package/dist/visual/domid.d.ts +21 -0
- package/dist/visual/domid.js +36 -0
- package/dist/visual/ext.d.ts +19 -0
- package/dist/visual/ext.js +29 -0
- package/dist/visual/icons/Icon.svelte +39 -0
- package/dist/visual/icons/Icon.svelte.d.ts +12 -0
- package/dist/visual/icons/nodes.d.ts +103 -0
- package/dist/visual/icons/nodes.js +55 -0
- package/dist/visual/index.d.ts +9 -0
- package/dist/visual/index.js +24 -0
- package/dist/visual/motion.d.ts +18 -0
- package/dist/visual/motion.js +53 -0
- package/dist/visual/props.d.ts +77 -0
- package/dist/visual/props.js +1 -0
- package/dist/visual/signals.d.ts +59 -0
- package/dist/visual/signals.js +1 -0
- package/dist/visual/strings.d.ts +123 -0
- package/dist/visual/strings.js +80 -0
- package/dist/visual/structure.d.ts +224 -0
- package/dist/visual/structure.js +270 -0
- package/dist/visual/synced.svelte.d.ts +15 -0
- package/dist/visual/synced.svelte.js +34 -0
- package/dist/visual/tips.d.ts +24 -0
- package/dist/visual/tips.js +50 -0
- package/package.json +102 -0
|
@@ -0,0 +1,493 @@
|
|
|
1
|
+
// The prose leaf: `createField` (VISUAL_EDITOR §Surface). One content field, one
|
|
2
|
+
// PM `EditorState`/`EditorView`, wired to the WASM edit surface. It reads the
|
|
3
|
+
// leaf's `Content`, decodes to a PM state, mounts a view + plugin stack (history,
|
|
4
|
+
// keymap, input rules, the anchor-position plugin), and `dispatchTransaction`:
|
|
5
|
+
// (a) apply optimistically to the view,
|
|
6
|
+
// (b) lower the tr to a `ChangeBundle` (or `overwrite` for a field not yet at
|
|
7
|
+
// content rest, which has nothing to splice),
|
|
8
|
+
// (c) commit via `doc.applyChange(addr, bundle)`,
|
|
9
|
+
// (d) fire `onChange` when the transaction COMMITTED, then `onCaretMove` with
|
|
10
|
+
// the new USV caret, which a bare selection move fires on its own.
|
|
11
|
+
// On an `applyChange` throw the optimistic PM state stays and the failure reports
|
|
12
|
+
// through `onError`: never a crash. Caret continuity across own-edits is the PM
|
|
13
|
+
// `StepMap`; an EXTERNAL content change re-hydrates through `applyExternal`, gated
|
|
14
|
+
// by `reconcile`.
|
|
15
|
+
import { baseKeymap, toggleMark } from 'prosemirror-commands';
|
|
16
|
+
import { gapCursor } from 'prosemirror-gapcursor';
|
|
17
|
+
import { history, redo, undo } from 'prosemirror-history';
|
|
18
|
+
import { keymap } from 'prosemirror-keymap';
|
|
19
|
+
import { EditorState, NodeSelection, Plugin, PluginKey, Selection, TextSelection } from 'prosemirror-state';
|
|
20
|
+
import { Decoration, DecorationSet, EditorView } from 'prosemirror-view';
|
|
21
|
+
import { reportError, errorMessage } from '../errors.js';
|
|
22
|
+
import { decode } from './decode.js';
|
|
23
|
+
import { usvToPM, pmToUsv, buildLineIndex } from './positions.js';
|
|
24
|
+
import { lower, pmToContent, contentEdit } from './encode.js';
|
|
25
|
+
import { anchorsFromContent } from './marks.js';
|
|
26
|
+
import { createReconciler } from './reconcile.js';
|
|
27
|
+
import { inputRulesPlugin } from './inputrules.js';
|
|
28
|
+
import { bodyKeymap } from './keymap.js';
|
|
29
|
+
import { blockSchema, inlineSchema } from './schema.js';
|
|
30
|
+
import { DEFAULT_TABLE_STRINGS, tableNodeView } from './table-view.js';
|
|
31
|
+
import { DEFAULT_SLASH_STRINGS, focusSlashItem, runSlashItem, slashItems, slashPlugin } from './slash.js';
|
|
32
|
+
const anchorKey = new PluginKey('quill-anchors');
|
|
33
|
+
/** Plugin-held anchor positions (PM coords), mapped through every edit's `StepMap`
|
|
34
|
+
* and mutated by an {@link AnchorEdit} meta (an insert/remove at a selection). */
|
|
35
|
+
function anchorPlugin(seed) {
|
|
36
|
+
return new Plugin({
|
|
37
|
+
key: anchorKey,
|
|
38
|
+
state: {
|
|
39
|
+
init: () => seed,
|
|
40
|
+
apply: (tr, anchors) => {
|
|
41
|
+
let next = tr.docChanged
|
|
42
|
+
? anchors.map((a) => ({ id: a.id, pos: tr.mapping.map(a.pos) }))
|
|
43
|
+
: anchors;
|
|
44
|
+
const edit = tr.getMeta(anchorKey);
|
|
45
|
+
if (edit?.op === 'add')
|
|
46
|
+
next = [...next, { id: edit.id, pos: edit.pos }];
|
|
47
|
+
else if (edit?.op === 'remove')
|
|
48
|
+
next = next.filter((a) => a.id !== edit.id);
|
|
49
|
+
return next;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
/** Read the leaf's `Content` for `addr`: `reader.getContent` (DOCUMENT_MODEL),
|
|
55
|
+
* the schema-plane read that decodes through the codec the field's declared type
|
|
56
|
+
* names. A field value, or the body `Content` when `addr.field` is absent, without
|
|
57
|
+
* materializing the whole card. Reads are total over the field axis, so an absent
|
|
58
|
+
* field, a default-only richtext field (e.g. `tag_line`, no stored value until
|
|
59
|
+
* first edited), reads `undefined`; decode an empty content rather than crash, and
|
|
60
|
+
* the first edit overwrites it. Only an out-of-range `addr.card` throws, unreachable
|
|
61
|
+
* here: a removed card unmounts its keyed leaf before a stale index is read.
|
|
62
|
+
*
|
|
63
|
+
* Bound rather than verbatim, because the storage form is not the leaf's business
|
|
64
|
+
* and the verbatim read cannot answer it alone. A field the editor committed rests
|
|
65
|
+
* as `Content`; a field a markdown parse left rests as the authored STRING, and
|
|
66
|
+
* what that string MEANS is the declared type's to say: `richtext` is markdown,
|
|
67
|
+
* `plaintext` is literal text, and reading one as the other silently eats every
|
|
68
|
+
* `*` the author typed. */
|
|
69
|
+
function readLeaf(reader, addr) {
|
|
70
|
+
return reader.getContent(addr) ?? emptyContent();
|
|
71
|
+
}
|
|
72
|
+
/** The canonical empty `Content`: one empty `para` line. The zero value a prose
|
|
73
|
+
* leaf (or an array's prose element) seeds from. */
|
|
74
|
+
export function emptyContent() {
|
|
75
|
+
return { text: '', lines: [{ containers: [], kind: 'para' }], marks: [], islands: [] };
|
|
76
|
+
}
|
|
77
|
+
/** Whether ops may commit here: the stored value is a `Content` object, so
|
|
78
|
+
* `applyChange` splices exactly the content the leaf read and PM is showing.
|
|
79
|
+
*
|
|
80
|
+
* The two other rest forms both take `overwrite` instead. An UNSET field (`undefined`;
|
|
81
|
+
* a default-only richtext field before its first edit) has nothing to splice.
|
|
82
|
+
* An AUTHORED STRING is the trap: `applyChange` reads it as markdown whatever the
|
|
83
|
+
* declared type is, so on a `plaintext` field its pre-image is not the content the
|
|
84
|
+
* leaf read and a delta computed against that content lands at the wrong offsets.
|
|
85
|
+
* Installing over it costs nothing either way, since content-only marks do not
|
|
86
|
+
* survive markdown and a string therefore carries no anchors to pay. A body always
|
|
87
|
+
* rests as `Content`. */
|
|
88
|
+
function opsCommittable(doc, addr) {
|
|
89
|
+
const stored = doc.getStored(addr);
|
|
90
|
+
return stored !== null && typeof stored === 'object';
|
|
91
|
+
}
|
|
92
|
+
/** The naming attributes for the `contenteditable`. `aria-labelledby` supersedes
|
|
93
|
+
* `aria-label`: a leaf carrying both is the ambiguity where implementations
|
|
94
|
+
* disagree about which wins, and `undefined` is returned whole when a leaf takes
|
|
95
|
+
* neither, since ProseMirror reads the absence, not an empty object. */
|
|
96
|
+
function proseAttributes(opts) {
|
|
97
|
+
const attrs = {};
|
|
98
|
+
if (opts.labelledBy)
|
|
99
|
+
attrs['aria-labelledby'] = opts.labelledBy;
|
|
100
|
+
else if (opts.label)
|
|
101
|
+
attrs['aria-label'] = opts.label;
|
|
102
|
+
if (opts.describedBy)
|
|
103
|
+
attrs['aria-describedby'] = opts.describedBy;
|
|
104
|
+
return Object.keys(attrs).length ? attrs : undefined;
|
|
105
|
+
}
|
|
106
|
+
export function createField(opts) {
|
|
107
|
+
const { doc, addr, container } = opts;
|
|
108
|
+
const inline = !!opts.inline || !!opts.plaintext;
|
|
109
|
+
const plaintext = !!opts.plaintext;
|
|
110
|
+
const schema = inline ? inlineSchema : blockSchema;
|
|
111
|
+
// Bound once and held: the reader is a `{quill, doc}` pair that reads live, so
|
|
112
|
+
// every read below sees the commit before it.
|
|
113
|
+
const reader = opts.quill.reader(doc);
|
|
114
|
+
// `known` is the codec's view of the stored content: kept in sync after every
|
|
115
|
+
// own-edit so `reconcile` can tell an external change from the field's own.
|
|
116
|
+
const reconciler = createReconciler(readLeaf(reader, addr));
|
|
117
|
+
let index; // rebuilt on every structural change
|
|
118
|
+
let view;
|
|
119
|
+
// The ghost's live cell: the placeholder plugin reads it per pass, so moving it
|
|
120
|
+
// is an assignment plus a re-render rather than a rebuilt plugin stack.
|
|
121
|
+
let placeholderText = opts.placeholder;
|
|
122
|
+
// The views nested INSIDE this leaf: one per table cell (`table-view.ts`). The
|
|
123
|
+
// leaf holds the set because chrome asks the leaf, not the island, which view
|
|
124
|
+
// holds the caret: the format popover raises over a cell exactly as over the
|
|
125
|
+
// body, and only the leaf knows both.
|
|
126
|
+
const nested = new Set();
|
|
127
|
+
// The menu's offers, derived from the wording per read: the chrome names an item
|
|
128
|
+
// and the codec runs it, so a locale swap changes the labels and not the picks.
|
|
129
|
+
const menuItems = () => slashItems(opts.slashStrings?.() ?? DEFAULT_SLASH_STRINGS);
|
|
130
|
+
const slash = !inline && opts.onSlash ? { items: menuItems, onState: opts.onSlash } : undefined;
|
|
131
|
+
const state = buildState(reconciler.last);
|
|
132
|
+
index = buildLineIndex(state.doc);
|
|
133
|
+
view = new EditorView(container, {
|
|
134
|
+
state,
|
|
135
|
+
attributes: proseAttributes(opts),
|
|
136
|
+
// Islands are block-schema only, so an inline leaf mounts no node view at all.
|
|
137
|
+
nodeViews: inline
|
|
138
|
+
? undefined
|
|
139
|
+
: {
|
|
140
|
+
island_block: tableNodeView({
|
|
141
|
+
strings: opts.tableStrings ?? (() => DEFAULT_TABLE_STRINGS),
|
|
142
|
+
register: (cellView) => {
|
|
143
|
+
nested.add(cellView);
|
|
144
|
+
return () => nested.delete(cellView);
|
|
145
|
+
},
|
|
146
|
+
// A focus event does not bubble, so the leaf's own `focus` handler
|
|
147
|
+
// never fires for a cell: without this the active address would not
|
|
148
|
+
// follow a caret clicked straight into a table.
|
|
149
|
+
onCellFocus: () => opts.onFocus?.(addr)
|
|
150
|
+
})
|
|
151
|
+
},
|
|
152
|
+
dispatchTransaction: (tr) => {
|
|
153
|
+
const oldRt = reconciler.last;
|
|
154
|
+
const next = view.state.apply(tr);
|
|
155
|
+
view.updateState(next); // (a) optimistic
|
|
156
|
+
index = buildLineIndex(next.doc);
|
|
157
|
+
// Commit a content edit OR an anchor mutation. An anchor insert/remove is
|
|
158
|
+
// zero-width, so `docChanged` is false: the `anchorKey` meta is what
|
|
159
|
+
// routes it through the same commit path (the diff emits the anchor op).
|
|
160
|
+
if (tr.docChanged || tr.getMeta(anchorKey)) {
|
|
161
|
+
// (d) the change signal, only for what actually landed.
|
|
162
|
+
if (commitEdit(oldRt, next.doc))
|
|
163
|
+
opts.onChange?.(addr);
|
|
164
|
+
}
|
|
165
|
+
// The caret, for both structural and selection-only changes: a host
|
|
166
|
+
// following the caret wants an arrow key, and a host recompiling wants
|
|
167
|
+
// `onChange`. Reported after the commit, so the edit has landed by the
|
|
168
|
+
// time the caret names where it is.
|
|
169
|
+
opts.onCaretMove?.(addr, pmToUsv(index, next.selection.head));
|
|
170
|
+
},
|
|
171
|
+
handleDOMEvents: {
|
|
172
|
+
focus: () => {
|
|
173
|
+
opts.onFocus?.(addr);
|
|
174
|
+
return false;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
});
|
|
178
|
+
function buildState(rt) {
|
|
179
|
+
const pmDoc = decode(rt, schema, { plaintext });
|
|
180
|
+
const anchors = plaintext ? [] : anchorsFromContent(rt);
|
|
181
|
+
// Seed anchor plugin positions in PM coords via a fresh index over pmDoc.
|
|
182
|
+
const seedIndex = buildLineIndex(pmDoc);
|
|
183
|
+
const seededAnchors = anchors.map((a) => ({
|
|
184
|
+
id: a.id,
|
|
185
|
+
pos: usvToPM(seedIndex, a.pos)
|
|
186
|
+
}));
|
|
187
|
+
return EditorState.create({
|
|
188
|
+
doc: pmDoc,
|
|
189
|
+
plugins: proseLeafPlugins(schema, {
|
|
190
|
+
inline,
|
|
191
|
+
plaintext,
|
|
192
|
+
slash,
|
|
193
|
+
noInputRules: opts.noInputRules,
|
|
194
|
+
// Always installed, so a leaf that mounts without a ghost can still be
|
|
195
|
+
// given one later; the plugin draws nothing while the text is empty.
|
|
196
|
+
placeholder: () => placeholderText,
|
|
197
|
+
afterHistory: [anchorPlugin(seededAnchors)]
|
|
198
|
+
})
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
// (b)+(c): lower the edit to ops and commit, or `overwrite` a field not yet at
|
|
202
|
+
// content rest. Keep the optimistic PM on throw.
|
|
203
|
+
// Returns whether anything landed: the caller's change signal, which must not
|
|
204
|
+
// be a property of which branch the commit took.
|
|
205
|
+
function commitEdit(oldRt, newDoc) {
|
|
206
|
+
// The projection and its text splice, each computed ONCE per keystroke: the gate
|
|
207
|
+
// below, both overwrite fallbacks, and `lower` all read this one `edit`.
|
|
208
|
+
const edit = contentEdit(oldRt, pmToContent(newDoc));
|
|
209
|
+
try {
|
|
210
|
+
// A field not yet at content rest is the only edit that overwrites by choice:
|
|
211
|
+
// `applyChange` throws on an absent declared field (verified) and mis-reads
|
|
212
|
+
// an authored string (`opsCommittable`), and neither has anchors to lose.
|
|
213
|
+
// Every structural edit lowers, island creation included: the island channel
|
|
214
|
+
// places a slot the `delta` may not carry.
|
|
215
|
+
if (!opsCommittable(doc, addr)) {
|
|
216
|
+
doc.overwrite(addr, edit.newRt); // brings the field to content rest
|
|
217
|
+
}
|
|
218
|
+
else {
|
|
219
|
+
// Pre-edit anchors are the stored content's anchors (USV); post-edit
|
|
220
|
+
// anchors are the plugin's positions (mapped through the tr) as USV.
|
|
221
|
+
const oldAnchors = plaintext ? [] : anchorsFromContent(oldRt);
|
|
222
|
+
const newAnchors = plaintext ? [] : readAnchorsUsv(newDoc);
|
|
223
|
+
doc.applyChange(addr, lower(edit, { oldAnchors, newAnchors }));
|
|
224
|
+
}
|
|
225
|
+
reconciler.commit(readLeaf(reader, addr));
|
|
226
|
+
return true;
|
|
227
|
+
}
|
|
228
|
+
catch (e) {
|
|
229
|
+
// Bound the damage: an op path the gates missed leaves the store STALE
|
|
230
|
+
// while PM keeps the edit; and because the reconciler then re-diffs
|
|
231
|
+
// from that stale content, every later edit re-throws and the field
|
|
232
|
+
// silently stops persisting. Install the full projection instead
|
|
233
|
+
// (correct store, pays this field's anchors).
|
|
234
|
+
reportError(opts.onError, {
|
|
235
|
+
code: 'commit-fallback',
|
|
236
|
+
severity: 'error',
|
|
237
|
+
message: `applyChange refused; overwrote the whole field: ${errorMessage(e)}`,
|
|
238
|
+
cause: e
|
|
239
|
+
});
|
|
240
|
+
try {
|
|
241
|
+
doc.overwrite(addr, edit.newRt);
|
|
242
|
+
reconciler.commit(readLeaf(reader, addr));
|
|
243
|
+
return true;
|
|
244
|
+
}
|
|
245
|
+
catch (e2) {
|
|
246
|
+
// Optimistic PM stays; the store is stale for this field from here.
|
|
247
|
+
reportError(opts.onError, {
|
|
248
|
+
code: 'commit-lost',
|
|
249
|
+
severity: 'error',
|
|
250
|
+
message: `overwrite fallback failed; the stored value is stale while the editor keeps the edit: ${errorMessage(e2)}`,
|
|
251
|
+
cause: e2
|
|
252
|
+
});
|
|
253
|
+
return false;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
/** The anchors the plugin currently holds (PM coords); `[]` before the first apply. */
|
|
258
|
+
const heldAnchors = () => anchorKey.getState(view.state) ?? [];
|
|
259
|
+
/** Anchor positions in the CURRENT (new) doc, as USV. */
|
|
260
|
+
function readAnchorsUsv(newDoc) {
|
|
261
|
+
return heldAnchors().map((a) => ({ id: a.id, pos: pmToUsv(index, a.pos) }));
|
|
262
|
+
}
|
|
263
|
+
const controller = {
|
|
264
|
+
el: container,
|
|
265
|
+
setCaret(pos) {
|
|
266
|
+
const pm = usvToPM(index, pos);
|
|
267
|
+
// `Selection.near`, not `TextSelection.create`: the mapped position can
|
|
268
|
+
// be non-inline (before an island/rule block, or doc-level in an empty
|
|
269
|
+
// nested textblock), where a raw TextSelection is invalid.
|
|
270
|
+
const sel = Selection.near(view.state.doc.resolve(pm));
|
|
271
|
+
view.dispatch(view.state.tr.setSelection(sel));
|
|
272
|
+
view.focus();
|
|
273
|
+
},
|
|
274
|
+
applyExternal() {
|
|
275
|
+
const current = readLeaf(reader, addr);
|
|
276
|
+
if (!reconciler.shouldRehydrate(current))
|
|
277
|
+
return; // own edit / no change
|
|
278
|
+
const caretUsv = pmToUsv(index, view.state.selection.head);
|
|
279
|
+
const fresh = buildState(current);
|
|
280
|
+
view.updateState(fresh);
|
|
281
|
+
index = buildLineIndex(fresh.doc);
|
|
282
|
+
// Best-effort caret continuity across an external change: keep the USV.
|
|
283
|
+
const pm = usvToPM(index, caretUsv);
|
|
284
|
+
view.dispatch(view.state.tr.setSelection(Selection.near(fresh.doc.resolve(pm))));
|
|
285
|
+
reconciler.commit(current);
|
|
286
|
+
},
|
|
287
|
+
setPlaceholder(text) {
|
|
288
|
+
if (text === placeholderText)
|
|
289
|
+
return;
|
|
290
|
+
placeholderText = text;
|
|
291
|
+
// `setProps` re-runs the decoration pass against the unchanged state: no
|
|
292
|
+
// transaction, so nothing commits and no caret is reported.
|
|
293
|
+
view.setProps({});
|
|
294
|
+
},
|
|
295
|
+
focus() {
|
|
296
|
+
view.focus();
|
|
297
|
+
},
|
|
298
|
+
getContent() {
|
|
299
|
+
return readLeaf(reader, addr);
|
|
300
|
+
},
|
|
301
|
+
insertAnchor(id, pos) {
|
|
302
|
+
if (plaintext)
|
|
303
|
+
return; // a plaintext field carries no marks (§Inline mode)
|
|
304
|
+
if (heldAnchors().some((a) => a.id === id))
|
|
305
|
+
return; // ids are unique + invariant (0.97 policy)
|
|
306
|
+
// An anchor commits through `applyChange`, so the field is brought to content
|
|
307
|
+
// rest first; else the commit's overwrite branch (value semantics) would drop
|
|
308
|
+
// the just-added anchor.
|
|
309
|
+
if (!opsCommittable(doc, addr))
|
|
310
|
+
doc.overwrite(addr, reconciler.last);
|
|
311
|
+
const pm = usvToPM(index, pos);
|
|
312
|
+
view.dispatch(view.state.tr.setMeta(anchorKey, { op: 'add', id, pos: pm }));
|
|
313
|
+
},
|
|
314
|
+
removeAnchor(id) {
|
|
315
|
+
if (plaintext)
|
|
316
|
+
return;
|
|
317
|
+
if (!heldAnchors().some((a) => a.id === id))
|
|
318
|
+
return; // absent: nothing to commit
|
|
319
|
+
view.dispatch(view.state.tr.setMeta(anchorKey, { op: 'remove', id }));
|
|
320
|
+
},
|
|
321
|
+
anchorsInRange(from, to) {
|
|
322
|
+
return heldAnchors()
|
|
323
|
+
.filter((a) => {
|
|
324
|
+
const u = pmToUsv(index, a.pos);
|
|
325
|
+
return u >= from && u <= to;
|
|
326
|
+
})
|
|
327
|
+
.map((a) => a.id);
|
|
328
|
+
},
|
|
329
|
+
slashFocus(id) {
|
|
330
|
+
if (slash)
|
|
331
|
+
focusSlashItem(view, id, slash.items());
|
|
332
|
+
},
|
|
333
|
+
slashPick(id) {
|
|
334
|
+
if (slash)
|
|
335
|
+
runSlashItem(view, id);
|
|
336
|
+
},
|
|
337
|
+
selectionRange() {
|
|
338
|
+
const { from, to } = view.state.selection;
|
|
339
|
+
return {
|
|
340
|
+
from: pmToUsv(index, from),
|
|
341
|
+
to: pmToUsv(index, to)
|
|
342
|
+
};
|
|
343
|
+
},
|
|
344
|
+
destroy() {
|
|
345
|
+
view.destroy();
|
|
346
|
+
}
|
|
347
|
+
};
|
|
348
|
+
// The underlying views, exposed as available (undocumented) handles: the
|
|
349
|
+
// VisualEditor composes views, and tests drive edits through them.
|
|
350
|
+
const handles = controller;
|
|
351
|
+
handles.view = view;
|
|
352
|
+
handles.nestedViews = () => [...nested];
|
|
353
|
+
handles.focusedView = () => {
|
|
354
|
+
for (const cellView of nested)
|
|
355
|
+
if (cellView.hasFocus())
|
|
356
|
+
return cellView;
|
|
357
|
+
return view;
|
|
358
|
+
};
|
|
359
|
+
return controller;
|
|
360
|
+
}
|
|
361
|
+
/**
|
|
362
|
+
* The prose-leaf plugin stack (VISUAL_EDITOR §Surface): shared by
|
|
363
|
+
* {@link createField} and the array-element inline editor (`ProseArrayElement`),
|
|
364
|
+
* so the two never fork the keymap/plugin ordering. History first, then any
|
|
365
|
+
* leaf-specific plugins (`afterHistory`: the addressed leaf passes its
|
|
366
|
+
* anchor-position plugin; a by-value array element passes none), the
|
|
367
|
+
* markdown-shorthand input rules, then the field keymap over the base keymap, and
|
|
368
|
+
* last the two that answer to a caret or a selection BESIDE a block: the gap cursor
|
|
369
|
+
* and {@link pastAtomPlugin}.
|
|
370
|
+
*
|
|
371
|
+
* A `plaintext` field carries no marks (decode strips them, the keymap suppresses
|
|
372
|
+
* Mod-b/i/u), so it also skips the input rules: `inlineSchema` still declares the
|
|
373
|
+
* mark types, so a `**bold**` rule would apply a strong mark AND eat the literal
|
|
374
|
+
* delimiters. The inline schema has no block nodes, so those rules are the ONLY
|
|
375
|
+
* ones it would add: skip all.
|
|
376
|
+
*/
|
|
377
|
+
export function proseLeafPlugins(schema, opts) {
|
|
378
|
+
const list = [history(), ...(opts.afterHistory ?? [])];
|
|
379
|
+
if (opts.slash)
|
|
380
|
+
list.push(slashPlugin(opts.slash));
|
|
381
|
+
if (!opts.noInputRules && !opts.plaintext)
|
|
382
|
+
list.push(inputRulesPlugin(schema));
|
|
383
|
+
list.push(keymap(editorKeymap(schema, opts.inline, opts.plaintext, opts.slash?.items)));
|
|
384
|
+
list.push(keymap(baseKeymap));
|
|
385
|
+
// Both answer to a selection over an ATOM, which only a block leaf holds: an
|
|
386
|
+
// inline leaf is one textblock with no island and no gap to put a cursor in.
|
|
387
|
+
if (!opts.inline)
|
|
388
|
+
list.push(gapCursor(), pastAtomPlugin());
|
|
389
|
+
if (opts.placeholder)
|
|
390
|
+
list.push(placeholderPlugin(opts.placeholder));
|
|
391
|
+
return list;
|
|
392
|
+
}
|
|
393
|
+
/**
|
|
394
|
+
* A printable key over a selected atom writes PAST it. A selection is the subject
|
|
395
|
+
* of the next command, never a thing armed for
|
|
396
|
+
* replacement: Backspace deletes it, Mod-C copies it, and a character lands beside
|
|
397
|
+
* it. A BLOCK island takes a new paragraph after it, an INLINE one the caret after
|
|
398
|
+
* the image in the same line: one rule, both node types.
|
|
399
|
+
*
|
|
400
|
+
* PM routes text input over a non-`TextSelection` through `handleTextInput` before
|
|
401
|
+
* falling back to replacing the selection, so this one prop is the whole of it.
|
|
402
|
+
* Paste keeps replacing: a paste is deliberate, and it is the gesture that swaps one
|
|
403
|
+
* island for another.
|
|
404
|
+
*/
|
|
405
|
+
function pastAtomPlugin() {
|
|
406
|
+
return new Plugin({
|
|
407
|
+
props: {
|
|
408
|
+
handleTextInput(view, _from, _to, text) {
|
|
409
|
+
const { selection, schema } = view.state;
|
|
410
|
+
if (!text || !(selection instanceof NodeSelection) || !selection.node.isAtom)
|
|
411
|
+
return false;
|
|
412
|
+
const tr = view.state.tr;
|
|
413
|
+
const at = selection.to; // just past the node, in both coordinate senses
|
|
414
|
+
if (selection.node.isInline) {
|
|
415
|
+
tr.setSelection(TextSelection.create(tr.doc, at)).insertText(text);
|
|
416
|
+
}
|
|
417
|
+
else {
|
|
418
|
+
const para = schema.nodes.paragraph.create(null, schema.text(text));
|
|
419
|
+
tr.insert(at, para);
|
|
420
|
+
tr.setSelection(TextSelection.create(tr.doc, at + para.nodeSize - 1));
|
|
421
|
+
}
|
|
422
|
+
view.dispatch(tr.scrollIntoView());
|
|
423
|
+
return true;
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
});
|
|
427
|
+
}
|
|
428
|
+
/**
|
|
429
|
+
* The empty-leaf ghost placeholder. A node decoration stamps the sole empty
|
|
430
|
+
* textblock with a class + the ghost text,
|
|
431
|
+
* which CSS renders via `::before { content: attr(data-placeholder) }`: so the
|
|
432
|
+
* text never enters the document, the caret path, or a `pmToContent` export. It
|
|
433
|
+
* vanishes the instant the leaf holds any content (the emptiness test fails).
|
|
434
|
+
*
|
|
435
|
+
* `read` is called per decoration pass rather than closed over, so moving the
|
|
436
|
+
* ghost is a re-render and never a document edit.
|
|
437
|
+
*/
|
|
438
|
+
function placeholderPlugin(read) {
|
|
439
|
+
return new Plugin({
|
|
440
|
+
props: {
|
|
441
|
+
decorations(state) {
|
|
442
|
+
const text = read();
|
|
443
|
+
if (!text)
|
|
444
|
+
return null;
|
|
445
|
+
const { doc } = state;
|
|
446
|
+
const first = doc.firstChild;
|
|
447
|
+
const empty = doc.childCount === 1 && !!first && first.isTextblock && first.content.size === 0;
|
|
448
|
+
if (!empty)
|
|
449
|
+
return null;
|
|
450
|
+
return DecorationSet.create(doc, [
|
|
451
|
+
Decoration.node(0, first.nodeSize, {
|
|
452
|
+
class: 'qm-prose-placeholder',
|
|
453
|
+
'data-placeholder': text
|
|
454
|
+
})
|
|
455
|
+
]);
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
});
|
|
459
|
+
}
|
|
460
|
+
/**
|
|
461
|
+
* The field's keymap: history, mark toggles, and the body's structural keys; Enter
|
|
462
|
+
* suppressed inline.
|
|
463
|
+
*
|
|
464
|
+
* Tab forks on the leaf's ROLE, not on the caret's position. An inline/plaintext
|
|
465
|
+
* leaf is a form field: Tab stays unbound, so field navigation is open for a shell
|
|
466
|
+
* keymap (VISUAL_EDITOR §Settled and open). A block-schema body is a document: Tab
|
|
467
|
+
* is structural, a chain each nested surface prepends to (`keymap.ts`). One key
|
|
468
|
+
* never means two things within one surface.
|
|
469
|
+
*/
|
|
470
|
+
function editorKeymap(schema, inline, plaintext, slashItems) {
|
|
471
|
+
const map = {
|
|
472
|
+
'Mod-z': undo,
|
|
473
|
+
'Mod-y': redo,
|
|
474
|
+
'Shift-Mod-z': redo
|
|
475
|
+
};
|
|
476
|
+
if (!plaintext) {
|
|
477
|
+
if (schema.marks.strong)
|
|
478
|
+
map['Mod-b'] = toggleMark(schema.marks.strong);
|
|
479
|
+
if (schema.marks.em)
|
|
480
|
+
map['Mod-i'] = toggleMark(schema.marks.em);
|
|
481
|
+
if (schema.marks.underline)
|
|
482
|
+
map['Mod-u'] = toggleMark(schema.marks.underline);
|
|
483
|
+
}
|
|
484
|
+
if (inline) {
|
|
485
|
+
// One textblock only: swallow Enter so no second block is attempted.
|
|
486
|
+
map['Enter'] = () => true;
|
|
487
|
+
}
|
|
488
|
+
else {
|
|
489
|
+
// Each binding falls through to `baseKeymap` when no link claims the key.
|
|
490
|
+
Object.assign(map, bodyKeymap(schema, slashItems));
|
|
491
|
+
}
|
|
492
|
+
return map;
|
|
493
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import 'prosemirror-view/style/prosemirror.css';
|
|
2
|
+
import 'prosemirror-gapcursor/style/gapcursor.css';
|
|
3
|
+
import './prose.css';
|
|
4
|
+
export { rangeAnchor } from './anchor.js';
|
|
5
|
+
export type { RangeAnchor } from './anchor.js';
|
|
6
|
+
export { createField, emptyContent, proseLeafPlugins } from './field.js';
|
|
7
|
+
export type { CreateFieldOpts, FieldController, LeafViews } from './field.js';
|
|
8
|
+
export { DEFAULT_TABLE_STRINGS } from './table-view.js';
|
|
9
|
+
export type { TableChromeStrings } from './table-view.js';
|
|
10
|
+
export { DEFAULT_SLASH_STRINGS } from './slash.js';
|
|
11
|
+
export type { SlashState, SlashStrings } from './slash.js';
|
|
12
|
+
export { blockSchema, inlineSchema } from './schema.js';
|
|
13
|
+
export { decode, renderContent, usvLength } from './decode.js';
|
|
14
|
+
export { pmToContent, contentEdit, lower } from './encode.js';
|
|
15
|
+
export type { ContentEdit } from './encode.js';
|
|
16
|
+
export { usvToPM, pmToUsv, buildLineIndex } from './positions.js';
|
|
17
|
+
export type { LineIndex } from './positions.js';
|
|
18
|
+
export { createReconciler, contentEqual } from './reconcile.js';
|
|
19
|
+
export type { Reconciler } from './reconcile.js';
|
|
20
|
+
export { inputRulesPlugin } from './inputrules.js';
|
|
21
|
+
export { bodyKeymap } from './keymap.js';
|
|
22
|
+
export { listKeymap } from './lists.js';
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// The codec barrel: the bidirectional bridge between one content field (`Content`)
|
|
2
|
+
// and one ProseMirror document (CODEC). Decode (content → PM), lower (PM tr → a
|
|
3
|
+
// `ChangeBundle` for `applyChange`), the USV↔PM position map, and the `createField`
|
|
4
|
+
// prose leaf. Consumed by the VisualEditor and the codec test suite.
|
|
5
|
+
// ProseMirror's structural base styles and the gap cursor's, then the retint of the
|
|
6
|
+
// hues they mint. Imported HERE because `createField` is what mounts a view: a
|
|
7
|
+
// consumer reaching the codec directly gets them with it, and the order is what makes
|
|
8
|
+
// the retint win.
|
|
9
|
+
import 'prosemirror-view/style/prosemirror.css';
|
|
10
|
+
import 'prosemirror-gapcursor/style/gapcursor.css';
|
|
11
|
+
import './prose.css';
|
|
12
|
+
// What a floating surface anchors to. The visual tier's own surfaces mint one over a
|
|
13
|
+
// selection (`visual/FormatPopover.svelte`), so it is reached off-barrel.
|
|
14
|
+
export { rangeAnchor } from './anchor.js';
|
|
15
|
+
// The prose leaf.
|
|
16
|
+
export { createField, emptyContent, proseLeafPlugins } from './field.js';
|
|
17
|
+
// The table island's chrome vocabulary: the visual tier's `strings` set extends it,
|
|
18
|
+
// so the island's wording is overridden beside every other key.
|
|
19
|
+
export { DEFAULT_TABLE_STRINGS } from './table-view.js';
|
|
20
|
+
// The slash menu: the codec owns the model and the picks, the chrome the words and
|
|
21
|
+
// the pixels (`visual/SlashMenu.svelte` is the only off-barrel caller).
|
|
22
|
+
export { DEFAULT_SLASH_STRINGS } from './slash.js';
|
|
23
|
+
// Schemas (the decode/encode target; the VisualEditor mounts them).
|
|
24
|
+
export { blockSchema, inlineSchema } from './schema.js';
|
|
25
|
+
// Decode / encode / positions (tests + VisualEditor).
|
|
26
|
+
//
|
|
27
|
+
// THE BARREL CARRIES WHAT HAS AN OFF-BARREL CALLER, and nothing else. The mark
|
|
28
|
+
// algebra, the island bridge, the schema predicate, the `diff*` helpers,
|
|
29
|
+
// `scanDoc`, `codePoints`: every one of them is reached by RELATIVE import
|
|
30
|
+
// within `codec/`, so none of them is here. An export nothing imports is surface
|
|
31
|
+
// that still has to stay honest; a symbol earns its line when a caller outside
|
|
32
|
+
// this folder wants it.
|
|
33
|
+
export { decode, renderContent, usvLength } from './decode.js';
|
|
34
|
+
export { pmToContent, contentEdit, lower } from './encode.js';
|
|
35
|
+
export { usvToPM, pmToUsv, buildLineIndex } from './positions.js';
|
|
36
|
+
// Reconciliation gate.
|
|
37
|
+
export { createReconciler, contentEqual } from './reconcile.js';
|
|
38
|
+
// Input rules (`createField` mounts them unless `noInputRules`/`plaintext`).
|
|
39
|
+
export { inputRulesPlugin } from './inputrules.js';
|
|
40
|
+
// The body leaf's structural keys: `createField` binds the composed chains
|
|
41
|
+
// (`bodyKeymap`); the suite drives both it and the list link directly.
|
|
42
|
+
export { bodyKeymap } from './keymap.js';
|
|
43
|
+
export { listKeymap } from './lists.js';
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { InputRule } from 'prosemirror-inputrules';
|
|
2
|
+
import type { Schema } from 'prosemirror-model';
|
|
3
|
+
import type { Plugin } from 'prosemirror-state';
|
|
4
|
+
/** The full markdown-shorthand rule set for a block schema. */
|
|
5
|
+
export declare function markdownInputRules(schema: Schema): InputRule[];
|
|
6
|
+
/** The input-rules plugin the field mounts by default (opt-out via createField). */
|
|
7
|
+
export declare function inputRulesPlugin(schema: Schema): Plugin;
|