@octane-xplat/lexical 0.0.1 → 0.10.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,346 @@
1
+ import { __s, bag10, bag3, child, clone, componentSlot, createScopedElement, createScopedValue, drainFrag, hookSlots, markChildrenBlock, setStyle, sibling, template, useEffect, useImperativeHandle, useRef, useState } from "octane";
2
+ import { bindSignalAttribute, bindSignalChild, createElementFromConfig, enableSignalBindings } from "octane/internal/client";
3
+ import { AutoFocusPlugin, ContentEditable, EditorRefPlugin, HistoryPlugin, LexicalComposer, LexicalErrorBoundary, LinkPlugin, ListPlugin, OnChangePlugin, RichTextPlugin } from "@octanejs/lexical";
4
+ import { $createParagraphNode, $createTextNode, $findMatchingParent, $getRoot, $getSelection, $isNodeSelection, $isRangeSelection, FORMAT_ELEMENT_COMMAND, FORMAT_TEXT_COMMAND, REDO_COMMAND, UNDO_COMMAND } from "lexical";
5
+ import { $createHeadingNode, $createQuoteNode, $isHeadingNode, $isQuoteNode, HeadingNode, QuoteNode } from "@lexical/rich-text";
6
+ import { $isListNode, INSERT_CHECK_LIST_COMMAND, INSERT_ORDERED_LIST_COMMAND, INSERT_UNORDERED_LIST_COMMAND, ListItemNode, ListNode } from "@lexical/list";
7
+ import { $createLinkNode, $isLinkNode, AutoLinkNode, LinkNode, TOGGLE_LINK_COMMAND } from "@lexical/link";
8
+ import { $createCodeNode, $isCodeNode, CodeNode } from "@lexical/code";
9
+ import { $isHorizontalRuleNode, HorizontalRuleNode, INSERT_HORIZONTAL_RULE_COMMAND } from "@lexical/extension";
10
+ import { $setBlocksType } from "@lexical/selection";
11
+ import { $generateHtmlFromNodes, $generateNodesFromDOM } from "@lexical/html";
12
+ //#region src/LexicalEditor.web.tsrx
13
+ enableSignalBindings(1, true);
14
+ var _t$0 = /* @__PURE__ */ template("<!><!><!><!><!><!><!>", 3, 1);
15
+ var _t$1 = /* @__PURE__ */ template("<div></div>");
16
+ var _hs$ = /* @__PURE__ */ hookSlots(11);
17
+ var __childrenBody = {};
18
+ var supported = true;
19
+ var NODES = [
20
+ HeadingNode,
21
+ QuoteNode,
22
+ ListNode,
23
+ ListItemNode,
24
+ LinkNode,
25
+ AutoLinkNode,
26
+ CodeNode,
27
+ HorizontalRuleNode
28
+ ];
29
+ var HEADING_TAG = {
30
+ heading1: "h1",
31
+ heading2: "h2",
32
+ heading3: "h3",
33
+ heading4: "h4",
34
+ heading5: "h5",
35
+ heading6: "h6"
36
+ };
37
+ var TEXT_FORMAT = {
38
+ bold: "bold",
39
+ italic: "italic",
40
+ underline: "underline",
41
+ strikethrough: "strikethrough",
42
+ code: "code",
43
+ highlight: "highlight",
44
+ subscript: "subscript",
45
+ superscript: "superscript"
46
+ };
47
+ var ELEMENT_FORMAT = {
48
+ alignLeft: "left",
49
+ alignCenter: "center",
50
+ alignRight: "right"
51
+ };
52
+ function setBlock(editor, is, create, match) {
53
+ editor.update(() => {
54
+ const sel = $getSelection();
55
+ if (!$isRangeSelection(sel)) return;
56
+ const block = $findMatchingParent(sel.anchor.getNode(), is);
57
+ const active = block != null && (match ? match(block) : true);
58
+ $setBlocksType(sel, () => active ? $createParagraphNode() : create());
59
+ });
60
+ }
61
+ function applyFormat(editor, format) {
62
+ const text = TEXT_FORMAT[format];
63
+ if (text) {
64
+ editor.dispatchCommand(FORMAT_TEXT_COMMAND, text);
65
+ return;
66
+ }
67
+ const el = ELEMENT_FORMAT[format];
68
+ if (el) {
69
+ editor.dispatchCommand(FORMAT_ELEMENT_COMMAND, el);
70
+ return;
71
+ }
72
+ const tag = HEADING_TAG[format];
73
+ if (tag) {
74
+ setBlock(editor, $isHeadingNode, () => $createHeadingNode(tag), (n) => n.getTag() === tag);
75
+ return;
76
+ }
77
+ switch (format) {
78
+ case "paragraph":
79
+ setBlock(editor, () => true, () => $createParagraphNode(), () => false);
80
+ break;
81
+ case "blockquote":
82
+ setBlock(editor, $isQuoteNode, () => $createQuoteNode());
83
+ break;
84
+ case "codeBlock":
85
+ setBlock(editor, $isCodeNode, () => $createCodeNode());
86
+ break;
87
+ case "bulletList":
88
+ editor.dispatchCommand(INSERT_UNORDERED_LIST_COMMAND, void 0);
89
+ break;
90
+ case "orderedList":
91
+ editor.dispatchCommand(INSERT_ORDERED_LIST_COMMAND, void 0);
92
+ break;
93
+ case "taskList":
94
+ editor.dispatchCommand(INSERT_CHECK_LIST_COMMAND, void 0);
95
+ break;
96
+ case "horizontalRule": editor.dispatchCommand(INSERT_HORIZONTAL_RULE_COMMAND, void 0);
97
+ }
98
+ }
99
+ function isActive(editor, format) {
100
+ return editor.read(() => {
101
+ const sel = $getSelection();
102
+ if (sel == null) return false;
103
+ const text = TEXT_FORMAT[format];
104
+ if (text) return $isRangeSelection(sel) && sel.hasFormat(text);
105
+ const el = ELEMENT_FORMAT[format];
106
+ if (el) return sel.getNodes().some((n) => {
107
+ const block = $findMatchingParent(n, (p) => p.getFormatType != null);
108
+ return block != null && block.getFormatType() === el;
109
+ });
110
+ const tag = HEADING_TAG[format];
111
+ if (tag) return sel.getNodes().some((n) => {
112
+ return $findMatchingParent(n, $isHeadingNode)?.getTag() === tag;
113
+ });
114
+ switch (format) {
115
+ case "blockquote": return sel.getNodes().some((n) => $findMatchingParent(n, $isQuoteNode) != null);
116
+ case "codeBlock": return sel.getNodes().some((n) => $findMatchingParent(n, $isCodeNode) != null);
117
+ case "bulletList":
118
+ case "orderedList":
119
+ case "taskList": {
120
+ const want = format === "bulletList" ? "bullet" : format === "orderedList" ? "number" : "check";
121
+ return sel.getNodes().some((n) => {
122
+ return $findMatchingParent(n, $isListNode)?.getListType() === want;
123
+ });
124
+ }
125
+ case "link": return sel.getNodes().some((n) => $isLinkNode(n) || $isLinkNode($findMatchingParent(n, $isLinkNode)));
126
+ case "horizontalRule": return $isNodeSelection(sel) && sel.getNodes().some($isHorizontalRuleNode);
127
+ case "paragraph": return sel.getNodes().some((n) => $findMatchingParent(n, (p) => p.getType?.() === "paragraph"));
128
+ default: return false;
129
+ }
130
+ });
131
+ }
132
+ function flatOffset(root, key, offset) {
133
+ const contains = (node) => node.getKey() === key || (node.getChildren?.() ?? []).some(contains);
134
+ let acc = 0;
135
+ const visit = (node) => {
136
+ if (node.getKey() === key) {
137
+ acc += offset;
138
+ return true;
139
+ }
140
+ for (const c of node.getChildren?.() ?? []) if (contains(c)) {
141
+ if (visit(c)) return true;
142
+ } else acc += c.getTextContentSize?.() ?? 0;
143
+ return false;
144
+ };
145
+ visit(root);
146
+ return acc;
147
+ }
148
+ function activeFormats(editor) {
149
+ const out = [];
150
+ for (const f of Object.keys(TEXT_FORMAT)) if (isActive(editor, f)) out.push(f);
151
+ for (const f of [
152
+ "paragraph",
153
+ "heading1",
154
+ "heading2",
155
+ "heading3",
156
+ "heading4",
157
+ "heading5",
158
+ "heading6",
159
+ "blockquote",
160
+ "bulletList",
161
+ "orderedList",
162
+ "taskList",
163
+ "codeBlock",
164
+ "link"
165
+ ]) if (isActive(editor, f)) out.push(f);
166
+ return out;
167
+ }
168
+ var LexicalEditor = /* @__PURE__ */ __s(function LexicalEditor(props, __s, __extra) {
169
+ __s.block;
170
+ const editorRef = useRef(null, _hs$ + 0);
171
+ const lastEmitted = useRef(void 0, _hs$ + 1);
172
+ const lastJSON = useRef(void 0, _hs$ + 2);
173
+ const lastHTMLInput = useRef(JSON.stringify([props.json !== void 0, props.value]), _hs$ + 3);
174
+ const lastJSONInput = useRef(JSON.stringify(props.json), _hs$ + 4);
175
+ const bound = useRef(void 0, _hs$ + 5);
176
+ const [exposedHandle, setHandle] = useState(null, _hs$ + 6);
177
+ useImperativeHandle(props.ref, () => exposedHandle, [exposedHandle], _hs$ + 7);
178
+ const initialConfig = {
179
+ namespace: "octane-xplat/lexical",
180
+ nodes: NODES,
181
+ editable: props.editable !== false,
182
+ editorState: props.json != null ? JSON.stringify(props.json) : props.value != null ? (editor) => {
183
+ const dom = new DOMParser().parseFromString(props.value, "text/html");
184
+ const nodes = $generateNodesFromDOM(editor, dom);
185
+ $getRoot().append(...nodes);
186
+ } : void 0,
187
+ onError: (e) => {
188
+ throw e;
189
+ }
190
+ };
191
+ const attach = (editor) => {
192
+ if (!editor || bound.current?.native === editor) return;
193
+ editorRef.current = editor;
194
+ bound.current = {
195
+ getHTML: () => editor.read(() => $generateHtmlFromNodes(editor)),
196
+ setHTML: (html) => editor.update(() => {
197
+ const dom = new DOMParser().parseFromString(html, "text/html");
198
+ const nodes = $generateNodesFromDOM(editor, dom);
199
+ $getRoot().clear().append(...nodes);
200
+ }),
201
+ getJSON: () => editor.getEditorState().toJSON(),
202
+ setJSON: (doc) => editor.setEditorState(editor.parseEditorState(JSON.stringify(doc))),
203
+ apply: (format) => {
204
+ editor.focus();
205
+ applyFormat(editor, format);
206
+ },
207
+ linkTo: (url, anchor) => {
208
+ if (anchor != null && anchor !== "") editor.update(() => {
209
+ const link = $createLinkNode(url);
210
+ link.append($createTextNode(anchor));
211
+ const sel = $getSelection();
212
+ if ($isRangeSelection(sel)) sel.insertNodes([link]);
213
+ });
214
+ else editor.dispatchCommand(TOGGLE_LINK_COMMAND, url);
215
+ },
216
+ removeLink: () => editor.dispatchCommand(TOGGLE_LINK_COMMAND, null),
217
+ isActive: (format) => isActive(editor, format),
218
+ undo: () => editor.dispatchCommand(UNDO_COMMAND, void 0),
219
+ redo: () => editor.dispatchCommand(REDO_COMMAND, void 0),
220
+ focus: () => editor.focus(),
221
+ blur: () => editor.blur(),
222
+ isFocused: () => {
223
+ const root = editor.getRootElement();
224
+ return root != null && root === document.activeElement;
225
+ },
226
+ native: editor
227
+ };
228
+ setHandle(bound.current);
229
+ props.onReady?.();
230
+ };
231
+ useEffect(() => {
232
+ if (!editorRef.current) return;
233
+ const stamp = JSON.stringify([props.json !== void 0, props.value]);
234
+ if (stamp === lastHTMLInput.current) return;
235
+ lastHTMLInput.current = stamp;
236
+ if (props.json !== void 0 || props.value === void 0 || props.value === lastEmitted.current) return;
237
+ bound.current?.setHTML(props.value);
238
+ }, [
239
+ exposedHandle,
240
+ props.value,
241
+ props.json
242
+ ], _hs$ + 8);
243
+ useEffect(() => {
244
+ if (!editorRef.current) return;
245
+ const stamp = JSON.stringify(props.json);
246
+ if (stamp === lastJSONInput.current) return;
247
+ lastJSONInput.current = stamp;
248
+ if (props.json === void 0 || stamp === lastJSON.current) return;
249
+ bound.current?.setJSON(props.json);
250
+ }, [exposedHandle, props.json], _hs$ + 9);
251
+ useEffect(() => {
252
+ editorRef.current?.setEditable(props.editable !== false);
253
+ }, [exposedHandle, props.editable], _hs$ + 10);
254
+ const emitChange = (state, editor) => {
255
+ const html = state.read(() => $generateHtmlFromNodes(editor));
256
+ lastEmitted.current = html;
257
+ props.onChange?.(html);
258
+ const doc = state.toJSON();
259
+ lastJSON.current = JSON.stringify(doc);
260
+ props.onJSONChange?.(doc);
261
+ state.read(() => {
262
+ const sel = $getSelection();
263
+ if ($isRangeSelection(sel) && props.onSelectionChange) {
264
+ const root = $getRoot();
265
+ props.onSelectionChange({
266
+ start: flatOffset(root, sel.anchor.key, sel.anchor.offset),
267
+ end: flatOffset(root, sel.focus.key, sel.focus.offset),
268
+ active: activeFormats(editor)
269
+ });
270
+ }
271
+ });
272
+ };
273
+ function __children$1(__props, __s, __extra) {
274
+ const __block = __s.block;
275
+ let _b = __s.slots[0];
276
+ if (_b === void 0) {
277
+ let _m0, _m1, _m2, _m3, _m4, _m5, _m8, _m9;
278
+ const _root = clone(_t$0);
279
+ _m0 = __block.parentNode;
280
+ const _el0 = child(_root);
281
+ _m1 = _el0;
282
+ const _el1 = sibling(_el0, 1);
283
+ _m2 = _el1;
284
+ const _el2 = sibling(_el1, 1);
285
+ _m3 = _el2;
286
+ const _el3 = sibling(_el2, 1);
287
+ _m4 = _el3;
288
+ const _el4 = sibling(_el3, 1);
289
+ _m5 = _el4;
290
+ const _el5 = sibling(_el4, 1);
291
+ _m8 = _el5;
292
+ _m9 = sibling(_el5, 1);
293
+ drainFrag(_root, __block.parentNode, __block.endMarker);
294
+ _b = bag10(__s, null, _m0, _m1, _m2, _m3, _m4, _m5, null, void 0, _m8, _m9);
295
+ }
296
+ componentSlot(__s, 1, _b.a, RichTextPlugin, {
297
+ "contentEditable": createScopedValue(() => createElementFromConfig("c:7597daf3", ContentEditable, {
298
+ id: props.id,
299
+ "aria-placeholder": props.placeholder ?? "",
300
+ placeholder: null,
301
+ className: "vx-lexical-input",
302
+ style: { outline: "none" }
303
+ })),
304
+ "placeholder": props.placeholder ? createScopedValue(() => createScopedElement("div", {
305
+ className: "vx-lexical-placeholder",
306
+ style: {
307
+ position: "absolute",
308
+ inset: "8px auto auto 10px",
309
+ pointerEvents: "none",
310
+ opacity: .5
311
+ }
312
+ }, () => props.placeholder)) : null,
313
+ "ErrorBoundary": LexicalErrorBoundary
314
+ }, _b.b, void 0, 2, void 0, void 0, "c:1283dc87");
315
+ componentSlot(__s, 2, _b.a, HistoryPlugin, {}, _b.c, void 0, 2, void 0, void 0, "c:1dd1c554");
316
+ componentSlot(__s, 3, _b.a, ListPlugin, {}, _b.d, void 0, 2, void 0, void 0, "c:4bb68e0f");
317
+ componentSlot(__s, 4, _b.a, LinkPlugin, {}, _b.e, void 0, 2, void 0, void 0, "c:5c76533c");
318
+ _b.k = bindSignalChild(__s, _b.k, 5, _b.a, props.autofocus ? createElementFromConfig("c:980e387e", AutoFocusPlugin, {}) : null, "b:928b159c", _b.f, true, void 0, void 0, void 0);
319
+ componentSlot(__s, 6, _b.a, OnChangePlugin, { "onChange": emitChange }, _b.i, void 0, 2, void 0, void 0, "c:079f6eaf");
320
+ componentSlot(__s, 7, _b.a, EditorRefPlugin, { "editorRef": attach }, _b.j, void 0, 2, void 0, void 0, "c:230323aa");
321
+ }
322
+ let _b = __s.slots[0];
323
+ if (_b === void 0) {
324
+ let _m1;
325
+ const _root = clone(_t$1);
326
+ _m1 = _root;
327
+ _b = bag3(__s, _root, void 0, _m1, void 0);
328
+ }
329
+ _b.a = bindSignalAttribute(__s, _b.a, _b.b, "class", props.className, "b:db7e728b", "class");
330
+ {
331
+ const _v = {
332
+ position: "relative",
333
+ ...props.style
334
+ };
335
+ if (_b.c !== _v) {
336
+ setStyle(_b.b, _v, _b.c);
337
+ _b.c = _v;
338
+ }
339
+ }
340
+ componentSlot(__s, 1, _b.b, LexicalComposer, {
341
+ "initialConfig": initialConfig,
342
+ "children": markChildrenBlock(__children$1, __childrenBody)
343
+ }, void 0, void 0, void 0, void 0, void 0, "c:9d1ef90e");
344
+ });
345
+ //#endregion
346
+ export { LexicalEditor, supported };
@@ -0,0 +1,8 @@
1
+ {
2
+ "version": 1,
3
+ "buildId": "41f913ce-4dc3-4a50-9909-0d4d1b93f3ed",
4
+ "mode": "production",
5
+ "capabilities": {
6
+ "independentHydration": false
7
+ }
8
+ }
package/package.json CHANGED
@@ -1,11 +1,97 @@
1
1
  {
2
2
  "name": "@octane-xplat/lexical",
3
- "version": "0.0.1",
4
- "description": "Placeholder stub for @octane-xplat/lexical — no supported API yet.",
3
+ "version": "0.10.0",
4
+ "description": "Unified rich-text editor facade for Octane xplat — live lexical on web/AppKit, Aztec + lexical document model on Android",
5
+ "keywords": [
6
+ "cross-platform",
7
+ "editor",
8
+ "lexical",
9
+ "octane",
10
+ "rich-text",
11
+ "richtext",
12
+ "xplat"
13
+ ],
14
+ "homepage": "https://github.com/octane-xplat/octane-xplat/tree/main/packages/lexical",
5
15
  "license": "MIT",
6
16
  "repository": {
7
17
  "type": "git",
8
18
  "url": "https://github.com/octane-xplat/octane-xplat",
9
19
  "directory": "packages/lexical"
20
+ },
21
+ "files": [
22
+ "dist",
23
+ "types",
24
+ "src",
25
+ "platforms"
26
+ ],
27
+ "type": "module",
28
+ "exports": {
29
+ ".": {
30
+ "macos": {
31
+ "types": "./types/index.macos.d.ts",
32
+ "default": "./src/index.macos.ts"
33
+ },
34
+ "web": {
35
+ "types": "./types/index.d.ts",
36
+ "default": "./dist/web/index.js"
37
+ },
38
+ "native": {
39
+ "types": "./types/index.d.ts",
40
+ "default": "./dist/native/index.js"
41
+ },
42
+ "types": "./types/index.d.ts",
43
+ "default": "./dist/native/index.js"
44
+ }
45
+ },
46
+ "publishConfig": {
47
+ "access": "public"
48
+ },
49
+ "dependencies": {
50
+ "@lexical/code": "0.51.0",
51
+ "@lexical/extension": "0.51.0",
52
+ "@lexical/html": "0.51.0",
53
+ "@lexical/link": "0.51.0",
54
+ "@lexical/list": "0.51.0",
55
+ "@lexical/rich-text": "0.51.0",
56
+ "@lexical/selection": "0.51.0",
57
+ "@lexical/utils": "0.51.0",
58
+ "@octane-xplat/richtext": "0.10.0",
59
+ "@octanejs/lexical": "0.2.0",
60
+ "lexical": "0.51.0",
61
+ "zeed-dom": "0.18.0"
62
+ },
63
+ "devDependencies": {
64
+ "@nativescript-community/octane": "0.2.4",
65
+ "@nativescript/core": "9.1.2",
66
+ "@octane-xplat/macos-renderer": "0.10.0",
67
+ "@octanejs/vite-plugin": "0.1.61",
68
+ "octane": "0.6.3",
69
+ "typescript": "5.9.3",
70
+ "vite": "8.3.0"
71
+ },
72
+ "peerDependencies": {
73
+ "@nativescript-community/octane": ">=0.2.1 <1",
74
+ "@nativescript/core": ">=9.1.0 <10",
75
+ "octane": ">=0.6.3 <1"
76
+ },
77
+ "peerDependenciesMeta": {
78
+ "@nativescript-community/octane": {
79
+ "optional": true
80
+ },
81
+ "@nativescript/core": {
82
+ "optional": true
83
+ },
84
+ "octane": {
85
+ "optional": true
86
+ }
87
+ },
88
+ "nativescript": {
89
+ "platforms": {
90
+ "android": "9.1.0",
91
+ "ios": "9.1.0"
92
+ }
93
+ },
94
+ "scripts": {
95
+ "build": "vite build && vite build --mode native && tsc -p tsconfig.types.json && node ../../scripts/check-native-dist.mjs && node ../richtext/build-browser.mjs"
10
96
  }
11
- }
97
+ }
@@ -0,0 +1,9 @@
1
+ /** @jsxImportSource @octane-xplat/macos-renderer */
2
+ import type { LexicalEditorProps } from './types'
3
+ import browser from '../dist/macos/browser.js'
4
+ import { NativeEditor } from '@octane-xplat/richtext/macos-host'
5
+
6
+ /** AppKit mounts the local editor engine in WKWebView. */
7
+ export function LexicalEditor(props: LexicalEditorProps) {
8
+ return <NativeEditor {...props} browser={browser} />
9
+ }
@@ -0,0 +1,111 @@
1
+ /** @jsxImportSource @nativescript-community/octane */
2
+ import { useState, useImperativeHandle, useCallback, useEffect, useRef } from 'octane'
3
+ import { RichTextEditor, supported as leafSupported } from '@octane-xplat/richtext'
4
+ import type { RichTextEditorHandle } from '@octane-xplat/richtext'
5
+ import type { LexicalEditorHandle, LexicalEditorProps } from './types'
6
+ import { ensureJSONBridge, htmlToJSON, jsonBridgeReady, jsonToHTML } from './json-bridge'
7
+
8
+ export const supported = leafSupported
9
+
10
+ /** Unified editor facade, native half. Rendering/editing run through the
11
+ * Aztec leaf; the lexical document model (headless createEditor +
12
+ * @lexical/html over zeed-dom) handles serialized-state interchange on top
13
+ * of it. This is a read/write model, not a live editor — no
14
+ * dispatchCommand, node transforms, or plugin behavior on native. */
15
+ export function LexicalEditor(props: LexicalEditorProps) @{
16
+ const inner = useRef<RichTextEditorHandle | null>(null)
17
+ const [exposedHandle, setHandle] = useState<LexicalEditorHandle | null>(null)
18
+ useImperativeHandle<LexicalEditorHandle | null>(props.ref, () => exposedHandle, [exposedHandle])
19
+ const jsonReady = useRef(false)
20
+ const lastJSON = useRef<string | undefined>(undefined)
21
+
22
+ const bindHost = useCallback((h: RichTextEditorHandle | null) => {
23
+ inner.current = h
24
+
25
+ setHandle((previous) => previous?.native === h?.native ? previous : h ? ({
26
+ getHTML: () => h.getHTML(),
27
+ setHTML: (html: string) => h.setHTML(html),
28
+ getJSON: () => (jsonBridgeReady() ? htmlToJSON(h.getHTML()) : null),
29
+ setJSON: (doc) => {
30
+ const html = jsonBridgeReady() ? jsonToHTML(doc) : null
31
+ if (html != null) {
32
+ lastJSON.current = JSON.stringify(doc)
33
+ h.setHTML(html)
34
+ }
35
+ },
36
+ apply: (format) => h.apply(format),
37
+ linkTo: (url, anchor) => h.linkTo(url, anchor),
38
+ removeLink: () => h.removeLink(),
39
+ isActive: (format) => h.isActive(format),
40
+ undo: () => h.undo(),
41
+ redo: () => h.redo(),
42
+ focus: () => h.focus(),
43
+ blur: () => h.blur(),
44
+ isFocused: () => h.isFocused(),
45
+ native: h.native,
46
+ } satisfies LexicalEditorHandle) : null)
47
+ }, [])
48
+
49
+ // The bridge loads once per process — dynamic imports of the DOM-free
50
+ // lexical slices, plus the zeed-dom parse shim.
51
+ useEffect(() => {
52
+ ensureJSONBridge().then((ok) => {
53
+ jsonReady.current = ok
54
+ props.onJSONReady?.(ok)
55
+ })
56
+ }, [])
57
+
58
+ // `json` prop → editor content, when the bridge is up and the doc wasn't
59
+ // emitted by the editor itself.
60
+ useEffect(() => {
61
+ if (!props.json || !jsonBridgeReady() || !inner.current) return
62
+ const stamp = JSON.stringify(props.json)
63
+ if (stamp === lastJSON.current) return
64
+ const html = jsonToHTML(props.json)
65
+ if (html != null) {
66
+ lastJSON.current = stamp
67
+ inner.current.setHTML(html)
68
+ }
69
+ }, [props.json])
70
+
71
+ const emitJSON = () => {
72
+ if (!inner.current || !jsonBridgeReady()) return
73
+ const doc = htmlToJSON(inner.current.getHTML())
74
+ if (doc) {
75
+ lastJSON.current = JSON.stringify(doc)
76
+ props.onJSONChange?.(doc)
77
+ }
78
+ }
79
+
80
+ <RichTextEditor
81
+ id={props.id}
82
+ className={props.className}
83
+ style={props.style}
84
+ value={props.value}
85
+ placeholder={props.placeholder}
86
+ editable={props.editable}
87
+ autofocus={props.autofocus}
88
+ onReady={props.onReady}
89
+ onChange={(html) => {
90
+ props.onChange?.(html)
91
+ emitJSON()
92
+ }}
93
+ onSelectionChange={props.onSelectionChange}
94
+ onFocus={props.onFocus}
95
+ onBlur={props.onBlur}
96
+ ref={bindHost}
97
+ android={props.android}
98
+ ios={props.ios}
99
+ row={props.row}
100
+ col={props.col}
101
+ rowSpan={props.rowSpan}
102
+ colSpan={props.colSpan}
103
+ dock={props.dock}
104
+ left={props.left}
105
+ top={props.top}
106
+ flexGrow={props.flexGrow}
107
+ flexShrink={props.flexShrink}
108
+ alignSelf={props.alignSelf}
109
+ order={props.order}
110
+ />;
111
+ }