foldkit-richtext-dom 0.1.0 → 0.2.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.
Files changed (51) hide show
  1. package/README.md +448 -166
  2. package/dist/decorations-Ar6URa7n.mjs +57 -0
  3. package/dist/decorations-Ar6URa7n.mjs.map +1 -0
  4. package/dist/editor-C76tfEyV.d.mts +1016 -0
  5. package/dist/editor-C76tfEyV.d.mts.map +1 -0
  6. package/dist/editor-bundle-dnjyuk1P.d.mts +8576 -0
  7. package/dist/editor-bundle-dnjyuk1P.d.mts.map +1 -0
  8. package/dist/editor-bundle.d.mts +2 -7535
  9. package/dist/editor-bundle.mjs +352 -81
  10. package/dist/editor-bundle.mjs.map +1 -1
  11. package/dist/editor.d.mts +2 -849
  12. package/dist/editor.mjs +176 -9
  13. package/dist/editor.mjs.map +1 -1
  14. package/dist/{events-BTw9SJ7r.d.mts → events-Dasls6a_.d.mts} +31 -5
  15. package/dist/events-Dasls6a_.d.mts.map +1 -0
  16. package/dist/events.d.mts +2 -2
  17. package/dist/events.mjs +84 -14
  18. package/dist/events.mjs.map +1 -1
  19. package/dist/host-1dtxuKIA.d.mts +86 -0
  20. package/dist/host-1dtxuKIA.d.mts.map +1 -0
  21. package/dist/host.d.mts +2 -25
  22. package/dist/host.mjs +78 -6
  23. package/dist/host.mjs.map +1 -1
  24. package/dist/html.d.mts +1 -1
  25. package/dist/html.d.mts.map +1 -1
  26. package/dist/html.mjs +150 -20
  27. package/dist/html.mjs.map +1 -1
  28. package/dist/index-DDV_JesZ.d.mts +57 -0
  29. package/dist/index-DDV_JesZ.d.mts.map +1 -0
  30. package/dist/index.d.mts +2 -2
  31. package/dist/index.mjs +249 -99
  32. package/dist/index.mjs.map +1 -1
  33. package/dist/input.d.mts +936 -0
  34. package/dist/input.d.mts.map +1 -0
  35. package/dist/input.mjs +89 -0
  36. package/dist/input.mjs.map +1 -0
  37. package/dist/toolbar.d.mts +39 -0
  38. package/dist/toolbar.d.mts.map +1 -1
  39. package/dist/toolbar.mjs +325 -1
  40. package/dist/toolbar.mjs.map +1 -1
  41. package/dist/view.d.mts +14 -2
  42. package/dist/view.d.mts.map +1 -1
  43. package/dist/view.mjs +90 -19
  44. package/dist/view.mjs.map +1 -1
  45. package/package.json +26 -5
  46. package/dist/editor-bundle.d.mts.map +0 -1
  47. package/dist/editor.d.mts.map +0 -1
  48. package/dist/events-BTw9SJ7r.d.mts.map +0 -1
  49. package/dist/host.d.mts.map +0 -1
  50. package/dist/index-6QEOehXh.d.mts +0 -38
  51. package/dist/index-6QEOehXh.d.mts.map +0 -1
@@ -1,8 +1,10 @@
1
- import { placeRendering } from "./host.mjs";
2
- import { Message, events, patchEditor } from "./editor.mjs";
1
+ import { decorationsFor, inputRulesFor, placeDecorations, placeInputRules, placeOverlay, placePlaceholder, placeRendering, placeServerRendered, placeVocabulary, renderingFor, serverRenderedFor, vocabularyFor } from "./host.mjs";
2
+ import { Message, events, heldDocument, patchEditor, redecorateEditor, slashEntries, slashMenu } from "./editor.mjs";
3
+ import { renderEditable } from "./view.mjs";
3
4
  import * as RichText from "foldkit-richtext";
4
- import { Effect, Option, Schema } from "effect";
5
+ import { Effect, Equal, Match, Option, Schema } from "effect";
5
6
  import { defineMessageUnion } from "foldkit/message";
7
+ import { define } from "foldkit/customElement";
6
8
  import { Bundle, Link } from "foldkit-bundle";
7
9
  import * as Submodel from "foldkit/submodel";
8
10
  //#region src/editor-bundle.ts
@@ -25,6 +27,12 @@ const EditorState = Schema.Struct({
25
27
  /** Null inherits neighboring marks; an array explicitly sets them, even when empty. */
26
28
  storedMarks: Schema.NullOr(Schema.Array(Schema.String)),
27
29
  /**
30
+ * The highlighted entry in a live slash menu (§123). The document says whether the
31
+ * caret is in a query and what matches; this is the one thing the menu owns, and
32
+ * `update` resolves Enter against it.
33
+ */
34
+ menuIndex: Schema.Number,
35
+ /**
28
36
  * The host element the view renders and the patch Command finds (§118). It is
29
37
  * per-placement, so it arrives as the Bundle's args and is carried here
30
38
  * because `read` re-projects the whole child from the parent.
@@ -42,19 +50,78 @@ const EditorView = Schema.Struct({
42
50
  nextId: Schema.Number,
43
51
  history: RichText.History,
44
52
  storedMarks: Schema.NullOr(Schema.Array(Schema.String)),
53
+ menuIndex: Schema.Number,
45
54
  hostId: Schema.String
46
55
  });
47
- const idsOf = (content) => new Set(content.children.flatMap((block) => [block.id, ...block.children.map((run) => run.id)]));
48
- /** Everything that differs between two whole documents, for a replace patch. */
56
+ /** Every block and run of a document by id, nested ones included, with where it stands. */
57
+ const nodesOf = (content) => {
58
+ const nodes = /* @__PURE__ */ new Map();
59
+ const visit = (blocks, parent) => {
60
+ for (const [index, block] of blocks.entries()) {
61
+ nodes.set(block.id, {
62
+ node: block,
63
+ parent,
64
+ index
65
+ });
66
+ for (const [at, run] of block.children.entries()) nodes.set(run.id, {
67
+ node: run,
68
+ parent: block.id,
69
+ index: at
70
+ });
71
+ if (block.type === "Node" && block.blocks !== void 0) visit(block.blocks, block.id);
72
+ }
73
+ };
74
+ visit(content.children, null);
75
+ return nodes;
76
+ };
77
+ const sameMarks = (left, right) => left.marks.length === right.marks.length && left.marks.every((mark, index) => RichText.sameMark(mark, right.marks[index]));
78
+ /** A block's own fields, apart from what it holds: its type, level, kind and props. */
79
+ const ownFields = ({ children: _runs, ...block }) => ({
80
+ ...block,
81
+ blocks: void 0
82
+ });
83
+ /**
84
+ * What differs between two whole documents, for a patch from one to the other, in a
85
+ * transaction's terms: the runs whose text or marks changed and the blocks holding them,
86
+ * blocks whose own fields changed, what came and went, and whether any block moved.
87
+ */
49
88
  const replaceChangeSet = (previous, next) => {
50
- const before = idsOf(previous);
51
- const after = idsOf(next);
89
+ const before = nodesOf(previous);
90
+ const after = nodesOf(next);
91
+ const dirtyNodes = /* @__PURE__ */ new Set();
92
+ const textChanged = /* @__PURE__ */ new Set();
93
+ let structureChanged = false;
94
+ for (const [id, placed] of after) {
95
+ const was = before.get(id);
96
+ if (placed.node.type === "Text") {
97
+ const run = placed.node;
98
+ const old = was?.node.type === "Text" ? was.node : void 0;
99
+ if (old === void 0 || old.text !== run.text) textChanged.add(id);
100
+ if (old === void 0 || old.text !== run.text || !sameMarks(old, run)) {
101
+ dirtyNodes.add(id);
102
+ if (placed.parent !== null) dirtyNodes.add(placed.parent);
103
+ }
104
+ if (was !== void 0 && (was.parent !== placed.parent || was.index !== placed.index)) {
105
+ if (placed.parent !== null) dirtyNodes.add(placed.parent);
106
+ if (was.parent !== null && after.has(was.parent)) dirtyNodes.add(was.parent);
107
+ }
108
+ continue;
109
+ }
110
+ if (was === void 0 || was.parent !== placed.parent || was.index !== placed.index) structureChanged = true;
111
+ if (was === void 0 || !Equal.equals(ownFields(was.node), ownFields(placed.node))) dirtyNodes.add(id);
112
+ }
113
+ const removedNodes = new Set([...before.keys()].filter((id) => !after.has(id)));
114
+ for (const id of removedNodes) {
115
+ const was = before.get(id);
116
+ if (was.node.type !== "Text") structureChanged = true;
117
+ else if (was.parent !== null && after.has(was.parent)) dirtyNodes.add(was.parent);
118
+ }
52
119
  return {
53
- dirtyNodes: after,
54
- insertedNodes: new Set([...after].filter((id) => !before.has(id))),
55
- removedNodes: new Set([...before].filter((id) => !after.has(id))),
56
- textChanged: /* @__PURE__ */ new Set(),
57
- structureChanged: true,
120
+ dirtyNodes,
121
+ insertedNodes: new Set([...after.keys()].filter((id) => !before.has(id))),
122
+ removedNodes,
123
+ textChanged,
124
+ structureChanged,
58
125
  selectionChanged: false
59
126
  };
60
127
  };
@@ -71,6 +138,32 @@ const toCommand = (message) => {
71
138
  type: "ToggleMark",
72
139
  mark: message.mark
73
140
  };
141
+ case "AppliedMark": return {
142
+ type: "SetMark",
143
+ mark: message.mark
144
+ };
145
+ case "ClearedMark": return {
146
+ type: "ClearMark",
147
+ mark: message.mark
148
+ };
149
+ case "RetypedBlock": return {
150
+ type: "RetypeBlock",
151
+ to: message.block
152
+ };
153
+ case "WrappedBlock": return {
154
+ type: "WrapBlock",
155
+ containers: message.containers
156
+ };
157
+ case "ConvertedBlock": return {
158
+ type: "ConvertBlock",
159
+ to: message.to
160
+ };
161
+ case "LiftedBlock": return { type: "LiftBlock" };
162
+ case "MovedBlock": return {
163
+ type: "MoveBlock",
164
+ node: message.node,
165
+ to: message.to
166
+ };
74
167
  case "Selected": return {
75
168
  type: "SetSelection",
76
169
  selection: message.selection
@@ -92,68 +185,175 @@ const patch = (hostId, state, changeSet) => ({
92
185
  patchEditor(hostId, state, changeSet);
93
186
  }), Message.Patched())
94
187
  });
95
- const Editor = Bundle.make({
96
- name: "RichTextEditor",
97
- Model: EditorView,
98
- Message,
99
- args: Schema.Struct({ hostId: Schema.String }),
100
- init: (args) => ({ model: {
101
- document: RichText.Document.make({
102
- version: 1,
103
- children: []
104
- }),
105
- selection: null,
106
- nextId: 0,
107
- history: RichText.emptyHistory,
108
- storedMarks: null,
109
- hostId: args.hostId
110
- } }),
111
- update: (model, message) => {
112
- const state = {
113
- document: model.document,
114
- selection: model.selection
115
- };
116
- if (message._tag === "Patched") return { model };
117
- if (message._tag === "Undone" || message._tag === "Redone") {
118
- const restored = message._tag === "Undone" ? RichText.undo(model.history, state) : RichText.redo(model.history, state);
119
- if (restored === void 0) return {
120
- model,
121
- outMessage: {
122
- _tag: "Rejected",
123
- error: message._tag === "Undone" ? "NothingToUndo" : "NothingToRedo"
124
- }
125
- };
126
- const changeSet = replaceChangeSet(model.document, restored.state.document);
127
- return {
128
- model: {
129
- ...model,
130
- selection: restored.state.selection,
131
- history: restored.history
132
- },
133
- outMessage: {
134
- _tag: "Replaced",
135
- state: restored.state,
136
- changeSet
137
- },
138
- commands: [patch(model.hostId, restored.state, changeSet)]
139
- };
140
- }
188
+ /**
189
+ * The text between the start of the caret's block and the caret, or `''` when the
190
+ * selection names no caret. A menu and the input rules read this; nothing about the query
191
+ * is stored. A range is read from its start, which is where what is typed over it lands,
192
+ * whichever way it was dragged.
193
+ */
194
+ const textBeforeOf = (model) => {
195
+ if (model.selection?.type !== "Range") return "";
196
+ const start = RichText.rangeStart(model.document, model.selection);
197
+ return start === void 0 ? "" : RichText.textBefore(model.document, start);
198
+ };
199
+ /**
200
+ * The kinds of the node blocks around the caret's block, innermost first; a range is read
201
+ * from its start, as `textBeforeOf` reads it.
202
+ */
203
+ const withinOf = (model) => {
204
+ if (model.selection?.type !== "Range") return [];
205
+ const start = RichText.rangeStart(model.document, model.selection);
206
+ const run = start === void 0 ? void 0 : RichText.locateRun(model.document, start.node);
207
+ if (run === void 0) return [];
208
+ const kinds = [];
209
+ for (let depth = run.path.length - 1; depth > 0; depth--) {
210
+ const block = RichText.blockAtPath(model.document, run.path.slice(0, depth));
211
+ if (block?.type === "Node") kinds.push(block.kind);
212
+ }
213
+ return kinds;
214
+ };
215
+ const Host = define({
216
+ tag: "foldkit-richtext",
217
+ properties: {},
218
+ events: {}
219
+ });
220
+ /**
221
+ * Each host id's keys, by the document shown. The host's Mount reads its document once, so a
222
+ * document that did not come from an edit here (a form's fill or reset, an entry opened, a
223
+ * revision restored) needs a fresh host. A committed edit moves its document's key to the next
224
+ * document (`carryKey`), and the document it leaves keeps none, so returning to it is a new host
225
+ * too. The first document a host id shows has no key, as the server's render has none.
226
+ */
227
+ const lineages = /* @__PURE__ */ new Map();
228
+ const lineageOf = (hostId) => {
229
+ const found = lineages.get(hostId);
230
+ if (found !== void 0) return found;
231
+ const created = {
232
+ next: 0,
233
+ keys: /* @__PURE__ */ new WeakMap()
234
+ };
235
+ lineages.set(hostId, created);
236
+ return created;
237
+ };
238
+ const hostKey = (hostId, document) => {
239
+ const lineage = lineageOf(hostId);
240
+ const key = lineage.keys.get(document) ?? lineage.next++;
241
+ lineage.keys.set(document, key);
242
+ return key === 0 ? void 0 : `${hostId}-${key}`;
243
+ };
244
+ const carryKey = (hostId, from, to) => {
245
+ const keys = lineageOf(hostId).keys;
246
+ const key = keys.get(from);
247
+ if (key === void 0 || from === to) return;
248
+ keys.delete(from);
249
+ keys.set(to, key);
250
+ };
251
+ /**
252
+ * Patches the editor at `hostId` to `next`, a document the parent put in place of
253
+ * `previous`, keeping the host element instead of mounting a new one. A parent that
254
+ * replaces its document outside an edit here (another replica's change arriving) returns
255
+ * this, in the same transition as the replacement.
256
+ *
257
+ * The host is found by the document the view renders, so `previous` is the object it
258
+ * rendered and `next.document` the object it will render; building the Command hands the
259
+ * host on from one to the other, so build it only to return it. The patch itself starts
260
+ * from the latest state the editor holds (what it has drawn, or a state waiting on a
261
+ * composition), and `next.selection` becomes the browser selection;
262
+ * a replacement that changes nothing drawn leaves the DOM alone, selection included, as an
263
+ * exchange that only confirms edits already shown does.
264
+ */
265
+ const patchTo = (hostId, previous, next) => {
266
+ carryKey(hostId, previous, next.document);
267
+ return {
268
+ name: "RichText.patch",
269
+ effect: Effect.as(Effect.sync(() => {
270
+ const held = heldDocument(hostId);
271
+ if (held === void 0) return;
272
+ const changeSet = replaceChangeSet(held, next.document);
273
+ if (!(!changeSet.structureChanged && changeSet.dirtyNodes.size === 0 && changeSet.removedNodes.size === 0)) patchEditor(hostId, next, changeSet);
274
+ }), Message.Patched())
275
+ };
276
+ };
277
+ /**
278
+ * Draws `decorations` over the editor at `hostId`, beside what its placement draws, until the
279
+ * next overlay replaces them: decorations an application derives from its own state, such as
280
+ * other people's carets, which a placement's `decorate` cannot see. Their positions name the
281
+ * document shown, so an application sets them again when that changes. The caret stays put.
282
+ */
283
+ const overlay = (hostId, decorations) => ({
284
+ name: "RichText.overlay",
285
+ effect: Effect.as(Effect.sync(() => {
286
+ placeOverlay(hostId, decorations);
287
+ redecorateEditor(hostId);
288
+ }), Message.Patched())
289
+ });
290
+ /**
291
+ * The host element the editor mounts into, and whose id the patch Command finds (§118). It is a
292
+ * custom element so hydration leaves what is inside it alone (§145): on the server it holds the
293
+ * document's markup, which the editor adopts; in the browser the view declares it empty, and the
294
+ * editor owns everything below.
295
+ */
296
+ const editorView = Submodel.defineView((model, h) => {
297
+ const serverRendered = serverRenderedFor(model.hostId)();
298
+ const key = serverRendered ? void 0 : hostKey(model.hostId, model.document);
299
+ return Host.withMessage(h)([
300
+ h.Id(model.hostId),
301
+ ...key === void 0 ? [] : [h.Key(key)],
302
+ h.Style({ display: "block" }),
303
+ h.OnMount(events({ content: model.document }))
304
+ ], serverRendered ? [renderEditable(model.document, renderingFor(model.hostId), decorationsFor(model.hostId)(model.document))] : []);
305
+ });
306
+ /** What a Message does to the editor; `Editor.update` carries the host's key on a commit. */
307
+ const transition = (model, incoming) => {
308
+ const state = {
309
+ document: model.document,
310
+ selection: model.selection
311
+ };
312
+ const vocabulary = vocabularyFor(model.hostId);
313
+ const menu = incoming._tag === "Entered" ? slashMenu(slashEntries, textBeforeOf(model), model.menuIndex) : void 0;
314
+ const message = menu?.highlighted?.message ?? incoming;
315
+ return Match.value(message).pipe(Match.withReturnType(), Match.tags({
316
+ Patched: () => ({ model }),
317
+ Undone: (undone) => travel(model, state, undone),
318
+ Redone: (redone) => travel(model, state, redone)
319
+ }), Match.orElse((message) => {
141
320
  let nextId = model.nextId;
142
321
  const collapsed = model.selection?.type === "Range" && model.selection.anchor.node === model.selection.focus.node && model.selection.anchor.offset === model.selection.focus.offset;
143
- if (message._tag === "ToggledMark" && collapsed && !RichText.shippedRegistry.declares(message.mark)) return {
322
+ if (message._tag === "ToggledMark" && collapsed && !(vocabulary.marks ?? RichText.shippedRegistry).accepts(message.mark)) return {
144
323
  model,
145
324
  outMessage: {
146
325
  _tag: "Rejected",
147
326
  error: "InvalidInput"
148
327
  }
149
328
  };
150
- const storedMarks = message._tag === "ToggledMark" && collapsed ? model.storedMarks?.includes(message.mark) ? model.storedMarks.filter((mark) => mark !== message.mark) : [...model.storedMarks ?? [], message.mark] : message._tag === "Selected" ? null : model.storedMarks;
329
+ const storedMarks = message._tag === "ToggledMark" && collapsed ? model.storedMarks?.includes(message.mark) ? model.storedMarks.filter((mark) => mark !== message.mark) : [...model.storedMarks ?? [], message.mark] : model.storedMarks;
151
330
  const command = message._tag === "Typed" && storedMarks !== null ? {
152
331
  type: "InsertText",
153
332
  text: message.text,
154
333
  marks: storedMarks
155
334
  } : toCommand(message);
156
- const result = RichText.run(state, command, { mint: () => `e${nextId++}` });
335
+ const queryRange = menu === void 0 || menu.highlighted === void 0 || model.selection?.type !== "Range" ? void 0 : RichText.textRangeBefore(model.document, model.selection.anchor, menu.query.length + 1);
336
+ const runAction = (action) => RichText.runAction(state, action, { mint: () => `e${nextId++}` }, {
337
+ marks: vocabulary.marks,
338
+ nodes: vocabulary.nodes
339
+ });
340
+ const ruled = queryRange === void 0 && message._tag === "Typed" ? RichText.applyInputRules(inputRulesFor(model.hostId), {
341
+ textBefore: textBeforeOf(model),
342
+ text: message.text,
343
+ insertion: command,
344
+ within: withinOf(model)
345
+ }) : void 0;
346
+ const attempt = runAction(queryRange !== void 0 ? [
347
+ {
348
+ type: "SetSelection",
349
+ selection: queryRange
350
+ },
351
+ { type: "DeleteBackward" },
352
+ command
353
+ ] : ruled ?? [command]);
354
+ const fallback = !attempt.ok && ruled !== void 0;
355
+ if (fallback) nextId = model.nextId;
356
+ const result = fallback ? runAction([command]) : attempt;
157
357
  if (!result.ok) return {
158
358
  model,
159
359
  outMessage: {
@@ -161,24 +361,75 @@ const Editor = Bundle.make({
161
361
  error: result.error
162
362
  }
163
363
  };
364
+ if (result.state === state && storedMarks === model.storedMarks) return { model };
164
365
  const contentChanged = result.state.document !== model.document;
165
366
  return {
166
367
  model: {
167
368
  ...model,
168
369
  selection: result.state.selection,
169
370
  nextId,
170
- storedMarks,
371
+ storedMarks: message._tag === "Selected" ? null : storedMarks,
171
372
  history: contentChanged ? RichText.commit(model.history, state, { group: RichText.groupFor(command) }) : model.history
172
373
  },
173
374
  outMessage: {
174
375
  _tag: "Edited",
175
376
  state: result.state,
176
- changeSet: result.changeSet
377
+ changeSet: result.changeSet,
378
+ transactions: result.transactions
177
379
  },
178
380
  commands: [patch(model.hostId, result.state, result.changeSet)]
179
381
  };
382
+ }));
383
+ };
384
+ /** Undo or redo: history restores content and selection, and the host is told as a replace. */
385
+ const travel = (model, state, message) => {
386
+ const restored = message._tag === "Undone" ? RichText.undo(model.history, state) : RichText.redo(model.history, state);
387
+ if (restored === void 0) return {
388
+ model,
389
+ outMessage: {
390
+ _tag: "Rejected",
391
+ error: message._tag === "Undone" ? "NothingToUndo" : "NothingToRedo"
392
+ }
393
+ };
394
+ const changeSet = replaceChangeSet(model.document, restored.state.document);
395
+ return {
396
+ model: {
397
+ ...model,
398
+ selection: restored.state.selection,
399
+ history: restored.history
400
+ },
401
+ outMessage: {
402
+ _tag: "Replaced",
403
+ state: restored.state,
404
+ changeSet
405
+ },
406
+ commands: [patch(model.hostId, restored.state, changeSet)]
407
+ };
408
+ };
409
+ const Editor = Bundle.make({
410
+ name: "RichTextEditor",
411
+ Model: EditorView,
412
+ Message,
413
+ args: Schema.Struct({ hostId: Schema.String }),
414
+ init: (args) => ({ model: {
415
+ document: RichText.Document.make({
416
+ version: 1,
417
+ children: []
418
+ }),
419
+ selection: null,
420
+ nextId: 0,
421
+ history: RichText.emptyHistory,
422
+ storedMarks: null,
423
+ menuIndex: 0,
424
+ hostId: args.hostId
425
+ } }),
426
+ update: (model, incoming) => {
427
+ const result = transition(model, incoming);
428
+ const out = result.outMessage;
429
+ if (out?._tag === "Edited" || out?._tag === "Replaced") carryKey(model.hostId, model.document, out.state.document);
430
+ return result;
180
431
  },
181
- view: Submodel.defineView((model, h) => h.div([h.Id(model.hostId), h.OnMount(events({ content: model.document }))], []))
432
+ view: editorView
182
433
  });
183
434
  const GotEditor = Link.wrapper("GotEditorMessage", Message);
184
435
  const ParentMessage = defineMessageUnion({ ...GotEditor.cases });
@@ -189,29 +440,39 @@ const editorLink = Link.make({
189
440
  nextId: parent.editor.nextId,
190
441
  history: parent.editor.history,
191
442
  storedMarks: parent.editor.storedMarks,
443
+ menuIndex: parent.editor.menuIndex,
192
444
  hostId: parent.editor.hostId
193
445
  }),
194
- write: (parent, child) => ({
195
- ...parent,
196
- editor: {
197
- selection: child.selection,
198
- nextId: child.nextId,
199
- history: child.history,
200
- storedMarks: child.storedMarks,
201
- hostId: child.hostId
202
- }
203
- }),
446
+ write: (parent, { document: _document, ...editor }) => {
447
+ const current = parent.editor;
448
+ return current !== void 0 && Object.keys(editor).every((key) => editor[key] === current[key]) ? parent : {
449
+ ...parent,
450
+ editor
451
+ };
452
+ },
204
453
  wrapper: GotEditor,
205
454
  path: ["editor"]
206
455
  });
207
456
  /**
208
- * Places one editor, bound to the host element the view renders and the patch
209
- * Command finds. Each placement picks its own id. A rendering registry (§121) is
210
- * placed for that id rather than passed as an arg (§122): it holds functions, so
211
- * the mount looks it up by host id instead.
457
+ * Records everything a placement names for its host id, replacing what the id had: one call,
458
+ * so a placement cannot half-apply. `editorAt` and the form control both place through it.
459
+ */
460
+ const placeEditor = (hostId, placement) => {
461
+ placeRendering(hostId, placement.rendering ?? RichText.noRendering);
462
+ placeVocabulary(hostId, placement.vocabulary ?? {});
463
+ placeInputRules(hostId, placement.inputRules ?? []);
464
+ placeDecorations(hostId, placement.decorate ?? (() => []));
465
+ placeOverlay(hostId, []);
466
+ placePlaceholder(hostId, placement.placeholder);
467
+ placeServerRendered(hostId, placement.serverRendered ?? (() => false));
468
+ };
469
+ /**
470
+ * Places one editor, bound to the host element the view renders and the patch Command
471
+ * finds. Each placement picks its own id and names what it places; one call records all
472
+ * of it, so a placement cannot half-apply.
212
473
  */
213
- const editorAt = (hostId, rendering = RichText.noRendering) => {
214
- placeRendering(hostId, rendering);
474
+ const editorAt = (hostId, placement = {}) => {
475
+ placeEditor(hostId, placement);
215
476
  return Editor.at(editorLink, {
216
477
  args: { hostId },
217
478
  onOut: (out) => (parent) => out._tag === "Edited" || out._tag === "Replaced" ? { model: {
@@ -233,11 +494,21 @@ const edited = (message) => GotEditor.make(message);
233
494
  const typed = (text) => edited(Message.Typed({ text }));
234
495
  const pressed = (tag) => edited(Message[tag]());
235
496
  const toggled = (mark) => edited(Message.ToggledMark({ mark }));
497
+ const applied = (mark) => edited(Message.AppliedMark({ mark }));
498
+ const cleared = (mark) => edited(Message.ClearedMark({ mark }));
499
+ const retyped = (block) => edited(Message.RetypedBlock({ block }));
500
+ const wrapped = (containers) => edited(Message.WrappedBlock({ containers }));
501
+ const converted = (to) => edited(Message.ConvertedBlock({ to }));
502
+ const lifted = () => edited(Message.LiftedBlock());
503
+ const moved = (node, to) => edited(Message.MovedBlock({
504
+ node,
505
+ to
506
+ }));
236
507
  const selected = (selection) => edited(Message.Selected({ selection }));
237
508
  const undone = () => edited(Message.Undone());
238
509
  const redone = () => edited(Message.Redone());
239
510
  const patched = () => edited(Message.Patched());
240
511
  //#endregion
241
- export { Editor, EditorState, EditorView, Model, ParentMessage, application, edited, editor, editorAt, patched, pressed, redone, replaceChangeSet, selected, toggled, typed, undone, update };
512
+ export { Editor, EditorState, EditorView, Model, ParentMessage, application, applied, cleared, converted, edited, editor, editorAt, editorView, lifted, moved, overlay, patchTo, patched, placeEditor, pressed, redone, replaceChangeSet, retyped, selected, toggled, typed, undone, update, wrapped };
242
513
 
243
514
  //# sourceMappingURL=editor-bundle.mjs.map