@octane-xplat/lexical 0.0.1 → 0.11.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 +171 -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 +228 -0
- package/dist/native/octane-client-build.json +8 -0
- package/dist/web/index.js +12 -0
- package/dist/web/index2.js +349 -0
- package/dist/web/octane-client-build.json +8 -0
- package/package.json +91 -3
- package/src/Editor.macos.tsx +9 -0
- package/src/LexicalEditor.mobile.test.ts +229 -0
- package/src/LexicalEditor.tsrx +143 -0
- package/src/LexicalEditor.web.test.tsrx +111 -0
- package/src/LexicalEditor.web.tsrx +413 -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 +131 -0
- package/types/index.d.ts +8 -0
- package/types/index.macos.d.ts +7 -0
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
import { Fragment, __s, bag10, bag3, child, clone, componentSlot, createScopedElement, createScopedValue, drainFrag, hookSlots, markChildrenBlock, positionalChildren, 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: props.web?.replaceNodes ? props.web.nodes ?? [] : [.../* @__PURE__ */ new Set([...NODES, ...props.web?.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.onJSONReady?.(true);
|
|
230
|
+
props.onReady?.();
|
|
231
|
+
};
|
|
232
|
+
useEffect(() => {
|
|
233
|
+
if (!editorRef.current) return;
|
|
234
|
+
const stamp = JSON.stringify([props.json !== void 0, props.value]);
|
|
235
|
+
if (stamp === lastHTMLInput.current) return;
|
|
236
|
+
lastHTMLInput.current = stamp;
|
|
237
|
+
if (props.json !== void 0 || props.value === void 0 || props.value === lastEmitted.current) return;
|
|
238
|
+
bound.current?.setHTML(props.value);
|
|
239
|
+
}, [
|
|
240
|
+
exposedHandle,
|
|
241
|
+
props.value,
|
|
242
|
+
props.json
|
|
243
|
+
], _hs$ + 8);
|
|
244
|
+
useEffect(() => {
|
|
245
|
+
if (!editorRef.current) return;
|
|
246
|
+
const stamp = JSON.stringify(props.json);
|
|
247
|
+
if (stamp === lastJSONInput.current) return;
|
|
248
|
+
lastJSONInput.current = stamp;
|
|
249
|
+
if (props.json === void 0 || stamp === lastJSON.current) return;
|
|
250
|
+
bound.current?.setJSON(props.json);
|
|
251
|
+
}, [exposedHandle, props.json], _hs$ + 9);
|
|
252
|
+
useEffect(() => {
|
|
253
|
+
editorRef.current?.setEditable(props.editable !== false);
|
|
254
|
+
}, [exposedHandle, props.editable], _hs$ + 10);
|
|
255
|
+
const emitChange = (state, editor) => {
|
|
256
|
+
const html = state.read(() => $generateHtmlFromNodes(editor));
|
|
257
|
+
lastEmitted.current = html;
|
|
258
|
+
props.onChange?.(html);
|
|
259
|
+
const doc = state.toJSON();
|
|
260
|
+
lastJSON.current = JSON.stringify(doc);
|
|
261
|
+
props.onJSONChange?.(doc);
|
|
262
|
+
state.read(() => {
|
|
263
|
+
const sel = $getSelection();
|
|
264
|
+
if ($isRangeSelection(sel) && props.onSelectionChange) {
|
|
265
|
+
const root = $getRoot();
|
|
266
|
+
props.onSelectionChange({
|
|
267
|
+
start: flatOffset(root, sel.anchor.key, sel.anchor.offset),
|
|
268
|
+
end: flatOffset(root, sel.focus.key, sel.focus.offset),
|
|
269
|
+
active: activeFormats(editor)
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
});
|
|
273
|
+
};
|
|
274
|
+
if (__s.block.pending && !__s.block.crossRenderUpdate) return;
|
|
275
|
+
function __children$1(__props, __s, __extra) {
|
|
276
|
+
const __block = __s.block;
|
|
277
|
+
let _b = __s.slots[0];
|
|
278
|
+
if (_b === void 0) {
|
|
279
|
+
let _m0, _m1, _m2, _m5, _m6, _m7;
|
|
280
|
+
const _root = clone(_t$0);
|
|
281
|
+
_m0 = __block.parentNode;
|
|
282
|
+
const _el0 = child(_root);
|
|
283
|
+
_m1 = _el0;
|
|
284
|
+
const _el1 = sibling(_el0, 1);
|
|
285
|
+
_m2 = _el1;
|
|
286
|
+
const _el2 = sibling(_el1, 1);
|
|
287
|
+
_m5 = _el2;
|
|
288
|
+
const _el3 = sibling(_el2, 1);
|
|
289
|
+
_m6 = _el3;
|
|
290
|
+
_m7 = sibling(_el3, 1);
|
|
291
|
+
drainFrag(_root, __block.parentNode, __block.endMarker);
|
|
292
|
+
_b = bag10(__s, null, _m0, _m1, _m2, null, void 0, _m5, _m6, _m7, null, void 0);
|
|
293
|
+
}
|
|
294
|
+
componentSlot(__s, 1, _b.a, RichTextPlugin, {
|
|
295
|
+
"contentEditable": createScopedValue(() => createElementFromConfig("c:8bd4dc5e", ContentEditable, {
|
|
296
|
+
id: props.id,
|
|
297
|
+
"aria-placeholder": props.placeholder ?? "",
|
|
298
|
+
placeholder: null,
|
|
299
|
+
className: "vx-lexical-input",
|
|
300
|
+
style: { outline: "none" },
|
|
301
|
+
onFocus: props.onFocus,
|
|
302
|
+
onBlur: props.onBlur
|
|
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
|
+
_b.k = bindSignalChild(__s, _b.k, 2, _b.a, !props.web?.replacePlugins ? createScopedElement(Fragment, {}, () => positionalChildren([
|
|
316
|
+
createElementFromConfig("c:2ac25b31", HistoryPlugin, {}),
|
|
317
|
+
createElementFromConfig("c:8c987b0a", ListPlugin, {}),
|
|
318
|
+
createElementFromConfig("c:c85acd4c", LinkPlugin, {}),
|
|
319
|
+
props.autofocus ? createElementFromConfig("c:4e4f8e9d", AutoFocusPlugin, {}) : null
|
|
320
|
+
])) : null, "b:d23e6e54", _b.c, true, void 0, void 0, void 0);
|
|
321
|
+
componentSlot(__s, 3, _b.a, OnChangePlugin, { "onChange": emitChange }, _b.f, void 0, 2, void 0, void 0, "c:fbe62b73");
|
|
322
|
+
componentSlot(__s, 4, _b.a, EditorRefPlugin, { "editorRef": attach }, _b.g, void 0, 2, void 0, void 0, "c:bcb623dd");
|
|
323
|
+
_b.l = bindSignalChild(__s, _b.l, 5, _b.a, props.web?.plugins, "b:6078ff9a", _b.h, true, void 0, void 0, void 0);
|
|
324
|
+
}
|
|
325
|
+
let _b = __s.slots[0];
|
|
326
|
+
if (_b === void 0) {
|
|
327
|
+
let _m1;
|
|
328
|
+
const _root = clone(_t$1);
|
|
329
|
+
_m1 = _root;
|
|
330
|
+
_b = bag3(__s, _root, void 0, _m1, void 0);
|
|
331
|
+
}
|
|
332
|
+
_b.a = bindSignalAttribute(__s, _b.a, _b.b, "class", props.className, "b:748f4137", "class");
|
|
333
|
+
{
|
|
334
|
+
const _v = {
|
|
335
|
+
position: "relative",
|
|
336
|
+
...props.style
|
|
337
|
+
};
|
|
338
|
+
if (_b.c !== _v) {
|
|
339
|
+
setStyle(_b.b, _v, _b.c);
|
|
340
|
+
_b.c = _v;
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
componentSlot(__s, 1, _b.b, LexicalComposer, {
|
|
344
|
+
"initialConfig": initialConfig,
|
|
345
|
+
"children": markChildrenBlock(__children$1, __childrenBody)
|
|
346
|
+
}, void 0, void 0, void 0, void 0, void 0, "c:9d1ef90e");
|
|
347
|
+
});
|
|
348
|
+
//#endregion
|
|
349
|
+
export { LexicalEditor, supported };
|
package/package.json
CHANGED
|
@@ -1,11 +1,99 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@octane-xplat/lexical",
|
|
3
|
-
"version": "0.0
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.11.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.11.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.11.0",
|
|
67
|
+
"@octanejs/vite-plugin": "0.1.61",
|
|
68
|
+
"octane": "0.6.3",
|
|
69
|
+
"typescript": "5.9.3",
|
|
70
|
+
"vite": "8.3.0",
|
|
71
|
+
"vitest": "5.0.1"
|
|
72
|
+
},
|
|
73
|
+
"peerDependencies": {
|
|
74
|
+
"@nativescript-community/octane": ">=0.2.1 <1",
|
|
75
|
+
"@nativescript/core": ">=9.1.0 <10",
|
|
76
|
+
"octane": ">=0.6.3 <1"
|
|
77
|
+
},
|
|
78
|
+
"peerDependenciesMeta": {
|
|
79
|
+
"@nativescript-community/octane": {
|
|
80
|
+
"optional": true
|
|
81
|
+
},
|
|
82
|
+
"@nativescript/core": {
|
|
83
|
+
"optional": true
|
|
84
|
+
},
|
|
85
|
+
"octane": {
|
|
86
|
+
"optional": true
|
|
87
|
+
}
|
|
88
|
+
},
|
|
89
|
+
"nativescript": {
|
|
90
|
+
"platforms": {
|
|
91
|
+
"android": "9.1.0",
|
|
92
|
+
"ios": "9.1.0"
|
|
93
|
+
}
|
|
94
|
+
},
|
|
95
|
+
"scripts": {
|
|
96
|
+
"build": "vite build && vite build --mode native && tsc -p tsconfig.types.json && node ../../scripts/check-native-dist.mjs && node ../richtext/build-browser.mjs",
|
|
97
|
+
"test:native": "vitest run --config vitest.native.config.mts"
|
|
10
98
|
}
|
|
11
|
-
}
|
|
99
|
+
}
|
|
@@ -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,229 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
2
|
+
import {
|
|
3
|
+
createObjectContainer,
|
|
4
|
+
createObjectDriver,
|
|
5
|
+
createUniversalRoot,
|
|
6
|
+
flushUniversalSync,
|
|
7
|
+
} from 'octane/universal/native'
|
|
8
|
+
|
|
9
|
+
import type { LexicalJSON } from './types'
|
|
10
|
+
|
|
11
|
+
// Controlled fake for the lazy JSON bridge — the real module's dynamic
|
|
12
|
+
// imports aren't needed to test facade timing. `resolved`/`ready` model the
|
|
13
|
+
// bridge settling; docs carry `html` so conversions stay deterministic.
|
|
14
|
+
const bridge = vi.hoisted(() => ({
|
|
15
|
+
ready: false,
|
|
16
|
+
resolved: undefined as boolean | undefined,
|
|
17
|
+
waiters: [] as Array<(ok: boolean) => void>,
|
|
18
|
+
}))
|
|
19
|
+
|
|
20
|
+
vi.mock('./json-bridge', () => ({
|
|
21
|
+
ensureJSONBridge: () =>
|
|
22
|
+
bridge.resolved !== undefined
|
|
23
|
+
? Promise.resolve(bridge.resolved)
|
|
24
|
+
: new Promise<boolean>((resolve) => bridge.waiters.push(resolve)),
|
|
25
|
+
jsonBridgeReady: () => bridge.ready,
|
|
26
|
+
jsonToHTML: (doc: any) =>
|
|
27
|
+
doc && typeof doc === 'object' && typeof doc.html === 'string' ? doc.html : null,
|
|
28
|
+
htmlToJSON: (html: string) => ({ type: 'doc', html }),
|
|
29
|
+
}))
|
|
30
|
+
|
|
31
|
+
vi.mock('@octane-xplat/richtext', async () => {
|
|
32
|
+
return await import('../tests/fake-richtext.fixture.mobile.tsrx')
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
import { LexicalEditor } from './LexicalEditor.tsrx'
|
|
36
|
+
import { hosts, resetFakeRichtext, setDefer } from '../tests/fake-richtext.fixture.mobile.tsrx'
|
|
37
|
+
|
|
38
|
+
const docA = { type: 'doc', html: '<p>alpha</p>' } as unknown as LexicalJSON
|
|
39
|
+
const docB = { type: 'doc', html: '<p>bravo</p>' } as unknown as LexicalJSON
|
|
40
|
+
|
|
41
|
+
const roots: ReturnType<typeof createUniversalRoot>[] = []
|
|
42
|
+
|
|
43
|
+
function mount(props: any) {
|
|
44
|
+
const container = createObjectContainer('nativescript')
|
|
45
|
+
const root = createUniversalRoot(container, createObjectDriver('nativescript'))
|
|
46
|
+
roots.push(root)
|
|
47
|
+
let handle: any = null
|
|
48
|
+
root.render(LexicalEditor, {
|
|
49
|
+
...props,
|
|
50
|
+
ref: (next: any) => {
|
|
51
|
+
handle = next
|
|
52
|
+
},
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
return {
|
|
56
|
+
root,
|
|
57
|
+
container,
|
|
58
|
+
get handle() {
|
|
59
|
+
return handle
|
|
60
|
+
},
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function settleBridge(ok: boolean) {
|
|
65
|
+
bridge.ready = ok
|
|
66
|
+
bridge.resolved = ok
|
|
67
|
+
for (const resolve of bridge.waiters.splice(0)) {
|
|
68
|
+
resolve(ok)
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async function settle() {
|
|
73
|
+
for (let i = 0; i < 12; i++) {
|
|
74
|
+
await Promise.resolve()
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
flushUniversalSync(() => {})
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
beforeEach(() => {
|
|
81
|
+
bridge.ready = false
|
|
82
|
+
bridge.resolved = undefined
|
|
83
|
+
bridge.waiters.length = 0
|
|
84
|
+
resetFakeRichtext()
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
afterEach(() => {
|
|
88
|
+
for (const root of roots.splice(0)) {
|
|
89
|
+
root.unmount()
|
|
90
|
+
}
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
describe('LexicalEditor native JSON readiness', () => {
|
|
94
|
+
it('applies an initial props.json once when the bridge settles after mount', async () => {
|
|
95
|
+
const onJSONReady = vi.fn()
|
|
96
|
+
mount({ json: docA, onJSONReady })
|
|
97
|
+
await settle()
|
|
98
|
+
expect(hosts[0].setCalls).toEqual([])
|
|
99
|
+
|
|
100
|
+
settleBridge(true)
|
|
101
|
+
await settle()
|
|
102
|
+
expect(onJSONReady).toHaveBeenCalledWith(true)
|
|
103
|
+
expect(hosts[0].html).toBe('<p>alpha</p>')
|
|
104
|
+
expect(hosts[0].setCalls).toEqual(['<p>alpha</p>'])
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
it('applies an initial props.json when the host readies after the bridge', async () => {
|
|
108
|
+
bridge.ready = true
|
|
109
|
+
bridge.resolved = true
|
|
110
|
+
setDefer(true)
|
|
111
|
+
mount({ json: docA })
|
|
112
|
+
await settle()
|
|
113
|
+
expect(hosts[0].setCalls).toEqual([])
|
|
114
|
+
|
|
115
|
+
hosts[0].ready()
|
|
116
|
+
await settle()
|
|
117
|
+
expect(hosts[0].html).toBe('<p>alpha</p>')
|
|
118
|
+
expect(hosts[0].setCalls).toEqual(['<p>alpha</p>'])
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
it('applies an initial props.json once when host and bridge are both late', async () => {
|
|
122
|
+
setDefer(true)
|
|
123
|
+
mount({ json: docA })
|
|
124
|
+
await settle()
|
|
125
|
+
hosts[0].ready()
|
|
126
|
+
await settle()
|
|
127
|
+
expect(hosts[0].setCalls).toEqual([])
|
|
128
|
+
|
|
129
|
+
settleBridge(true)
|
|
130
|
+
await settle()
|
|
131
|
+
expect(hosts[0].setCalls).toEqual(['<p>alpha</p>'])
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
it('keeps json precedence over value once readiness lands', async () => {
|
|
135
|
+
mount({ value: '<p>value</p>', json: docA })
|
|
136
|
+
await settle()
|
|
137
|
+
settleBridge(true)
|
|
138
|
+
await settle()
|
|
139
|
+
expect(hosts[0].html).toBe('<p>alpha</p>')
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
it('applies later props.json updates and skips identical docs', async () => {
|
|
143
|
+
const view = mount({ json: docA })
|
|
144
|
+
settleBridge(true)
|
|
145
|
+
await settle()
|
|
146
|
+
expect(hosts[0].setCalls).toEqual(['<p>alpha</p>'])
|
|
147
|
+
|
|
148
|
+
view.root.render(LexicalEditor, { json: docB } as any)
|
|
149
|
+
await settle()
|
|
150
|
+
expect(hosts[0].setCalls).toEqual(['<p>alpha</p>', '<p>bravo</p>'])
|
|
151
|
+
|
|
152
|
+
view.root.render(LexicalEditor, { json: docB } as any)
|
|
153
|
+
await settle()
|
|
154
|
+
expect(hosts[0].setCalls).toEqual(['<p>alpha</p>', '<p>bravo</p>'])
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
it('does not push a doc the editor itself emitted back at it', async () => {
|
|
158
|
+
const onJSONChange = vi.fn()
|
|
159
|
+
const view = mount({ onJSONChange })
|
|
160
|
+
settleBridge(true)
|
|
161
|
+
await settle()
|
|
162
|
+
|
|
163
|
+
const rig = hosts[0]
|
|
164
|
+
rig.html = '<p>typed</p>'
|
|
165
|
+
// The leaf calls props.onChange(html); the facade wraps it to emit JSON.
|
|
166
|
+
rig.props.onChange('<p>typed</p>')
|
|
167
|
+
await settle()
|
|
168
|
+
const emitted = onJSONChange.mock.calls[0][0]
|
|
169
|
+
expect(emitted).toEqual({ type: 'doc', html: '<p>typed</p>' })
|
|
170
|
+
|
|
171
|
+
view.root.render(LexicalEditor, { json: emitted, onJSONChange } as any)
|
|
172
|
+
await settle()
|
|
173
|
+
expect(rig.setCalls).toEqual([])
|
|
174
|
+
})
|
|
175
|
+
|
|
176
|
+
it('defers an imperative setJSON issued before the bridge settles', async () => {
|
|
177
|
+
const view = mount({})
|
|
178
|
+
await settle()
|
|
179
|
+
expect(view.handle).not.toBeNull()
|
|
180
|
+
|
|
181
|
+
view.handle.setJSON(docB)
|
|
182
|
+
expect(hosts[0].setCalls).toEqual([])
|
|
183
|
+
settleBridge(true)
|
|
184
|
+
await settle()
|
|
185
|
+
expect(hosts[0].setCalls).toEqual(['<p>bravo</p>'])
|
|
186
|
+
})
|
|
187
|
+
|
|
188
|
+
it('re-applies the current doc when the host rebinds', async () => {
|
|
189
|
+
mount({ json: docA })
|
|
190
|
+
settleBridge(true)
|
|
191
|
+
await settle()
|
|
192
|
+
expect(hosts[0].setCalls).toEqual(['<p>alpha</p>'])
|
|
193
|
+
|
|
194
|
+
hosts[0].rebind()
|
|
195
|
+
await settle()
|
|
196
|
+
expect(hosts[0].html).toBe('<p>alpha</p>')
|
|
197
|
+
expect(hosts[0].setCalls).toEqual(['<p>alpha</p>'])
|
|
198
|
+
})
|
|
199
|
+
|
|
200
|
+
it('ignores a bridge that settles after unmount', async () => {
|
|
201
|
+
const onJSONReady = vi.fn()
|
|
202
|
+
const view = mount({ json: docA, onJSONReady })
|
|
203
|
+
await settle()
|
|
204
|
+
view.root.unmount()
|
|
205
|
+
|
|
206
|
+
settleBridge(true)
|
|
207
|
+
await settle()
|
|
208
|
+
expect(onJSONReady).not.toHaveBeenCalled()
|
|
209
|
+
expect(hosts[0].setCalls).toEqual([])
|
|
210
|
+
})
|
|
211
|
+
|
|
212
|
+
it('reports bridge failure through onJSONReady without touching the host', async () => {
|
|
213
|
+
const onJSONReady = vi.fn()
|
|
214
|
+
const view = mount({ json: docA, onJSONReady })
|
|
215
|
+
await settle()
|
|
216
|
+
settleBridge(false)
|
|
217
|
+
await settle()
|
|
218
|
+
expect(onJSONReady).toHaveBeenCalledWith(false)
|
|
219
|
+
expect(hosts[0].setCalls).toEqual([])
|
|
220
|
+
expect(view.handle.getJSON()).toBeNull()
|
|
221
|
+
})
|
|
222
|
+
|
|
223
|
+
it('drops a doc the converter rejects instead of writing null HTML', async () => {
|
|
224
|
+
mount({ json: { type: 'doc', content: [{ type: 'mystery' }] } as any })
|
|
225
|
+
settleBridge(true)
|
|
226
|
+
await settle()
|
|
227
|
+
expect(hosts[0].setCalls).toEqual([])
|
|
228
|
+
})
|
|
229
|
+
})
|