foldkit-richtext-dom 0.1.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.
@@ -0,0 +1 @@
1
+ {"version":3,"file":"editor.d.mts","names":[],"sources":["../src/editor.ts"],"mappings":";;;;;qBAea,mCAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;uBA+Ec;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;uBAgD2hG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;uBAAoiU;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;uBAAizK;;;;;;;;;;;;;;;;;;;;;;;mBAAupI;;;;;;;;;yBAA2Z;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;yBAAm9H;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;yBAAkvR;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;YAjH7nnC,iBAAiB,QAAQ;;;;;;YAOzB,cAAc,QAAQ;WAAoB;;;;;;;;qBAQzC,YAAa,SAAS,SAAS,YAAU;;;;;;;qBA6BzC,eACX,MAAM,SACN,SAAS,SAAS,UAClB,OAAO,SAAS,sBAChB,YAAW,SAAS,cAAA;;;;;;qBAqBT,cACX,gBACA,OAAO,SAAS,aAChB,WAAW,SAAS;;;;qBAaT,QAAM,MAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;qBA4BmvxF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;qBAAmiJ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;qBAAoiU;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;qBAAizK;;;;;;;;;;;;;;;;;;;;;;;iBAAupI;;;;;;;;;uBAA2Z;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;uBAAm9H;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;uBAAkvR"}
@@ -0,0 +1,95 @@
1
+ import { attachmentIn, mountInto, releaseMount, renderingFor } from "./host.mjs";
2
+ import * as RichText from "foldkit-richtext";
3
+ import { Effect, Queue, Schema, Stream } from "effect";
4
+ import { defineMessageUnion } from "foldkit/message";
5
+ import * as Mount from "foldkit/mount";
6
+ //#region src/editor.ts
7
+ /**
8
+ * The editor's Message vocabulary and the mount that produces it (§§31, 118).
9
+ *
10
+ * The DOM adapter reports what the browser asked for as commands, a caret, and a
11
+ * history chord. This is what those mean to an editor's `update`, so one set of
12
+ * Messages arrives whether a person typed, pasted, or moved the caret. The Bundle
13
+ * proof in `controlled.ts` consumed these by hand; `events` is where the browser
14
+ * produces them.
15
+ */
16
+ const Message = defineMessageUnion({
17
+ Typed: { text: Schema.String },
18
+ Backspace: {},
19
+ DeletedForward: {},
20
+ Entered: {},
21
+ ToggledMark: { mark: Schema.String },
22
+ Selected: { selection: Schema.NullOr(RichText.Selection) },
23
+ Pasted: { slice: RichText.Slice },
24
+ Undone: {},
25
+ Redone: {},
26
+ /** The patch Command's completion: Foldkit Commands report a Message, and a
27
+ * render has nothing to say beyond that it happened. */
28
+ Patched: {}
29
+ });
30
+ /**
31
+ * What a browser intent means to an editor. The adapter reports only plain
32
+ * insertions, so an insertion carrying marks is refused rather than quietly
33
+ * losing them: what a marked insertion means is the editor's decision, not the
34
+ * adapter's, and the adapter never has one to report.
35
+ */
36
+ const toMessage = (command) => {
37
+ switch (command.type) {
38
+ case "InsertText": return command.marks === void 0 ? Message.Typed({ text: command.text }) : void 0;
39
+ case "DeleteBackward": return Message.Backspace();
40
+ case "DeleteForward": return Message.DeletedForward();
41
+ case "SplitBlock": return Message.Entered();
42
+ case "ToggleMark": return typeof command.mark === "string" ? Message.ToggledMark({ mark: command.mark }) : void 0;
43
+ case "Paste": return Message.Pasted({ slice: command.slice });
44
+ default: return;
45
+ }
46
+ };
47
+ /**
48
+ * Attaches the translation to a host element and reports each Message through
49
+ * `emit`. Separate from the mount so a test can drive the DOM without pulling a
50
+ * stream, and so a caller embedding the editor directly can hand it a rendering
51
+ * registry.
52
+ */
53
+ const attachEditor = (host, content, emit, rendering = RichText.noRendering) => mountInto(host, content, {
54
+ onIntent: (command) => {
55
+ const message = toMessage(command);
56
+ if (message !== void 0) emit(message);
57
+ },
58
+ onHistory: (direction) => emit(direction === "undo" ? Message.Undone() : Message.Redone()),
59
+ onSelection: (selection) => emit(Message.Selected({ selection }))
60
+ }, rendering);
61
+ /**
62
+ * The patch Command's work: find the element the view gave an id to and sync the
63
+ * attachment it holds. A missing host is not an error — the editor went away
64
+ * while the transition was in flight — so it reports whether it patched.
65
+ */
66
+ const patchEditor = (hostId, state, changeSet) => {
67
+ const host = document.getElementById(hostId);
68
+ if (host === null) return false;
69
+ const attachment = attachmentIn(host);
70
+ if (attachment === void 0) return false;
71
+ attachment.sync(state, changeSet);
72
+ return true;
73
+ };
74
+ /** The editor's events as a mount: one attachment per element, released with it.
75
+ * The rendering registry is looked up by the host id the placement recorded
76
+ * (§122), so it reaches the mount without entering a Model or a mount's args. */
77
+ const events = Mount.defineStream("RichTextDomEvents", {
78
+ args: { content: RichText.Document },
79
+ messages: [
80
+ Message.Typed,
81
+ Message.Backspace,
82
+ Message.DeletedForward,
83
+ Message.Entered,
84
+ Message.ToggledMark,
85
+ Message.Selected,
86
+ Message.Pasted,
87
+ Message.Undone,
88
+ Message.Redone
89
+ ],
90
+ execute: ({ element, content }) => Stream.callback((queue) => Effect.acquireRelease(Effect.sync(() => attachEditor(element, content, (message) => Queue.offerUnsafe(queue, message), renderingFor(element.id))), () => Effect.sync(() => releaseMount(element))))
91
+ });
92
+ //#endregion
93
+ export { Message, attachEditor, events, patchEditor, toMessage };
94
+
95
+ //# sourceMappingURL=editor.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"editor.mjs","names":[],"sources":["../src/editor.ts"],"sourcesContent":["/**\n * The editor's Message vocabulary and the mount that produces it (§§31, 118).\n *\n * The DOM adapter reports what the browser asked for as commands, a caret, and a\n * history chord. This is what those mean to an editor's `update`, so one set of\n * Messages arrives whether a person typed, pasted, or moved the caret. The Bundle\n * proof in `controlled.ts` consumed these by hand; `events` is where the browser\n * produces them.\n */\nimport { Effect, Queue, Schema, Stream } from 'effect'\nimport { defineMessageUnion } from 'foldkit/message'\nimport * as Mount from 'foldkit/mount'\nimport * as RichText from 'foldkit-richtext'\nimport { attachmentIn, mountInto, releaseMount, renderingFor } from './host.js'\n\nexport const Message = defineMessageUnion({\n Typed: { text: Schema.String },\n Backspace: {},\n DeletedForward: {},\n Entered: {},\n ToggledMark: { mark: Schema.String },\n Selected: { selection: Schema.NullOr(RichText.Selection) },\n Pasted: { slice: RichText.Slice },\n Undone: {},\n Redone: {},\n /** The patch Command's completion: Foldkit Commands report a Message, and a\n * render has nothing to say beyond that it happened. */\n Patched: {},\n})\nexport type Message = typeof Message.Type\n\n/**\n * What the DOM can ask for: the vocabulary without the patch acknowledgement,\n * which only the Command produces. The adapter's stream is typed by this, so a\n * mount's result union names every Message it can actually emit.\n */\nexport type EditorEvent = Exclude<Message, { readonly _tag: 'Patched' }>\n\n/**\n * What a browser intent means to an editor. The adapter reports only plain\n * insertions, so an insertion carrying marks is refused rather than quietly\n * losing them: what a marked insertion means is the editor's decision, not the\n * adapter's, and the adapter never has one to report.\n */\nexport const toMessage = (command: RichText.Command): EditorEvent | undefined => {\n switch (command.type) {\n case 'InsertText':\n return command.marks === undefined ? Message.Typed({ text: command.text }) : undefined\n case 'DeleteBackward':\n return Message.Backspace()\n case 'DeleteForward':\n return Message.DeletedForward()\n case 'SplitBlock':\n return Message.Entered()\n case 'ToggleMark':\n // The adapter toggles by name; a mark value carrying props is not\n // expressible as a toggle, so it is refused rather than stripped.\n return typeof command.mark === 'string'\n ? Message.ToggledMark({ mark: command.mark })\n : undefined\n case 'Paste':\n return Message.Pasted({ slice: command.slice })\n default:\n return undefined\n }\n}\n\n/**\n * Attaches the translation to a host element and reports each Message through\n * `emit`. Separate from the mount so a test can drive the DOM without pulling a\n * stream, and so a caller embedding the editor directly can hand it a rendering\n * registry.\n */\nexport const attachEditor = (\n host: Element,\n content: RichText.Document,\n emit: (message: EditorEvent) => void,\n rendering: RichText.Rendering = RichText.noRendering,\n) =>\n mountInto(\n host,\n content,\n {\n onIntent: command => {\n const message = toMessage(command)\n if (message !== undefined) emit(message)\n },\n onHistory: direction => emit(direction === 'undo' ? Message.Undone() : Message.Redone()),\n onSelection: selection => emit(Message.Selected({ selection })),\n },\n rendering,\n )\n\n/**\n * The patch Command's work: find the element the view gave an id to and sync the\n * attachment it holds. A missing host is not an error — the editor went away\n * while the transition was in flight — so it reports whether it patched.\n */\nexport const patchEditor = (\n hostId: string,\n state: RichText.EditorState,\n changeSet: RichText.ChangeSet,\n): boolean => {\n const host = document.getElementById(hostId)\n if (host === null) return false\n const attachment = attachmentIn(host)\n if (attachment === undefined) return false\n attachment.sync(state, changeSet)\n return true\n}\n\n/** The editor's events as a mount: one attachment per element, released with it.\n * The rendering registry is looked up by the host id the placement recorded\n * (§122), so it reaches the mount without entering a Model or a mount's args. */\nexport const events = Mount.defineStream('RichTextDomEvents', {\n args: { content: RichText.Document },\n messages: [\n Message.Typed,\n Message.Backspace,\n Message.DeletedForward,\n Message.Entered,\n Message.ToggledMark,\n Message.Selected,\n Message.Pasted,\n Message.Undone,\n Message.Redone,\n ],\n execute: ({ element, content }) =>\n Stream.callback<EditorEvent>(queue =>\n Effect.acquireRelease(\n Effect.sync(() =>\n attachEditor(\n element,\n content,\n message => Queue.offerUnsafe(queue, message),\n renderingFor(element.id),\n ),\n ),\n () => Effect.sync(() => releaseMount(element)),\n ),\n ),\n})\n"],"mappings":";;;;;;;;;;;;;;;AAeA,MAAa,UAAU,mBAAmB;CACxC,OAAO,EAAE,MAAM,OAAO,OAAO;CAC7B,WAAW,CAAC;CACZ,gBAAgB,CAAC;CACjB,SAAS,CAAC;CACV,aAAa,EAAE,MAAM,OAAO,OAAO;CACnC,UAAU,EAAE,WAAW,OAAO,OAAO,SAAS,SAAS,EAAE;CACzD,QAAQ,EAAE,OAAO,SAAS,MAAM;CAChC,QAAQ,CAAC;CACT,QAAQ,CAAC;;;CAGT,SAAS,CAAC;AACZ,CAAC;;;;;;;AAgBD,MAAa,aAAa,YAAuD;CAC/E,QAAQ,QAAQ,MAAhB;EACE,KAAK,cACH,OAAO,QAAQ,UAAU,KAAA,IAAY,QAAQ,MAAM,EAAE,MAAM,QAAQ,KAAK,CAAC,IAAI,KAAA;EAC/E,KAAK,kBACH,OAAO,QAAQ,UAAU;EAC3B,KAAK,iBACH,OAAO,QAAQ,eAAe;EAChC,KAAK,cACH,OAAO,QAAQ,QAAQ;EACzB,KAAK,cAGH,OAAO,OAAO,QAAQ,SAAS,WAC3B,QAAQ,YAAY,EAAE,MAAM,QAAQ,KAAK,CAAC,IAC1C,KAAA;EACN,KAAK,SACH,OAAO,QAAQ,OAAO,EAAE,OAAO,QAAQ,MAAM,CAAC;EAChD,SACE;CACJ;AACF;;;;;;;AAQA,MAAa,gBACX,MACA,SACA,MACA,YAAgC,SAAS,gBAEzC,UACE,MACA,SACA;CACE,WAAU,YAAW;EACnB,MAAM,UAAU,UAAU,OAAO;EACjC,IAAI,YAAY,KAAA,GAAW,KAAK,OAAO;CACzC;CACA,YAAW,cAAa,KAAK,cAAc,SAAS,QAAQ,OAAO,IAAI,QAAQ,OAAO,CAAC;CACvF,cAAa,cAAa,KAAK,QAAQ,SAAS,EAAE,UAAU,CAAC,CAAC;AAChE,GACA,SACF;;;;;;AAOF,MAAa,eACX,QACA,OACA,cACY;CACZ,MAAM,OAAO,SAAS,eAAe,MAAM;CAC3C,IAAI,SAAS,MAAM,OAAO;CAC1B,MAAM,aAAa,aAAa,IAAI;CACpC,IAAI,eAAe,KAAA,GAAW,OAAO;CACrC,WAAW,KAAK,OAAO,SAAS;CAChC,OAAO;AACT;;;;AAKA,MAAa,SAAS,MAAM,aAAa,qBAAqB;CAC5D,MAAM,EAAE,SAAS,SAAS,SAAS;CACnC,UAAU;EACR,QAAQ;EACR,QAAQ;EACR,QAAQ;EACR,QAAQ;EACR,QAAQ;EACR,QAAQ;EACR,QAAQ;EACR,QAAQ;EACR,QAAQ;CACV;CACA,UAAU,EAAE,SAAS,cACnB,OAAO,UAAsB,UAC3B,OAAO,eACL,OAAO,WACL,aACE,SACA,UACA,YAAW,MAAM,YAAY,OAAO,OAAO,GAC3C,aAAa,QAAQ,EAAE,CACzB,CACF,SACM,OAAO,WAAW,aAAa,OAAO,CAAC,CAC/C,CACF;AACJ,CAAC"}
@@ -0,0 +1,77 @@
1
+ import { t as EditorDom } from "./index-6QEOehXh.mjs";
2
+ import * as RichText from "foldkit-richtext";
3
+ //#region src/events.d.ts
4
+ interface Intent {
5
+ /** The semantic command to run, when the event maps to one. */
6
+ readonly command?: RichText.Command;
7
+ /** A history intent: undo or redo, which are editor intents, not commands. */
8
+ readonly history?: 'undo' | 'redo';
9
+ readonly preventDefault: boolean;
10
+ }
11
+ /**
12
+ * One chord an application adds to the adapter's table, or overrides in it.
13
+ * Modifiers match exactly and the key matches what the browser reports, so
14
+ * `Mod-b` does not fire for Shift held, and a chord that types a symbol names the
15
+ * symbol: Shift+8 is `Mod-Shift-*` on a US layout, not `Mod-Shift-8`.
16
+ */
17
+ interface KeyBinding {
18
+ /** `Mod-b`, `Mod-Shift-b`, `Alt-ArrowUp`. `Mod` is Meta or Control, either one. */
19
+ readonly chord: string;
20
+ readonly command: RichText.Command;
21
+ }
22
+ /**
23
+ * Reads one event as intent. `composing` tells the adapter that an IME owns the
24
+ * interaction: the browser must keep its temporary text, so composition input
25
+ * is passed through and committed on `compositionend` instead.
26
+ *
27
+ * `keymap` is checked before the built-in chords, so an application adds to the
28
+ * table or overrides an entry without forking the adapter.
29
+ */
30
+ declare const intentFor: (event: Event, composing?: boolean, keymap?: ReadonlyArray<KeyBinding>) => Intent | undefined;
31
+ /** The semantic selection the browser is currently showing, if it resolves. */
32
+ declare const readSelection: (dom: EditorDom) => RichText.Selection | null;
33
+ /**
34
+ * Puts the browser selection back where the semantic state says it is. Direction
35
+ * is preserved through `setBaseAndExtent` when the environment has it; jsdom and
36
+ * some engines only offer a range, which cannot express direction.
37
+ */
38
+ declare const restoreSelection: (dom: EditorDom, selection: RichText.Selection | null) => void;
39
+ /** The clipboard type a Foldkit RichText slice travels under. */
40
+ declare const SLICE_CLIPBOARD_TYPE = "application/x-foldkit-richtext+json";
41
+ interface AttachOptions {
42
+ /** Called with each semantic command the browser produced. */
43
+ readonly onIntent: (command: RichText.Command) => void;
44
+ /** Called for undo and redo, which are editor intents rather than commands. */
45
+ readonly onHistory?: (direction: 'undo' | 'redo') => void;
46
+ /**
47
+ * Called when the caret or range moves to a position the application did not
48
+ * just commit. Without it the editor never learns where the caret is; the
49
+ * adapter cannot follow a selection it is not told about.
50
+ */
51
+ readonly onSelection?: (selection: RichText.Selection | null) => void;
52
+ /**
53
+ * When given, imported HTML is constrained to this vocabulary: a node kind
54
+ * the Kit does not declare is degraded to a paragraph, never kept.
55
+ */
56
+ readonly kit?: RichText.Kit | undefined;
57
+ /** Chords the application adds or overrides, checked before the built-ins. */
58
+ readonly keymap?: ReadonlyArray<KeyBinding> | undefined;
59
+ }
60
+ interface Attachment {
61
+ /** The current subtree; replaced as patches are applied. */
62
+ readonly current: () => EditorDom;
63
+ /** Applies a committed state, patching and restoring the browser selection. */
64
+ readonly sync: (state: RichText.EditorState, changeSet: RichText.ChangeSet) => void;
65
+ /** True between compositionstart and compositionend. */
66
+ readonly composing: () => boolean;
67
+ readonly detach: () => void;
68
+ }
69
+ /**
70
+ * Wires the owned subtree to an application. The adapter never decides what an
71
+ * edit means: it translates events into commands, hands them to `onIntent`, and
72
+ * patches whatever the application commits.
73
+ */
74
+ declare const attach: (dom: EditorDom, options: AttachOptions) => Attachment;
75
+ //#endregion
76
+ export { SLICE_CLIPBOARD_TYPE as a, readSelection as c, KeyBinding as i, restoreSelection as l, Attachment as n, attach as o, Intent as r, intentFor as s, AttachOptions as t };
77
+ //# sourceMappingURL=events-BTw9SJ7r.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"events-BTw9SJ7r.d.mts","names":[],"sources":["../src/events.ts"],"mappings":";;;UAkBiB;;WAEN,UAAU,SAAS;;WAEnB;WACA;;;;;;;;UASM;;WAEN;WACA,SAAS,SAAS;;;;;;;;;;cA+ChB,YACX,OAAO,OACP,qBACA,SAAQ,cAAc,gBACrB;;cAiDU,gBAAiB,KAAK,cAAY,SAAS;;;;;;cAuB3C,mBAAoB,KAAK,WAAW,WAAW,SAAS;;cAoBxD;UAOI;;WAEN,WAAW,SAAS,SAAS;;WAE7B,aAAa;;;;;;WAMb,eAAe,WAAW,SAAS;;;;;WAKnC,MAAM,SAAS;;WAEf,SAAS,cAAc;;UAGjB;;WAEN,eAAe;;WAEf,OAAO,OAAO,SAAS,aAAa,WAAW,SAAS;;WAExD;WACA;;;;;;;cAsBE,SAAU,KAAK,WAAW,SAAS,kBAAgB"}
@@ -0,0 +1,2 @@
1
+ import { a as SLICE_CLIPBOARD_TYPE, c as readSelection, i as KeyBinding, l as restoreSelection, n as Attachment, o as attach, r as Intent, s as intentFor, t as AttachOptions } from "./events-BTw9SJ7r.mjs";
2
+ export { AttachOptions, Attachment, Intent, KeyBinding, SLICE_CLIPBOARD_TYPE, attach, intentFor, readSelection, restoreSelection };
@@ -0,0 +1,305 @@
1
+ import { patch, positionToRange, rangeToPosition, repair } from "./index.mjs";
2
+ import { parseHtml } from "./html.mjs";
3
+ import * as RichText from "foldkit-richtext";
4
+ //#region src/events.ts
5
+ /**
6
+ * Browser events as editor intent (§§30–31). The adapter owns the subtree, so
7
+ * every event it understands is `preventDefault`ed: the browser must not mutate
8
+ * the DOM behind the semantic document. An event the adapter does not
9
+ * understand is left alone (the caller lets it through), and one it understands
10
+ * but cannot yet honor is prevented without a command — a deliberate no-op
11
+ * rather than a silent DOM divergence.
12
+ */
13
+ const chordMatches = (chord, event) => {
14
+ const parts = chord.split("-");
15
+ const name = parts[parts.length - 1] ?? "";
16
+ const modifiers = parts.slice(0, -1);
17
+ const wantsMod = modifiers.includes("Mod");
18
+ if ((event.metaKey || event.ctrlKey) !== wantsMod) return false;
19
+ if (event.shiftKey !== modifiers.includes("Shift")) return false;
20
+ if (event.altKey !== modifiers.includes("Alt")) return false;
21
+ return name.length === 1 ? event.key.toLowerCase() === name.toLowerCase() : event.key === name;
22
+ };
23
+ const MOD = (event) => event.metaKey || event.ctrlKey;
24
+ const historyChord = (event) => {
25
+ if (!MOD(event)) return void 0;
26
+ const key = event.key.toLowerCase();
27
+ if (key === "z") return event.shiftKey ? "redo" : "undo";
28
+ if (key === "y") return "redo";
29
+ };
30
+ const markChord = (event) => {
31
+ if (!MOD(event)) return void 0;
32
+ switch (event.key.toLowerCase()) {
33
+ case "b": return "Bold";
34
+ case "i": return "Italic";
35
+ case "e": return "Code";
36
+ default: return;
37
+ }
38
+ };
39
+ /**
40
+ * Reads one event as intent. `composing` tells the adapter that an IME owns the
41
+ * interaction: the browser must keep its temporary text, so composition input
42
+ * is passed through and committed on `compositionend` instead.
43
+ *
44
+ * `keymap` is checked before the built-in chords, so an application adds to the
45
+ * table or overrides an entry without forking the adapter.
46
+ */
47
+ const intentFor = (event, composing = false, keymap = []) => {
48
+ if (event.type === "beforeinput") {
49
+ const input = event;
50
+ const inputType = input.inputType ?? "";
51
+ if (composing || inputType === "insertCompositionText") return { preventDefault: false };
52
+ switch (inputType) {
53
+ case "insertText": return input.data == null || input.data.length === 0 ? { preventDefault: true } : {
54
+ preventDefault: true,
55
+ command: {
56
+ type: "InsertText",
57
+ text: input.data
58
+ }
59
+ };
60
+ case "insertParagraph":
61
+ case "insertLineBreak": return {
62
+ preventDefault: true,
63
+ command: { type: "SplitBlock" }
64
+ };
65
+ case "deleteContentBackward": return {
66
+ preventDefault: true,
67
+ command: { type: "DeleteBackward" }
68
+ };
69
+ case "deleteContentForward": return {
70
+ preventDefault: true,
71
+ command: { type: "DeleteForward" }
72
+ };
73
+ case "formatBold": return {
74
+ preventDefault: true,
75
+ command: {
76
+ type: "ToggleMark",
77
+ mark: "Bold"
78
+ }
79
+ };
80
+ case "formatItalic": return {
81
+ preventDefault: true,
82
+ command: {
83
+ type: "ToggleMark",
84
+ mark: "Italic"
85
+ }
86
+ };
87
+ default: return inputType.length === 0 ? void 0 : { preventDefault: true };
88
+ }
89
+ }
90
+ if (event.type !== "keydown") return void 0;
91
+ const key = event;
92
+ if (composing) return { preventDefault: false };
93
+ const bound = keymap.find((binding) => chordMatches(binding.chord, key));
94
+ if (bound !== void 0) return {
95
+ preventDefault: true,
96
+ command: bound.command
97
+ };
98
+ const history = historyChord(key);
99
+ if (history !== void 0) return {
100
+ preventDefault: true,
101
+ history
102
+ };
103
+ const mark = markChord(key);
104
+ if (mark !== void 0) return {
105
+ preventDefault: true,
106
+ command: {
107
+ type: "ToggleMark",
108
+ mark
109
+ }
110
+ };
111
+ switch (key.key) {
112
+ case "Enter": return {
113
+ preventDefault: true,
114
+ command: { type: "SplitBlock" }
115
+ };
116
+ case "Backspace": return {
117
+ preventDefault: true,
118
+ command: { type: "DeleteBackward" }
119
+ };
120
+ case "Delete": return {
121
+ preventDefault: true,
122
+ command: { type: "DeleteForward" }
123
+ };
124
+ default: return;
125
+ }
126
+ };
127
+ /** The semantic selection the browser is currently showing, if it resolves. */
128
+ const readSelection = (dom) => {
129
+ const selection = dom.root.ownerDocument.defaultView?.getSelection();
130
+ if (selection === null || selection === void 0 || selection.rangeCount === 0) return null;
131
+ const anchor = rangeToPosition(dom, selection.anchorNode ?? dom.root, selection.anchorOffset);
132
+ const focus = rangeToPosition(dom, selection.focusNode ?? dom.root, selection.focusOffset);
133
+ if (anchor === void 0 || focus === void 0) return null;
134
+ return {
135
+ type: "Range",
136
+ anchor,
137
+ focus
138
+ };
139
+ };
140
+ const textFor = (dom, position) => {
141
+ const range = positionToRange(dom, position);
142
+ const container = range?.startContainer;
143
+ return container instanceof Text ? {
144
+ node: container,
145
+ offset: range.startOffset
146
+ } : void 0;
147
+ };
148
+ /**
149
+ * Puts the browser selection back where the semantic state says it is. Direction
150
+ * is preserved through `setBaseAndExtent` when the environment has it; jsdom and
151
+ * some engines only offer a range, which cannot express direction.
152
+ */
153
+ const restoreSelection = (dom, selection) => {
154
+ const window = dom.root.ownerDocument.defaultView;
155
+ const live = window?.getSelection();
156
+ if (window === null || window === void 0 || live === null || live === void 0) return;
157
+ live.removeAllRanges();
158
+ if (selection === null || selection.type === "Node") return;
159
+ const anchor = textFor(dom, selection.anchor);
160
+ const focus = textFor(dom, selection.focus);
161
+ if (anchor === void 0 || focus === void 0) return;
162
+ if (typeof live.setBaseAndExtent === "function") {
163
+ live.setBaseAndExtent(anchor.node, anchor.offset, focus.node, focus.offset);
164
+ return;
165
+ }
166
+ const range = dom.root.ownerDocument.createRange();
167
+ range.setStart(anchor.node, anchor.offset);
168
+ range.setEnd(focus.node, focus.offset);
169
+ live.addRange(range);
170
+ };
171
+ /** The clipboard type a Foldkit RichText slice travels under. */
172
+ const SLICE_CLIPBOARD_TYPE = "application/x-foldkit-richtext+json";
173
+ const samePosition = (left, right) => left.node === right.node && left.offset === right.offset;
174
+ const sameSelection = (left, right) => {
175
+ if (left === null || right === null) return left === right;
176
+ if (left.type === "Node" || right.type === "Node") return left.type === "Node" && right.type === "Node" && left.node === right.node;
177
+ return samePosition(left.anchor, right.anchor) && samePosition(left.focus, right.focus);
178
+ };
179
+ /**
180
+ * Wires the owned subtree to an application. The adapter never decides what an
181
+ * edit means: it translates events into commands, hands them to `onIntent`, and
182
+ * patches whatever the application commits.
183
+ */
184
+ const attach = (dom, options) => {
185
+ let current = dom;
186
+ let composing = false;
187
+ /**
188
+ * The selection the application last committed, or the adapter last reported.
189
+ * Restoring it fires `selectionchange`, so without this the adapter would
190
+ * report the position it had just been told as if a person had moved there.
191
+ * Affinity is ignored: it is derived from the range, not carried by it.
192
+ */
193
+ let lastSelection = null;
194
+ const reportSelection = () => {
195
+ if (composing) return;
196
+ const selection = readSelection(current);
197
+ if (sameSelection(selection, lastSelection)) return;
198
+ lastSelection = selection;
199
+ options.onSelection?.(selection);
200
+ };
201
+ /**
202
+ * The semantic selection as it was before the browser took over. The live
203
+ * caret during composition points into text the document does not have, so
204
+ * committing against it would resolve against the wrong document — or fail.
205
+ */
206
+ let composingSelection = null;
207
+ let placeholderIds = 0;
208
+ const onEvent = (event) => {
209
+ const intent = intentFor(event, composing, options.keymap);
210
+ if (intent === void 0) return;
211
+ if (intent.preventDefault) event.preventDefault();
212
+ if (intent.command !== void 0) options.onIntent(intent.command);
213
+ if (intent.history !== void 0) options.onHistory?.(intent.history);
214
+ };
215
+ const onCompositionStart = () => {
216
+ composing = true;
217
+ composingSelection = readSelection(current);
218
+ };
219
+ const onCompositionEnd = (event) => {
220
+ composing = false;
221
+ const data = event.data;
222
+ current = repair(current, current.content);
223
+ restoreSelection(current, composingSelection);
224
+ composingSelection = null;
225
+ if (data != null && data.length > 0) options.onIntent({
226
+ type: "InsertText",
227
+ text: data
228
+ });
229
+ };
230
+ const writeClipboard = (clipboard, slice) => {
231
+ clipboard.setData(SLICE_CLIPBOARD_TYPE, RichText.serializeSlice(slice));
232
+ clipboard.setData("text/html", RichText.toHtml(slice.blocks));
233
+ clipboard.setData("text/plain", RichText.toText(slice.blocks));
234
+ };
235
+ const onCopy = (event) => {
236
+ const clipboard = event.clipboardData;
237
+ const slice = RichText.sliceOf(current.content, readSelection(current));
238
+ if (clipboard === void 0 || slice === void 0) return;
239
+ event.preventDefault();
240
+ writeClipboard(clipboard, slice);
241
+ };
242
+ const onCut = (event) => {
243
+ const clipboard = event.clipboardData;
244
+ const selection = readSelection(current);
245
+ const slice = RichText.sliceOf(current.content, selection);
246
+ if (clipboard === void 0 || slice === void 0) return;
247
+ event.preventDefault();
248
+ writeClipboard(clipboard, slice);
249
+ if (!(selection?.type === "Range" && selection.anchor.node === selection.focus.node && selection.anchor.offset === selection.focus.offset)) options.onIntent({ type: "DeleteBackward" });
250
+ };
251
+ const onPaste = (event) => {
252
+ const clipboard = event.clipboardData;
253
+ if (clipboard === void 0) return;
254
+ const payload = clipboard.getData(SLICE_CLIPBOARD_TYPE);
255
+ const html = clipboard.getData("text/html");
256
+ const text = clipboard.getData("text/plain");
257
+ const placeholders = () => `clipboard-${++placeholderIds}`;
258
+ const slice = (payload.length > 0 ? RichText.deserializeSlice(payload) : void 0) ?? (html.length > 0 ? {
259
+ version: 1,
260
+ blocks: parseHtml(html, {
261
+ kit: options.kit,
262
+ mint: placeholders
263
+ }).blocks
264
+ } : void 0) ?? (text.length > 0 ? RichText.sliceFromText(text, placeholders) : void 0);
265
+ if (slice === void 0 || slice.blocks.length === 0) return;
266
+ event.preventDefault();
267
+ options.onIntent({
268
+ type: "Paste",
269
+ slice
270
+ });
271
+ };
272
+ const target = dom.root;
273
+ const ownerDocument = target.ownerDocument;
274
+ ownerDocument.addEventListener("selectionchange", reportSelection);
275
+ target.addEventListener("beforeinput", onEvent);
276
+ target.addEventListener("keydown", onEvent);
277
+ target.addEventListener("compositionstart", onCompositionStart);
278
+ target.addEventListener("compositionend", onCompositionEnd);
279
+ target.addEventListener("copy", onCopy);
280
+ target.addEventListener("cut", onCut);
281
+ target.addEventListener("paste", onPaste);
282
+ return {
283
+ current: () => current,
284
+ composing: () => composing,
285
+ sync: (state, changeSet) => {
286
+ current = patch(current, state.document, changeSet);
287
+ lastSelection = state.selection;
288
+ restoreSelection(current, state.selection);
289
+ },
290
+ detach: () => {
291
+ ownerDocument.removeEventListener("selectionchange", reportSelection);
292
+ target.removeEventListener("beforeinput", onEvent);
293
+ target.removeEventListener("keydown", onEvent);
294
+ target.removeEventListener("compositionstart", onCompositionStart);
295
+ target.removeEventListener("compositionend", onCompositionEnd);
296
+ target.removeEventListener("copy", onCopy);
297
+ target.removeEventListener("cut", onCut);
298
+ target.removeEventListener("paste", onPaste);
299
+ }
300
+ };
301
+ };
302
+ //#endregion
303
+ export { SLICE_CLIPBOARD_TYPE, attach, intentFor, readSelection, restoreSelection };
304
+
305
+ //# sourceMappingURL=events.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"events.mjs","names":["patchInto"],"sources":["../src/events.ts"],"sourcesContent":["/**\n * Browser events as editor intent (§§30–31). The adapter owns the subtree, so\n * every event it understands is `preventDefault`ed: the browser must not mutate\n * the DOM behind the semantic document. An event the adapter does not\n * understand is left alone (the caller lets it through), and one it understands\n * but cannot yet honor is prevented without a command — a deliberate no-op\n * rather than a silent DOM divergence.\n */\nimport * as RichText from 'foldkit-richtext'\nimport {\n patch as patchInto,\n positionToRange,\n rangeToPosition,\n repair,\n type EditorDom,\n} from './index.js'\nimport { parseHtml } from './html.js'\n\nexport interface Intent {\n /** The semantic command to run, when the event maps to one. */\n readonly command?: RichText.Command\n /** A history intent: undo or redo, which are editor intents, not commands. */\n readonly history?: 'undo' | 'redo'\n readonly preventDefault: boolean\n}\n\n/**\n * One chord an application adds to the adapter's table, or overrides in it.\n * Modifiers match exactly and the key matches what the browser reports, so\n * `Mod-b` does not fire for Shift held, and a chord that types a symbol names the\n * symbol: Shift+8 is `Mod-Shift-*` on a US layout, not `Mod-Shift-8`.\n */\nexport interface KeyBinding {\n /** `Mod-b`, `Mod-Shift-b`, `Alt-ArrowUp`. `Mod` is Meta or Control, either one. */\n readonly chord: string\n readonly command: RichText.Command\n}\n\nconst chordMatches = (chord: string, event: KeyboardEvent): boolean => {\n const parts = chord.split('-')\n const name = parts[parts.length - 1] ?? ''\n const modifiers = parts.slice(0, -1)\n const wantsMod = modifiers.includes('Mod')\n const hasMod = event.metaKey || event.ctrlKey\n if (hasMod !== wantsMod) return false\n if (event.shiftKey !== modifiers.includes('Shift')) return false\n if (event.altKey !== modifiers.includes('Alt')) return false\n return name.length === 1 ? event.key.toLowerCase() === name.toLowerCase() : event.key === name\n}\n\nconst MOD = (event: KeyboardEvent): boolean => event.metaKey || event.ctrlKey\n\nconst historyChord = (event: KeyboardEvent): 'undo' | 'redo' | undefined => {\n if (!MOD(event)) return undefined\n const key = event.key.toLowerCase()\n if (key === 'z') return event.shiftKey ? 'redo' : 'undo'\n if (key === 'y') return 'redo'\n return undefined\n}\n\nconst markChord = (event: KeyboardEvent): string | undefined => {\n if (!MOD(event)) return undefined\n switch (event.key.toLowerCase()) {\n case 'b':\n return 'Bold'\n case 'i':\n return 'Italic'\n case 'e':\n return 'Code'\n default:\n return undefined\n }\n}\n\n/**\n * Reads one event as intent. `composing` tells the adapter that an IME owns the\n * interaction: the browser must keep its temporary text, so composition input\n * is passed through and committed on `compositionend` instead.\n *\n * `keymap` is checked before the built-in chords, so an application adds to the\n * table or overrides an entry without forking the adapter.\n */\nexport const intentFor = (\n event: Event,\n composing = false,\n keymap: ReadonlyArray<KeyBinding> = [],\n): Intent | undefined => {\n if (event.type === 'beforeinput') {\n const input = event as Event & { readonly inputType?: string; readonly data?: string | null }\n const inputType = input.inputType ?? ''\n if (composing || inputType === 'insertCompositionText') return { preventDefault: false }\n switch (inputType) {\n case 'insertText':\n return input.data == null || input.data.length === 0\n ? { preventDefault: true }\n : { preventDefault: true, command: { type: 'InsertText', text: input.data } }\n case 'insertParagraph':\n case 'insertLineBreak':\n return { preventDefault: true, command: { type: 'SplitBlock' } }\n case 'deleteContentBackward':\n return { preventDefault: true, command: { type: 'DeleteBackward' } }\n case 'deleteContentForward':\n return { preventDefault: true, command: { type: 'DeleteForward' } }\n case 'formatBold':\n return { preventDefault: true, command: { type: 'ToggleMark', mark: 'Bold' } }\n case 'formatItalic':\n return { preventDefault: true, command: { type: 'ToggleMark', mark: 'Italic' } }\n default:\n // Paste, word deletion, autocorrect, and anything else the browser\n // invents: prevented so the DOM cannot drift, until it is implemented.\n return inputType.length === 0 ? undefined : { preventDefault: true }\n }\n }\n if (event.type !== 'keydown') return undefined\n const key = event as KeyboardEvent\n if (composing) return { preventDefault: false }\n const bound = keymap.find(binding => chordMatches(binding.chord, key))\n if (bound !== undefined) return { preventDefault: true, command: bound.command }\n const history = historyChord(key)\n if (history !== undefined) return { preventDefault: true, history }\n const mark = markChord(key)\n if (mark !== undefined) return { preventDefault: true, command: { type: 'ToggleMark', mark } }\n switch (key.key) {\n case 'Enter':\n return { preventDefault: true, command: { type: 'SplitBlock' } }\n case 'Backspace':\n return { preventDefault: true, command: { type: 'DeleteBackward' } }\n case 'Delete':\n return { preventDefault: true, command: { type: 'DeleteForward' } }\n default:\n return undefined\n }\n}\n\n/** The semantic selection the browser is currently showing, if it resolves. */\nexport const readSelection = (dom: EditorDom): RichText.Selection | null => {\n const selection = dom.root.ownerDocument.defaultView?.getSelection()\n if (selection === null || selection === undefined || selection.rangeCount === 0) return null\n const anchor = rangeToPosition(dom, selection.anchorNode ?? dom.root, selection.anchorOffset)\n const focus = rangeToPosition(dom, selection.focusNode ?? dom.root, selection.focusOffset)\n if (anchor === undefined || focus === undefined) return null\n return { type: 'Range', anchor, focus }\n}\n\nconst textFor = (\n dom: EditorDom,\n position: RichText.Position,\n): { node: Text; offset: number } | undefined => {\n const range = positionToRange(dom, position)\n const container = range?.startContainer\n return container instanceof Text ? { node: container, offset: range!.startOffset } : undefined\n}\n\n/**\n * Puts the browser selection back where the semantic state says it is. Direction\n * is preserved through `setBaseAndExtent` when the environment has it; jsdom and\n * some engines only offer a range, which cannot express direction.\n */\nexport const restoreSelection = (dom: EditorDom, selection: RichText.Selection | null): void => {\n const window = dom.root.ownerDocument.defaultView\n const live = window?.getSelection()\n if (window === null || window === undefined || live === null || live === undefined) return\n live.removeAllRanges()\n if (selection === null || selection.type === 'Node') return\n const anchor = textFor(dom, selection.anchor)\n const focus = textFor(dom, selection.focus)\n if (anchor === undefined || focus === undefined) return\n if (typeof live.setBaseAndExtent === 'function') {\n live.setBaseAndExtent(anchor.node, anchor.offset, focus.node, focus.offset)\n return\n }\n const range = dom.root.ownerDocument.createRange()\n range.setStart(anchor.node, anchor.offset)\n range.setEnd(focus.node, focus.offset)\n live.addRange(range)\n}\n\n/** The clipboard type a Foldkit RichText slice travels under. */\nexport const SLICE_CLIPBOARD_TYPE = 'application/x-foldkit-richtext+json'\n\ninterface ClipboardLike {\n getData: (type: string) => string\n setData: (type: string, value: string) => void\n}\n\nexport interface AttachOptions {\n /** Called with each semantic command the browser produced. */\n readonly onIntent: (command: RichText.Command) => void\n /** Called for undo and redo, which are editor intents rather than commands. */\n readonly onHistory?: (direction: 'undo' | 'redo') => void\n /**\n * Called when the caret or range moves to a position the application did not\n * just commit. Without it the editor never learns where the caret is; the\n * adapter cannot follow a selection it is not told about.\n */\n readonly onSelection?: (selection: RichText.Selection | null) => void\n /**\n * When given, imported HTML is constrained to this vocabulary: a node kind\n * the Kit does not declare is degraded to a paragraph, never kept.\n */\n readonly kit?: RichText.Kit | undefined\n /** Chords the application adds or overrides, checked before the built-ins. */\n readonly keymap?: ReadonlyArray<KeyBinding> | undefined\n}\n\nexport interface Attachment {\n /** The current subtree; replaced as patches are applied. */\n readonly current: () => EditorDom\n /** Applies a committed state, patching and restoring the browser selection. */\n readonly sync: (state: RichText.EditorState, changeSet: RichText.ChangeSet) => void\n /** True between compositionstart and compositionend. */\n readonly composing: () => boolean\n readonly detach: () => void\n}\n\nconst samePosition = (left: RichText.Position, right: RichText.Position): boolean =>\n left.node === right.node && left.offset === right.offset\n\nconst sameSelection = (\n left: RichText.Selection | null,\n right: RichText.Selection | null,\n): boolean => {\n if (left === null || right === null) return left === right\n if (left.type === 'Node' || right.type === 'Node') {\n return left.type === 'Node' && right.type === 'Node' && left.node === right.node\n }\n return samePosition(left.anchor, right.anchor) && samePosition(left.focus, right.focus)\n}\n\n/**\n * Wires the owned subtree to an application. The adapter never decides what an\n * edit means: it translates events into commands, hands them to `onIntent`, and\n * patches whatever the application commits.\n */\nexport const attach = (dom: EditorDom, options: AttachOptions): Attachment => {\n let current = dom\n let composing = false\n /**\n * The selection the application last committed, or the adapter last reported.\n * Restoring it fires `selectionchange`, so without this the adapter would\n * report the position it had just been told as if a person had moved there.\n * Affinity is ignored: it is derived from the range, not carried by it.\n */\n let lastSelection: RichText.Selection | null = null\n const reportSelection = (): void => {\n // While composing, the caret points into text the document does not have.\n if (composing) return\n const selection = readSelection(current)\n if (sameSelection(selection, lastSelection)) return\n lastSelection = selection\n options.onSelection?.(selection)\n }\n /**\n * The semantic selection as it was before the browser took over. The live\n * caret during composition points into text the document does not have, so\n * committing against it would resolve against the wrong document — or fail.\n */\n let composingSelection: RichText.Selection | null = null\n let placeholderIds = 0\n const onEvent = (event: Event): void => {\n const intent = intentFor(event, composing, options.keymap)\n if (intent === undefined) return\n if (intent.preventDefault) event.preventDefault()\n if (intent.command !== undefined) options.onIntent(intent.command)\n if (intent.history !== undefined) options.onHistory?.(intent.history)\n }\n const onCompositionStart = (): void => {\n composing = true\n composingSelection = readSelection(current)\n }\n const onCompositionEnd = (event: Event): void => {\n composing = false\n const data = (event as Event & { readonly data?: string | null }).data\n // The browser's temporary text is not in the document, whether the IME\n // committed or cancelled, so repair the subtree before anything else, then\n // put back the selection composition started from — not the caret the\n // browser moved into its own temporary text.\n current = repair(current, current.content)\n restoreSelection(current, composingSelection)\n composingSelection = null\n if (data != null && data.length > 0) options.onIntent({ type: 'InsertText', text: data })\n }\n const writeClipboard = (clipboard: ClipboardLike, slice: RichText.Slice): void => {\n clipboard.setData(SLICE_CLIPBOARD_TYPE, RichText.serializeSlice(slice))\n // HTML for other applications; importing it back waits on a\n // kit-constrained parser, so paste still prefers the slice.\n clipboard.setData('text/html', RichText.toHtml(slice.blocks))\n clipboard.setData('text/plain', RichText.toText(slice.blocks))\n }\n const onCopy = (event: Event): void => {\n const clipboard = (event as Event & { readonly clipboardData?: ClipboardLike }).clipboardData\n const slice = RichText.sliceOf(current.content, readSelection(current))\n if (clipboard === undefined || slice === undefined) return\n event.preventDefault()\n writeClipboard(clipboard, slice)\n }\n const onCut = (event: Event): void => {\n const clipboard = (event as Event & { readonly clipboardData?: ClipboardLike }).clipboardData\n const selection = readSelection(current)\n const slice = RichText.sliceOf(current.content, selection)\n if (clipboard === undefined || slice === undefined) return\n event.preventDefault()\n writeClipboard(clipboard, slice)\n // A collapsed caret cuts nothing; a range is removed through the same\n // delete intent a Backspace would produce.\n const collapsed =\n selection?.type === 'Range' &&\n selection.anchor.node === selection.focus.node &&\n selection.anchor.offset === selection.focus.offset\n if (!collapsed) options.onIntent({ type: 'DeleteBackward' })\n }\n const onPaste = (event: Event): void => {\n const clipboard = (event as Event & { readonly clipboardData?: ClipboardLike }).clipboardData\n if (clipboard === undefined) return\n const payload = clipboard.getData(SLICE_CLIPBOARD_TYPE)\n const html = clipboard.getData('text/html')\n const text = clipboard.getData('text/plain')\n const placeholders = () => `clipboard-${++placeholderIds}`\n // Slice first, then HTML, then plain text (§69). A payload this version\n // cannot read falls through rather than pasting nothing, and the Paste\n // command remints every identity, so these placeholders never reach the\n // document.\n const slice =\n (payload.length > 0 ? RichText.deserializeSlice(payload) : undefined) ??\n (html.length > 0\n ? {\n version: 1 as const,\n blocks: parseHtml(html, { kit: options.kit, mint: placeholders }).blocks,\n }\n : undefined) ??\n (text.length > 0 ? RichText.sliceFromText(text, placeholders) : undefined)\n if (slice === undefined || slice.blocks.length === 0) return\n event.preventDefault()\n options.onIntent({ type: 'Paste', slice })\n }\n const target = dom.root\n // The caret belongs to the document, not the subtree: a click elsewhere on the\n // page, or an arrow key, moves it without an event the root would see.\n const ownerDocument = target.ownerDocument\n ownerDocument.addEventListener('selectionchange', reportSelection)\n target.addEventListener('beforeinput', onEvent)\n target.addEventListener('keydown', onEvent)\n target.addEventListener('compositionstart', onCompositionStart)\n target.addEventListener('compositionend', onCompositionEnd)\n target.addEventListener('copy', onCopy)\n target.addEventListener('cut', onCut)\n target.addEventListener('paste', onPaste)\n return {\n current: () => current,\n composing: () => composing,\n sync: (state, changeSet) => {\n current = patchInto(current, state.document, changeSet)\n lastSelection = state.selection\n restoreSelection(current, state.selection)\n },\n detach: () => {\n ownerDocument.removeEventListener('selectionchange', reportSelection)\n target.removeEventListener('beforeinput', onEvent)\n target.removeEventListener('keydown', onEvent)\n target.removeEventListener('compositionstart', onCompositionStart)\n target.removeEventListener('compositionend', onCompositionEnd)\n target.removeEventListener('copy', onCopy)\n target.removeEventListener('cut', onCut)\n target.removeEventListener('paste', onPaste)\n },\n }\n}\n"],"mappings":";;;;;;;;;;;;AAsCA,MAAM,gBAAgB,OAAe,UAAkC;CACrE,MAAM,QAAQ,MAAM,MAAM,GAAG;CAC7B,MAAM,OAAO,MAAM,MAAM,SAAS,MAAM;CACxC,MAAM,YAAY,MAAM,MAAM,GAAG,EAAE;CACnC,MAAM,WAAW,UAAU,SAAS,KAAK;CAEzC,KADe,MAAM,WAAW,MAAM,aACvB,UAAU,OAAO;CAChC,IAAI,MAAM,aAAa,UAAU,SAAS,OAAO,GAAG,OAAO;CAC3D,IAAI,MAAM,WAAW,UAAU,SAAS,KAAK,GAAG,OAAO;CACvD,OAAO,KAAK,WAAW,IAAI,MAAM,IAAI,YAAY,MAAM,KAAK,YAAY,IAAI,MAAM,QAAQ;AAC5F;AAEA,MAAM,OAAO,UAAkC,MAAM,WAAW,MAAM;AAEtE,MAAM,gBAAgB,UAAsD;CAC1E,IAAI,CAAC,IAAI,KAAK,GAAG,OAAO,KAAA;CACxB,MAAM,MAAM,MAAM,IAAI,YAAY;CAClC,IAAI,QAAQ,KAAK,OAAO,MAAM,WAAW,SAAS;CAClD,IAAI,QAAQ,KAAK,OAAO;AAE1B;AAEA,MAAM,aAAa,UAA6C;CAC9D,IAAI,CAAC,IAAI,KAAK,GAAG,OAAO,KAAA;CACxB,QAAQ,MAAM,IAAI,YAAY,GAA9B;EACE,KAAK,KACH,OAAO;EACT,KAAK,KACH,OAAO;EACT,KAAK,KACH,OAAO;EACT,SACE;CACJ;AACF;;;;;;;;;AAUA,MAAa,aACX,OACA,YAAY,OACZ,SAAoC,CAAC,MACd;CACvB,IAAI,MAAM,SAAS,eAAe;EAChC,MAAM,QAAQ;EACd,MAAM,YAAY,MAAM,aAAa;EACrC,IAAI,aAAa,cAAc,yBAAyB,OAAO,EAAE,gBAAgB,MAAM;EACvF,QAAQ,WAAR;GACE,KAAK,cACH,OAAO,MAAM,QAAQ,QAAQ,MAAM,KAAK,WAAW,IAC/C,EAAE,gBAAgB,KAAK,IACvB;IAAE,gBAAgB;IAAM,SAAS;KAAE,MAAM;KAAc,MAAM,MAAM;IAAK;GAAE;GAChF,KAAK;GACL,KAAK,mBACH,OAAO;IAAE,gBAAgB;IAAM,SAAS,EAAE,MAAM,aAAa;GAAE;GACjE,KAAK,yBACH,OAAO;IAAE,gBAAgB;IAAM,SAAS,EAAE,MAAM,iBAAiB;GAAE;GACrE,KAAK,wBACH,OAAO;IAAE,gBAAgB;IAAM,SAAS,EAAE,MAAM,gBAAgB;GAAE;GACpE,KAAK,cACH,OAAO;IAAE,gBAAgB;IAAM,SAAS;KAAE,MAAM;KAAc,MAAM;IAAO;GAAE;GAC/E,KAAK,gBACH,OAAO;IAAE,gBAAgB;IAAM,SAAS;KAAE,MAAM;KAAc,MAAM;IAAS;GAAE;GACjF,SAGE,OAAO,UAAU,WAAW,IAAI,KAAA,IAAY,EAAE,gBAAgB,KAAK;EACvE;CACF;CACA,IAAI,MAAM,SAAS,WAAW,OAAO,KAAA;CACrC,MAAM,MAAM;CACZ,IAAI,WAAW,OAAO,EAAE,gBAAgB,MAAM;CAC9C,MAAM,QAAQ,OAAO,MAAK,YAAW,aAAa,QAAQ,OAAO,GAAG,CAAC;CACrE,IAAI,UAAU,KAAA,GAAW,OAAO;EAAE,gBAAgB;EAAM,SAAS,MAAM;CAAQ;CAC/E,MAAM,UAAU,aAAa,GAAG;CAChC,IAAI,YAAY,KAAA,GAAW,OAAO;EAAE,gBAAgB;EAAM;CAAQ;CAClE,MAAM,OAAO,UAAU,GAAG;CAC1B,IAAI,SAAS,KAAA,GAAW,OAAO;EAAE,gBAAgB;EAAM,SAAS;GAAE,MAAM;GAAc;EAAK;CAAE;CAC7F,QAAQ,IAAI,KAAZ;EACE,KAAK,SACH,OAAO;GAAE,gBAAgB;GAAM,SAAS,EAAE,MAAM,aAAa;EAAE;EACjE,KAAK,aACH,OAAO;GAAE,gBAAgB;GAAM,SAAS,EAAE,MAAM,iBAAiB;EAAE;EACrE,KAAK,UACH,OAAO;GAAE,gBAAgB;GAAM,SAAS,EAAE,MAAM,gBAAgB;EAAE;EACpE,SACE;CACJ;AACF;;AAGA,MAAa,iBAAiB,QAA8C;CAC1E,MAAM,YAAY,IAAI,KAAK,cAAc,aAAa,aAAa;CACnE,IAAI,cAAc,QAAQ,cAAc,KAAA,KAAa,UAAU,eAAe,GAAG,OAAO;CACxF,MAAM,SAAS,gBAAgB,KAAK,UAAU,cAAc,IAAI,MAAM,UAAU,YAAY;CAC5F,MAAM,QAAQ,gBAAgB,KAAK,UAAU,aAAa,IAAI,MAAM,UAAU,WAAW;CACzF,IAAI,WAAW,KAAA,KAAa,UAAU,KAAA,GAAW,OAAO;CACxD,OAAO;EAAE,MAAM;EAAS;EAAQ;CAAM;AACxC;AAEA,MAAM,WACJ,KACA,aAC+C;CAC/C,MAAM,QAAQ,gBAAgB,KAAK,QAAQ;CAC3C,MAAM,YAAY,OAAO;CACzB,OAAO,qBAAqB,OAAO;EAAE,MAAM;EAAW,QAAQ,MAAO;CAAY,IAAI,KAAA;AACvF;;;;;;AAOA,MAAa,oBAAoB,KAAgB,cAA+C;CAC9F,MAAM,SAAS,IAAI,KAAK,cAAc;CACtC,MAAM,OAAO,QAAQ,aAAa;CAClC,IAAI,WAAW,QAAQ,WAAW,KAAA,KAAa,SAAS,QAAQ,SAAS,KAAA,GAAW;CACpF,KAAK,gBAAgB;CACrB,IAAI,cAAc,QAAQ,UAAU,SAAS,QAAQ;CACrD,MAAM,SAAS,QAAQ,KAAK,UAAU,MAAM;CAC5C,MAAM,QAAQ,QAAQ,KAAK,UAAU,KAAK;CAC1C,IAAI,WAAW,KAAA,KAAa,UAAU,KAAA,GAAW;CACjD,IAAI,OAAO,KAAK,qBAAqB,YAAY;EAC/C,KAAK,iBAAiB,OAAO,MAAM,OAAO,QAAQ,MAAM,MAAM,MAAM,MAAM;EAC1E;CACF;CACA,MAAM,QAAQ,IAAI,KAAK,cAAc,YAAY;CACjD,MAAM,SAAS,OAAO,MAAM,OAAO,MAAM;CACzC,MAAM,OAAO,MAAM,MAAM,MAAM,MAAM;CACrC,KAAK,SAAS,KAAK;AACrB;;AAGA,MAAa,uBAAuB;AAqCpC,MAAM,gBAAgB,MAAyB,UAC7C,KAAK,SAAS,MAAM,QAAQ,KAAK,WAAW,MAAM;AAEpD,MAAM,iBACJ,MACA,UACY;CACZ,IAAI,SAAS,QAAQ,UAAU,MAAM,OAAO,SAAS;CACrD,IAAI,KAAK,SAAS,UAAU,MAAM,SAAS,QACzC,OAAO,KAAK,SAAS,UAAU,MAAM,SAAS,UAAU,KAAK,SAAS,MAAM;CAE9E,OAAO,aAAa,KAAK,QAAQ,MAAM,MAAM,KAAK,aAAa,KAAK,OAAO,MAAM,KAAK;AACxF;;;;;;AAOA,MAAa,UAAU,KAAgB,YAAuC;CAC5E,IAAI,UAAU;CACd,IAAI,YAAY;;;;;;;CAOhB,IAAI,gBAA2C;CAC/C,MAAM,wBAA8B;EAElC,IAAI,WAAW;EACf,MAAM,YAAY,cAAc,OAAO;EACvC,IAAI,cAAc,WAAW,aAAa,GAAG;EAC7C,gBAAgB;EAChB,QAAQ,cAAc,SAAS;CACjC;;;;;;CAMA,IAAI,qBAAgD;CACpD,IAAI,iBAAiB;CACrB,MAAM,WAAW,UAAuB;EACtC,MAAM,SAAS,UAAU,OAAO,WAAW,QAAQ,MAAM;EACzD,IAAI,WAAW,KAAA,GAAW;EAC1B,IAAI,OAAO,gBAAgB,MAAM,eAAe;EAChD,IAAI,OAAO,YAAY,KAAA,GAAW,QAAQ,SAAS,OAAO,OAAO;EACjE,IAAI,OAAO,YAAY,KAAA,GAAW,QAAQ,YAAY,OAAO,OAAO;CACtE;CACA,MAAM,2BAAiC;EACrC,YAAY;EACZ,qBAAqB,cAAc,OAAO;CAC5C;CACA,MAAM,oBAAoB,UAAuB;EAC/C,YAAY;EACZ,MAAM,OAAQ,MAAoD;EAKlE,UAAU,OAAO,SAAS,QAAQ,OAAO;EACzC,iBAAiB,SAAS,kBAAkB;EAC5C,qBAAqB;EACrB,IAAI,QAAQ,QAAQ,KAAK,SAAS,GAAG,QAAQ,SAAS;GAAE,MAAM;GAAc,MAAM;EAAK,CAAC;CAC1F;CACA,MAAM,kBAAkB,WAA0B,UAAgC;EAChF,UAAU,QAAQ,sBAAsB,SAAS,eAAe,KAAK,CAAC;EAGtE,UAAU,QAAQ,aAAa,SAAS,OAAO,MAAM,MAAM,CAAC;EAC5D,UAAU,QAAQ,cAAc,SAAS,OAAO,MAAM,MAAM,CAAC;CAC/D;CACA,MAAM,UAAU,UAAuB;EACrC,MAAM,YAAa,MAA6D;EAChF,MAAM,QAAQ,SAAS,QAAQ,QAAQ,SAAS,cAAc,OAAO,CAAC;EACtE,IAAI,cAAc,KAAA,KAAa,UAAU,KAAA,GAAW;EACpD,MAAM,eAAe;EACrB,eAAe,WAAW,KAAK;CACjC;CACA,MAAM,SAAS,UAAuB;EACpC,MAAM,YAAa,MAA6D;EAChF,MAAM,YAAY,cAAc,OAAO;EACvC,MAAM,QAAQ,SAAS,QAAQ,QAAQ,SAAS,SAAS;EACzD,IAAI,cAAc,KAAA,KAAa,UAAU,KAAA,GAAW;EACpD,MAAM,eAAe;EACrB,eAAe,WAAW,KAAK;EAO/B,IAAI,EAHF,WAAW,SAAS,WACpB,UAAU,OAAO,SAAS,UAAU,MAAM,QAC1C,UAAU,OAAO,WAAW,UAAU,MAAM,SAC9B,QAAQ,SAAS,EAAE,MAAM,iBAAiB,CAAC;CAC7D;CACA,MAAM,WAAW,UAAuB;EACtC,MAAM,YAAa,MAA6D;EAChF,IAAI,cAAc,KAAA,GAAW;EAC7B,MAAM,UAAU,UAAU,QAAQ,oBAAoB;EACtD,MAAM,OAAO,UAAU,QAAQ,WAAW;EAC1C,MAAM,OAAO,UAAU,QAAQ,YAAY;EAC3C,MAAM,qBAAqB,aAAa,EAAE;EAK1C,MAAM,SACH,QAAQ,SAAS,IAAI,SAAS,iBAAiB,OAAO,IAAI,KAAA,OAC1D,KAAK,SAAS,IACX;GACE,SAAS;GACT,QAAQ,UAAU,MAAM;IAAE,KAAK,QAAQ;IAAK,MAAM;GAAa,CAAC,CAAC,CAAC;EACpE,IACA,KAAA,OACH,KAAK,SAAS,IAAI,SAAS,cAAc,MAAM,YAAY,IAAI,KAAA;EAClE,IAAI,UAAU,KAAA,KAAa,MAAM,OAAO,WAAW,GAAG;EACtD,MAAM,eAAe;EACrB,QAAQ,SAAS;GAAE,MAAM;GAAS;EAAM,CAAC;CAC3C;CACA,MAAM,SAAS,IAAI;CAGnB,MAAM,gBAAgB,OAAO;CAC7B,cAAc,iBAAiB,mBAAmB,eAAe;CACjE,OAAO,iBAAiB,eAAe,OAAO;CAC9C,OAAO,iBAAiB,WAAW,OAAO;CAC1C,OAAO,iBAAiB,oBAAoB,kBAAkB;CAC9D,OAAO,iBAAiB,kBAAkB,gBAAgB;CAC1D,OAAO,iBAAiB,QAAQ,MAAM;CACtC,OAAO,iBAAiB,OAAO,KAAK;CACpC,OAAO,iBAAiB,SAAS,OAAO;CACxC,OAAO;EACL,eAAe;EACf,iBAAiB;EACjB,OAAO,OAAO,cAAc;GAC1B,UAAUA,MAAU,SAAS,MAAM,UAAU,SAAS;GACtD,gBAAgB,MAAM;GACtB,iBAAiB,SAAS,MAAM,SAAS;EAC3C;EACA,cAAc;GACZ,cAAc,oBAAoB,mBAAmB,eAAe;GACpE,OAAO,oBAAoB,eAAe,OAAO;GACjD,OAAO,oBAAoB,WAAW,OAAO;GAC7C,OAAO,oBAAoB,oBAAoB,kBAAkB;GACjE,OAAO,oBAAoB,kBAAkB,gBAAgB;GAC7D,OAAO,oBAAoB,QAAQ,MAAM;GACzC,OAAO,oBAAoB,OAAO,KAAK;GACvC,OAAO,oBAAoB,SAAS,OAAO;EAC7C;CACF;AACF"}
@@ -0,0 +1,25 @@
1
+ import { n as Attachment, t as AttachOptions } from "./events-BTw9SJ7r.mjs";
2
+ import * as RichText from "foldkit-richtext";
3
+ //#region src/host.d.ts
4
+ /**
5
+ * Records how a placement's host id renders (§122). A registry holds functions,
6
+ * so it cannot ride in a Bundle's args or Model; it is placed by host id because
7
+ * a view is placed before its element exists, and the mount looks it up with the
8
+ * id it renders. Re-placing an id replaces what it had.
9
+ */
10
+ export declare const placeRendering: (hostId: string, rendering: RichText.Rendering) => void;
11
+ /** The registry placed for a host id, or the default when none was placed. */
12
+ export declare const renderingFor: (hostId: string) => RichText.Rendering;
13
+ /**
14
+ * Renders `content` into `host` and records the attachment. The host is the
15
+ * view's element; the subtree the interpreter creates goes inside it, and the
16
+ * interpreter owns everything below. A rendering registry decides how a declared
17
+ * mark or node kind renders, and the same one is kept for later patches.
18
+ */
19
+ export declare const mountInto: (host: Element, content: RichText.Document, options: AttachOptions, rendering?: RichText.Rendering) => Attachment;
20
+ /** The attachment a host owns, for a patch Command that has only the element. */
21
+ export declare const attachmentIn: (host: Element) => Attachment | undefined;
22
+ /** Detaches a host's listeners and removes its subtree. Safe to call twice. */
23
+ export declare const releaseMount: (host: Element) => void;
24
+ //#endregion
25
+ //# sourceMappingURL=host.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"host.d.mts","names":[],"sources":["../src/host.ts"],"mappings":";;;;;;;;;qBAmBa,iBAAkB,gBAAgB,WAAW,SAAS;;qBAKtD,eAAgB,mBAAiB,SAAS;;;;;;;qBAS1C,YACX,MAAM,SACN,SAAS,SAAS,UAClB,SAAS,eACT,YAAW,SAAS,cACnB;;qBAYU,eAAgB,MAAM,YAAU;;qBAGhC,eAAgB,MAAM"}
package/dist/host.mjs ADDED
@@ -0,0 +1,52 @@
1
+ import { mount } from "./index.mjs";
2
+ import { attach } from "./events.mjs";
3
+ import * as RichText from "foldkit-richtext";
4
+ //#region src/host.ts
5
+ /**
6
+ * The host-element mount (§118): the bridge between a Foldkit view and the owned
7
+ * subtree. A view renders a host element and mounts the interpreter into it once;
8
+ * a patch Command later finds the attachment through that element, so a DOM
9
+ * reference never has to enter a Model.
10
+ */
11
+ const attachments = /* @__PURE__ */ new WeakMap();
12
+ const renderings = /* @__PURE__ */ new Map();
13
+ /**
14
+ * Records how a placement's host id renders (§122). A registry holds functions,
15
+ * so it cannot ride in a Bundle's args or Model; it is placed by host id because
16
+ * a view is placed before its element exists, and the mount looks it up with the
17
+ * id it renders. Re-placing an id replaces what it had.
18
+ */
19
+ const placeRendering = (hostId, rendering) => {
20
+ renderings.set(hostId, rendering);
21
+ };
22
+ /** The registry placed for a host id, or the default when none was placed. */
23
+ const renderingFor = (hostId) => renderings.get(hostId) ?? RichText.noRendering;
24
+ /**
25
+ * Renders `content` into `host` and records the attachment. The host is the
26
+ * view's element; the subtree the interpreter creates goes inside it, and the
27
+ * interpreter owns everything below. A rendering registry decides how a declared
28
+ * mark or node kind renders, and the same one is kept for later patches.
29
+ */
30
+ const mountInto = (host, content, options, rendering = RichText.noRendering) => {
31
+ releaseMount(host);
32
+ const dom = mount(host.ownerDocument, content, rendering);
33
+ host.append(dom.root);
34
+ const attachment = attach(dom, options);
35
+ attachments.set(host, attachment);
36
+ return attachment;
37
+ };
38
+ /** The attachment a host owns, for a patch Command that has only the element. */
39
+ const attachmentIn = (host) => attachments.get(host);
40
+ /** Detaches a host's listeners and removes its subtree. Safe to call twice. */
41
+ const releaseMount = (host) => {
42
+ const attachment = attachments.get(host);
43
+ if (attachment === void 0) return;
44
+ attachments.delete(host);
45
+ renderings.delete(host.id);
46
+ attachment.detach();
47
+ attachment.current().root.remove();
48
+ };
49
+ //#endregion
50
+ export { attachmentIn, mountInto, placeRendering, releaseMount, renderingFor };
51
+
52
+ //# sourceMappingURL=host.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"host.mjs","names":[],"sources":["../src/host.ts"],"sourcesContent":["/**\n * The host-element mount (§118): the bridge between a Foldkit view and the owned\n * subtree. A view renders a host element and mounts the interpreter into it once;\n * a patch Command later finds the attachment through that element, so a DOM\n * reference never has to enter a Model.\n */\nimport * as RichText from 'foldkit-richtext'\nimport { mount } from './index.js'\nimport { attach, type AttachOptions, type Attachment } from './events.js'\n\nconst attachments = new WeakMap<Element, Attachment>()\nconst renderings = new Map<string, RichText.Rendering>()\n\n/**\n * Records how a placement's host id renders (§122). A registry holds functions,\n * so it cannot ride in a Bundle's args or Model; it is placed by host id because\n * a view is placed before its element exists, and the mount looks it up with the\n * id it renders. Re-placing an id replaces what it had.\n */\nexport const placeRendering = (hostId: string, rendering: RichText.Rendering): void => {\n renderings.set(hostId, rendering)\n}\n\n/** The registry placed for a host id, or the default when none was placed. */\nexport const renderingFor = (hostId: string): RichText.Rendering =>\n renderings.get(hostId) ?? RichText.noRendering\n\n/**\n * Renders `content` into `host` and records the attachment. The host is the\n * view's element; the subtree the interpreter creates goes inside it, and the\n * interpreter owns everything below. A rendering registry decides how a declared\n * mark or node kind renders, and the same one is kept for later patches.\n */\nexport const mountInto = (\n host: Element,\n content: RichText.Document,\n options: AttachOptions,\n rendering: RichText.Rendering = RichText.noRendering,\n): Attachment => {\n // A mount runs once per element, so this is defensive: a remount replaces the\n // subtree rather than leaving two.\n releaseMount(host)\n const dom = mount(host.ownerDocument, content, rendering)\n host.append(dom.root)\n const attachment = attach(dom, options)\n attachments.set(host, attachment)\n return attachment\n}\n\n/** The attachment a host owns, for a patch Command that has only the element. */\nexport const attachmentIn = (host: Element): Attachment | undefined => attachments.get(host)\n\n/** Detaches a host's listeners and removes its subtree. Safe to call twice. */\nexport const releaseMount = (host: Element): void => {\n const attachment = attachments.get(host)\n if (attachment === undefined) return\n attachments.delete(host)\n renderings.delete(host.id)\n attachment.detach()\n attachment.current().root.remove()\n}\n"],"mappings":";;;;;;;;;;AAUA,MAAM,8BAAc,IAAI,QAA6B;AACrD,MAAM,6BAAa,IAAI,IAAgC;;;;;;;AAQvD,MAAa,kBAAkB,QAAgB,cAAwC;CACrF,WAAW,IAAI,QAAQ,SAAS;AAClC;;AAGA,MAAa,gBAAgB,WAC3B,WAAW,IAAI,MAAM,KAAK,SAAS;;;;;;;AAQrC,MAAa,aACX,MACA,SACA,SACA,YAAgC,SAAS,gBAC1B;CAGf,aAAa,IAAI;CACjB,MAAM,MAAM,MAAM,KAAK,eAAe,SAAS,SAAS;CACxD,KAAK,OAAO,IAAI,IAAI;CACpB,MAAM,aAAa,OAAO,KAAK,OAAO;CACtC,YAAY,IAAI,MAAM,UAAU;CAChC,OAAO;AACT;;AAGA,MAAa,gBAAgB,SAA0C,YAAY,IAAI,IAAI;;AAG3F,MAAa,gBAAgB,SAAwB;CACnD,MAAM,aAAa,YAAY,IAAI,IAAI;CACvC,IAAI,eAAe,KAAA,GAAW;CAC9B,YAAY,OAAO,IAAI;CACvB,WAAW,OAAO,KAAK,EAAE;CACzB,WAAW,OAAO;CAClB,WAAW,QAAQ,CAAC,CAAC,KAAK,OAAO;AACnC"}