@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.
- package/LICENSE +21 -0
- package/README.md +85 -3
- package/dist/macos/browser.d.ts +1 -0
- package/dist/macos/browser.js +2 -0
- package/dist/native/index.js +3 -0
- package/dist/native/index2.js +232 -0
- package/dist/native/index3.js +211 -0
- package/dist/native/octane-client-build.json +8 -0
- package/dist/web/index.js +12 -0
- package/dist/web/index2.js +346 -0
- package/dist/web/octane-client-build.json +8 -0
- package/package.json +89 -3
- package/src/Editor.macos.tsx +9 -0
- package/src/LexicalEditor.tsrx +111 -0
- package/src/LexicalEditor.web.tsrx +406 -0
- package/src/browser-entry.web.tsx +5 -0
- package/src/index.macos.ts +13 -0
- package/src/index.ts +3 -0
- package/src/index.web.ts +12 -0
- package/src/json-bridge.ts +335 -0
- package/src/types.ts +105 -0
- package/types/index.d.ts +8 -0
- package/types/index.macos.d.ts +7 -0
|
@@ -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 };
|
package/package.json
CHANGED
|
@@ -1,11 +1,97 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@octane-xplat/lexical",
|
|
3
|
-
"version": "0.0
|
|
4
|
-
"description": "
|
|
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
|
+
}
|