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.
- package/README.md +448 -166
- package/dist/decorations-Ar6URa7n.mjs +57 -0
- package/dist/decorations-Ar6URa7n.mjs.map +1 -0
- package/dist/editor-C76tfEyV.d.mts +1016 -0
- package/dist/editor-C76tfEyV.d.mts.map +1 -0
- package/dist/editor-bundle-dnjyuk1P.d.mts +8576 -0
- package/dist/editor-bundle-dnjyuk1P.d.mts.map +1 -0
- package/dist/editor-bundle.d.mts +2 -7535
- package/dist/editor-bundle.mjs +352 -81
- package/dist/editor-bundle.mjs.map +1 -1
- package/dist/editor.d.mts +2 -849
- package/dist/editor.mjs +176 -9
- package/dist/editor.mjs.map +1 -1
- package/dist/{events-BTw9SJ7r.d.mts → events-Dasls6a_.d.mts} +31 -5
- package/dist/events-Dasls6a_.d.mts.map +1 -0
- package/dist/events.d.mts +2 -2
- package/dist/events.mjs +84 -14
- package/dist/events.mjs.map +1 -1
- package/dist/host-1dtxuKIA.d.mts +86 -0
- package/dist/host-1dtxuKIA.d.mts.map +1 -0
- package/dist/host.d.mts +2 -25
- package/dist/host.mjs +78 -6
- package/dist/host.mjs.map +1 -1
- package/dist/html.d.mts +1 -1
- package/dist/html.d.mts.map +1 -1
- package/dist/html.mjs +150 -20
- package/dist/html.mjs.map +1 -1
- package/dist/index-DDV_JesZ.d.mts +57 -0
- package/dist/index-DDV_JesZ.d.mts.map +1 -0
- package/dist/index.d.mts +2 -2
- package/dist/index.mjs +249 -99
- package/dist/index.mjs.map +1 -1
- package/dist/input.d.mts +936 -0
- package/dist/input.d.mts.map +1 -0
- package/dist/input.mjs +89 -0
- package/dist/input.mjs.map +1 -0
- package/dist/toolbar.d.mts +39 -0
- package/dist/toolbar.d.mts.map +1 -1
- package/dist/toolbar.mjs +325 -1
- package/dist/toolbar.mjs.map +1 -1
- package/dist/view.d.mts +14 -2
- package/dist/view.d.mts.map +1 -1
- package/dist/view.mjs +90 -19
- package/dist/view.mjs.map +1 -1
- package/package.json +26 -5
- package/dist/editor-bundle.d.mts.map +0 -1
- package/dist/editor.d.mts.map +0 -1
- package/dist/events-BTw9SJ7r.d.mts.map +0 -1
- package/dist/host.d.mts.map +0 -1
- package/dist/index-6QEOehXh.d.mts +0 -38
- package/dist/index-6QEOehXh.d.mts.map +0 -1
package/dist/editor-bundle.mjs
CHANGED
|
@@ -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
|
-
|
|
48
|
-
|
|
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 =
|
|
51
|
-
const after =
|
|
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
|
|
54
|
-
insertedNodes: new Set([...after].filter((id) => !before.has(id))),
|
|
55
|
-
removedNodes
|
|
56
|
-
textChanged
|
|
57
|
-
structureChanged
|
|
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
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
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.
|
|
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] :
|
|
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
|
|
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:
|
|
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,
|
|
195
|
-
|
|
196
|
-
editor: {
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
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
|
-
*
|
|
209
|
-
*
|
|
210
|
-
|
|
211
|
-
|
|
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,
|
|
214
|
-
|
|
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
|