@apollohg/react-native-prose-editor 0.1.1 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +12 -7
- package/android/build.gradle +7 -2
- package/android/src/main/java/com/apollohg/editor/EditorEditText.kt +289 -2
- package/android/src/main/java/com/apollohg/editor/EditorTheme.kt +51 -1
- package/android/src/main/java/com/apollohg/editor/ImageResizeOverlayView.kt +199 -0
- package/android/src/main/java/com/apollohg/editor/NativeEditorExpoView.kt +16 -3
- package/android/src/main/java/com/apollohg/editor/NativeEditorModule.kt +82 -1
- package/android/src/main/java/com/apollohg/editor/NativeToolbar.kt +403 -45
- package/android/src/main/java/com/apollohg/editor/RemoteSelectionOverlayView.kt +246 -0
- package/android/src/main/java/com/apollohg/editor/RenderBridge.kt +841 -155
- package/android/src/main/java/com/apollohg/editor/RichTextEditorView.kt +125 -8
- package/{src/EditorTheme.ts → dist/EditorTheme.d.ts} +12 -52
- package/dist/EditorTheme.js +29 -0
- package/dist/EditorToolbar.d.ts +129 -0
- package/dist/EditorToolbar.js +394 -0
- package/dist/NativeEditorBridge.d.ts +242 -0
- package/dist/NativeEditorBridge.js +647 -0
- package/dist/NativeRichTextEditor.d.ts +142 -0
- package/dist/NativeRichTextEditor.js +649 -0
- package/dist/YjsCollaboration.d.ts +83 -0
- package/dist/YjsCollaboration.js +585 -0
- package/dist/addons.d.ts +70 -0
- package/dist/addons.js +77 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +26 -0
- package/dist/schemas.d.ts +35 -0
- package/{src/schemas.ts → dist/schemas.js} +62 -27
- package/dist/useNativeEditor.d.ts +40 -0
- package/dist/useNativeEditor.js +117 -0
- package/ios/EditorAddons.swift +26 -3
- package/ios/EditorCore.xcframework/Info.plist +5 -5
- package/ios/EditorCore.xcframework/ios-arm64/libeditor_core.a +0 -0
- package/ios/EditorCore.xcframework/ios-arm64_x86_64-simulator/libeditor_core.a +0 -0
- package/ios/EditorLayoutManager.swift +236 -0
- package/ios/EditorTheme.swift +51 -1
- package/ios/Generated_editor_core.swift +270 -2
- package/ios/NativeEditorExpoView.swift +612 -45
- package/ios/NativeEditorModule.swift +81 -0
- package/ios/PositionBridge.swift +22 -0
- package/ios/RenderBridge.swift +427 -39
- package/ios/RichTextEditorView.swift +1342 -18
- package/ios/editor_coreFFI/editor_coreFFI.h +209 -0
- package/package.json +80 -64
- package/rust/android/arm64-v8a/libeditor_core.so +0 -0
- package/rust/android/armeabi-v7a/libeditor_core.so +0 -0
- package/rust/android/x86_64/libeditor_core.so +0 -0
- package/rust/bindings/kotlin/uniffi/editor_core/editor_core.kt +404 -4
- package/src/EditorToolbar.tsx +0 -620
- package/src/NativeEditorBridge.ts +0 -607
- package/src/NativeRichTextEditor.tsx +0 -951
- package/src/addons.ts +0 -158
- package/src/index.ts +0 -63
- package/src/useNativeEditor.ts +0 -173
|
@@ -0,0 +1,647 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.NativeCollaborationBridge = exports.NativeEditorBridge = void 0;
|
|
4
|
+
exports.normalizeActiveState = normalizeActiveState;
|
|
5
|
+
exports.parseEditorUpdateJson = parseEditorUpdateJson;
|
|
6
|
+
exports.encodeCollaborationStateBase64 = encodeCollaborationStateBase64;
|
|
7
|
+
exports.decodeCollaborationStateBase64 = decodeCollaborationStateBase64;
|
|
8
|
+
exports.parseCollaborationResultJson = parseCollaborationResultJson;
|
|
9
|
+
exports._resetNativeModuleCache = _resetNativeModuleCache;
|
|
10
|
+
const expo_modules_core_1 = require("expo-modules-core");
|
|
11
|
+
const ERR_DESTROYED = 'NativeEditorBridge: editor has been destroyed';
|
|
12
|
+
const ERR_NATIVE_RESPONSE = 'NativeEditorBridge: invalid JSON response from native module';
|
|
13
|
+
const ERR_INVALID_ENCODED_STATE = 'NativeEditorBridge: invalid encoded collaboration state';
|
|
14
|
+
const BASE64_ALPHABET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
|
|
15
|
+
function normalizeActiveState(raw) {
|
|
16
|
+
const obj = raw ?? {};
|
|
17
|
+
return {
|
|
18
|
+
marks: (obj.marks ?? {}),
|
|
19
|
+
markAttrs: (obj.markAttrs ?? {}),
|
|
20
|
+
nodes: (obj.nodes ?? {}),
|
|
21
|
+
commands: (obj.commands ?? {}),
|
|
22
|
+
allowedMarks: (obj.allowedMarks ?? []),
|
|
23
|
+
insertableNodes: (obj.insertableNodes ?? []),
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
function parseRenderElements(json) {
|
|
27
|
+
if (!json || json === '[]')
|
|
28
|
+
return [];
|
|
29
|
+
try {
|
|
30
|
+
const parsed = JSON.parse(json);
|
|
31
|
+
if (parsed != null &&
|
|
32
|
+
typeof parsed === 'object' &&
|
|
33
|
+
!Array.isArray(parsed) &&
|
|
34
|
+
'error' in parsed) {
|
|
35
|
+
throw new Error(`NativeEditorBridge: ${parsed.error}`);
|
|
36
|
+
}
|
|
37
|
+
if (!Array.isArray(parsed)) {
|
|
38
|
+
throw new Error(ERR_NATIVE_RESPONSE);
|
|
39
|
+
}
|
|
40
|
+
return parsed;
|
|
41
|
+
}
|
|
42
|
+
catch (e) {
|
|
43
|
+
if (e instanceof Error && e.message.startsWith('NativeEditorBridge:')) {
|
|
44
|
+
throw e;
|
|
45
|
+
}
|
|
46
|
+
throw new Error(ERR_NATIVE_RESPONSE);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
function parseEditorUpdateJson(json) {
|
|
50
|
+
if (!json || json === '')
|
|
51
|
+
return null;
|
|
52
|
+
try {
|
|
53
|
+
const parsed = JSON.parse(json);
|
|
54
|
+
if ('error' in parsed) {
|
|
55
|
+
throw new Error(`NativeEditorBridge: ${parsed.error}`);
|
|
56
|
+
}
|
|
57
|
+
return {
|
|
58
|
+
renderElements: (parsed.renderElements ?? []),
|
|
59
|
+
selection: (parsed.selection ?? { type: 'text', anchor: 0, head: 0 }),
|
|
60
|
+
activeState: normalizeActiveState(parsed.activeState),
|
|
61
|
+
historyState: (parsed.historyState ?? {
|
|
62
|
+
canUndo: false,
|
|
63
|
+
canRedo: false,
|
|
64
|
+
}),
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
catch (e) {
|
|
68
|
+
if (e instanceof Error && e.message.startsWith('NativeEditorBridge:')) {
|
|
69
|
+
throw e;
|
|
70
|
+
}
|
|
71
|
+
throw new Error(ERR_NATIVE_RESPONSE);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
function parseDocumentJSON(json) {
|
|
75
|
+
if (!json || json === '{}')
|
|
76
|
+
return {};
|
|
77
|
+
try {
|
|
78
|
+
const parsed = JSON.parse(json);
|
|
79
|
+
if (parsed != null &&
|
|
80
|
+
typeof parsed === 'object' &&
|
|
81
|
+
'error' in parsed) {
|
|
82
|
+
throw new Error(`NativeEditorBridge: ${parsed.error}`);
|
|
83
|
+
}
|
|
84
|
+
return parsed;
|
|
85
|
+
}
|
|
86
|
+
catch (e) {
|
|
87
|
+
if (e instanceof Error && e.message.startsWith('NativeEditorBridge:')) {
|
|
88
|
+
throw e;
|
|
89
|
+
}
|
|
90
|
+
throw new Error(ERR_NATIVE_RESPONSE);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
function parseCollaborationPeersJson(json) {
|
|
94
|
+
if (!json || json === '[]')
|
|
95
|
+
return [];
|
|
96
|
+
try {
|
|
97
|
+
const parsed = JSON.parse(json);
|
|
98
|
+
return Array.isArray(parsed) ? parsed : [];
|
|
99
|
+
}
|
|
100
|
+
catch {
|
|
101
|
+
throw new Error(ERR_NATIVE_RESPONSE);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
function parseByteArrayJson(json) {
|
|
105
|
+
if (!json || json === '[]')
|
|
106
|
+
return new Uint8Array();
|
|
107
|
+
try {
|
|
108
|
+
const parsed = JSON.parse(json);
|
|
109
|
+
if (!Array.isArray(parsed)) {
|
|
110
|
+
throw new Error(ERR_NATIVE_RESPONSE);
|
|
111
|
+
}
|
|
112
|
+
return Uint8Array.from(parsed);
|
|
113
|
+
}
|
|
114
|
+
catch (e) {
|
|
115
|
+
if (e instanceof Error && e.message.startsWith('NativeEditorBridge:')) {
|
|
116
|
+
throw e;
|
|
117
|
+
}
|
|
118
|
+
throw new Error(ERR_NATIVE_RESPONSE);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
function bytesToBase64(bytes) {
|
|
122
|
+
let output = '';
|
|
123
|
+
for (let index = 0; index < bytes.length; index += 3) {
|
|
124
|
+
const byte1 = bytes[index] ?? 0;
|
|
125
|
+
const byte2 = bytes[index + 1] ?? 0;
|
|
126
|
+
const byte3 = bytes[index + 2] ?? 0;
|
|
127
|
+
const chunk = (byte1 << 16) | (byte2 << 8) | byte3;
|
|
128
|
+
output += BASE64_ALPHABET[(chunk >> 18) & 0x3f];
|
|
129
|
+
output += BASE64_ALPHABET[(chunk >> 12) & 0x3f];
|
|
130
|
+
output += index + 1 < bytes.length ? BASE64_ALPHABET[(chunk >> 6) & 0x3f] : '=';
|
|
131
|
+
output += index + 2 < bytes.length ? BASE64_ALPHABET[chunk & 0x3f] : '=';
|
|
132
|
+
}
|
|
133
|
+
return output;
|
|
134
|
+
}
|
|
135
|
+
function base64ToBytes(base64) {
|
|
136
|
+
const normalized = base64.replace(/\s+/g, '');
|
|
137
|
+
if (normalized.length === 0)
|
|
138
|
+
return new Uint8Array();
|
|
139
|
+
if (normalized.length % 4 === 1) {
|
|
140
|
+
throw new Error(ERR_INVALID_ENCODED_STATE);
|
|
141
|
+
}
|
|
142
|
+
const bytes = [];
|
|
143
|
+
for (let index = 0; index < normalized.length; index += 4) {
|
|
144
|
+
const chunk = normalized.slice(index, index + 4);
|
|
145
|
+
const values = chunk.split('').map((char, charIndex) => {
|
|
146
|
+
if (char === '=') {
|
|
147
|
+
return charIndex >= 2 ? 0 : -1;
|
|
148
|
+
}
|
|
149
|
+
const value = BASE64_ALPHABET.indexOf(char);
|
|
150
|
+
return value;
|
|
151
|
+
});
|
|
152
|
+
if (values[0] < 0 || values[1] < 0 || values.some((value) => value < 0)) {
|
|
153
|
+
throw new Error(ERR_INVALID_ENCODED_STATE);
|
|
154
|
+
}
|
|
155
|
+
const combined = (values[0] << 18) | (values[1] << 12) | (values[2] << 6) | values[3];
|
|
156
|
+
bytes.push((combined >> 16) & 0xff);
|
|
157
|
+
if (chunk[2] !== '=') {
|
|
158
|
+
bytes.push((combined >> 8) & 0xff);
|
|
159
|
+
}
|
|
160
|
+
if (chunk[3] !== '=') {
|
|
161
|
+
bytes.push(combined & 0xff);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return Uint8Array.from(bytes);
|
|
165
|
+
}
|
|
166
|
+
function normalizeEncodedStateInput(encodedState) {
|
|
167
|
+
if (typeof encodedState === 'string') {
|
|
168
|
+
return Array.from(base64ToBytes(encodedState));
|
|
169
|
+
}
|
|
170
|
+
return Array.from(encodedState);
|
|
171
|
+
}
|
|
172
|
+
function encodeCollaborationStateBase64(encodedState) {
|
|
173
|
+
return bytesToBase64(normalizeEncodedStateInput(encodedState));
|
|
174
|
+
}
|
|
175
|
+
function decodeCollaborationStateBase64(base64) {
|
|
176
|
+
return base64ToBytes(base64);
|
|
177
|
+
}
|
|
178
|
+
function parseCollaborationResultJson(json) {
|
|
179
|
+
if (!json || json === '') {
|
|
180
|
+
return {
|
|
181
|
+
messages: [],
|
|
182
|
+
documentChanged: false,
|
|
183
|
+
peersChanged: false,
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
try {
|
|
187
|
+
const parsed = JSON.parse(json);
|
|
188
|
+
if ('error' in parsed) {
|
|
189
|
+
throw new Error(`NativeEditorBridge: ${parsed.error}`);
|
|
190
|
+
}
|
|
191
|
+
return {
|
|
192
|
+
messages: Array.isArray(parsed.messages) ? parsed.messages : [],
|
|
193
|
+
documentChanged: parsed.documentChanged === true,
|
|
194
|
+
documentJson: parsed.documentJson && typeof parsed.documentJson === 'object'
|
|
195
|
+
? parsed.documentJson
|
|
196
|
+
: undefined,
|
|
197
|
+
peersChanged: parsed.peersChanged === true,
|
|
198
|
+
peers: Array.isArray(parsed.peers) ? parsed.peers : undefined,
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
catch (e) {
|
|
202
|
+
if (e instanceof Error && e.message.startsWith('NativeEditorBridge:')) {
|
|
203
|
+
throw e;
|
|
204
|
+
}
|
|
205
|
+
throw new Error(ERR_NATIVE_RESPONSE);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
let _nativeModule = null;
|
|
209
|
+
function getNativeModule() {
|
|
210
|
+
if (!_nativeModule) {
|
|
211
|
+
_nativeModule = (0, expo_modules_core_1.requireNativeModule)('NativeEditor');
|
|
212
|
+
}
|
|
213
|
+
return _nativeModule;
|
|
214
|
+
}
|
|
215
|
+
/** @internal Reset the cached native module reference. For testing only. */
|
|
216
|
+
function _resetNativeModuleCache() {
|
|
217
|
+
_nativeModule = null;
|
|
218
|
+
}
|
|
219
|
+
class NativeEditorBridge {
|
|
220
|
+
constructor(editorId) {
|
|
221
|
+
this._destroyed = false;
|
|
222
|
+
this._lastSelection = { type: 'text', anchor: 0, head: 0 };
|
|
223
|
+
this._editorId = editorId;
|
|
224
|
+
}
|
|
225
|
+
/** Create a new editor instance backed by the Rust engine. */
|
|
226
|
+
static create(config) {
|
|
227
|
+
const configObj = {};
|
|
228
|
+
if (config?.maxLength != null)
|
|
229
|
+
configObj.maxLength = config.maxLength;
|
|
230
|
+
if (config?.allowBase64Images != null) {
|
|
231
|
+
configObj.allowBase64Images = config.allowBase64Images;
|
|
232
|
+
}
|
|
233
|
+
if (config?.schemaJson != null) {
|
|
234
|
+
try {
|
|
235
|
+
configObj.schema = JSON.parse(config.schemaJson);
|
|
236
|
+
}
|
|
237
|
+
catch {
|
|
238
|
+
// Fall back to the default schema when the provided JSON is invalid.
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
const id = getNativeModule().editorCreate(JSON.stringify(configObj));
|
|
242
|
+
return new NativeEditorBridge(id);
|
|
243
|
+
}
|
|
244
|
+
/** The underlying native editor ID. */
|
|
245
|
+
get editorId() {
|
|
246
|
+
return this._editorId;
|
|
247
|
+
}
|
|
248
|
+
/** Whether this bridge has been destroyed. */
|
|
249
|
+
get isDestroyed() {
|
|
250
|
+
return this._destroyed;
|
|
251
|
+
}
|
|
252
|
+
/** Destroy the editor instance and free native resources. */
|
|
253
|
+
destroy() {
|
|
254
|
+
if (this._destroyed)
|
|
255
|
+
return;
|
|
256
|
+
this._destroyed = true;
|
|
257
|
+
getNativeModule().editorDestroy(this._editorId);
|
|
258
|
+
}
|
|
259
|
+
/** Set content from HTML. Returns render elements for display. */
|
|
260
|
+
setHtml(html) {
|
|
261
|
+
this.assertNotDestroyed();
|
|
262
|
+
const json = getNativeModule().editorSetHtml(this._editorId, html);
|
|
263
|
+
return parseRenderElements(json);
|
|
264
|
+
}
|
|
265
|
+
/** Get content as HTML. */
|
|
266
|
+
getHtml() {
|
|
267
|
+
this.assertNotDestroyed();
|
|
268
|
+
return getNativeModule().editorGetHtml(this._editorId);
|
|
269
|
+
}
|
|
270
|
+
/** Set content from ProseMirror JSON. Returns render elements. */
|
|
271
|
+
setJson(doc) {
|
|
272
|
+
this.assertNotDestroyed();
|
|
273
|
+
const json = getNativeModule().editorSetJson(this._editorId, JSON.stringify(doc));
|
|
274
|
+
return parseRenderElements(json);
|
|
275
|
+
}
|
|
276
|
+
/** Get content as ProseMirror JSON. */
|
|
277
|
+
getJson() {
|
|
278
|
+
this.assertNotDestroyed();
|
|
279
|
+
const json = getNativeModule().editorGetJson(this._editorId);
|
|
280
|
+
return parseDocumentJSON(json);
|
|
281
|
+
}
|
|
282
|
+
/** Insert text at a document position. Returns the full update. */
|
|
283
|
+
insertText(pos, text) {
|
|
284
|
+
this.assertNotDestroyed();
|
|
285
|
+
const json = getNativeModule().editorInsertText(this._editorId, pos, text);
|
|
286
|
+
const update = parseEditorUpdateJson(json);
|
|
287
|
+
if (update)
|
|
288
|
+
this._lastSelection = update.selection;
|
|
289
|
+
return update;
|
|
290
|
+
}
|
|
291
|
+
/** Delete a range [from, to). Returns the full update. */
|
|
292
|
+
deleteRange(from, to) {
|
|
293
|
+
this.assertNotDestroyed();
|
|
294
|
+
const json = getNativeModule().editorDeleteRange(this._editorId, from, to);
|
|
295
|
+
const update = parseEditorUpdateJson(json);
|
|
296
|
+
if (update)
|
|
297
|
+
this._lastSelection = update.selection;
|
|
298
|
+
return update;
|
|
299
|
+
}
|
|
300
|
+
/** Replace the current selection with text atomically. */
|
|
301
|
+
replaceSelectionText(text) {
|
|
302
|
+
this.assertNotDestroyed();
|
|
303
|
+
const json = getNativeModule().editorReplaceSelectionText(this._editorId, text);
|
|
304
|
+
const update = parseEditorUpdateJson(json);
|
|
305
|
+
if (update)
|
|
306
|
+
this._lastSelection = update.selection;
|
|
307
|
+
return update;
|
|
308
|
+
}
|
|
309
|
+
/** Toggle a mark (bold, italic, etc.) on the current selection. */
|
|
310
|
+
toggleMark(markType) {
|
|
311
|
+
this.assertNotDestroyed();
|
|
312
|
+
const scalarSelection = this.currentScalarSelection();
|
|
313
|
+
const json = scalarSelection
|
|
314
|
+
? getNativeModule().editorToggleMarkAtSelectionScalar(this._editorId, scalarSelection.anchor, scalarSelection.head, markType)
|
|
315
|
+
: getNativeModule().editorToggleMark(this._editorId, markType);
|
|
316
|
+
const update = parseEditorUpdateJson(json);
|
|
317
|
+
if (update)
|
|
318
|
+
this._lastSelection = update.selection;
|
|
319
|
+
return update;
|
|
320
|
+
}
|
|
321
|
+
/** Set a mark with attrs on the current selection. */
|
|
322
|
+
setMark(markType, attrs) {
|
|
323
|
+
this.assertNotDestroyed();
|
|
324
|
+
const attrsJson = JSON.stringify(attrs);
|
|
325
|
+
const scalarSelection = this.currentScalarSelection();
|
|
326
|
+
const json = scalarSelection
|
|
327
|
+
? getNativeModule().editorSetMarkAtSelectionScalar(this._editorId, scalarSelection.anchor, scalarSelection.head, markType, attrsJson)
|
|
328
|
+
: getNativeModule().editorSetMark(this._editorId, markType, attrsJson);
|
|
329
|
+
const update = parseEditorUpdateJson(json);
|
|
330
|
+
if (update)
|
|
331
|
+
this._lastSelection = update.selection;
|
|
332
|
+
return update;
|
|
333
|
+
}
|
|
334
|
+
/** Remove a mark from the current selection. */
|
|
335
|
+
unsetMark(markType) {
|
|
336
|
+
this.assertNotDestroyed();
|
|
337
|
+
const scalarSelection = this.currentScalarSelection();
|
|
338
|
+
const json = scalarSelection
|
|
339
|
+
? getNativeModule().editorUnsetMarkAtSelectionScalar(this._editorId, scalarSelection.anchor, scalarSelection.head, markType)
|
|
340
|
+
: getNativeModule().editorUnsetMark(this._editorId, markType);
|
|
341
|
+
const update = parseEditorUpdateJson(json);
|
|
342
|
+
if (update)
|
|
343
|
+
this._lastSelection = update.selection;
|
|
344
|
+
return update;
|
|
345
|
+
}
|
|
346
|
+
/** Toggle blockquote wrapping for the current block selection. */
|
|
347
|
+
toggleBlockquote() {
|
|
348
|
+
this.assertNotDestroyed();
|
|
349
|
+
const scalarSelection = this.currentScalarSelection();
|
|
350
|
+
const json = scalarSelection
|
|
351
|
+
? getNativeModule().editorToggleBlockquoteAtSelectionScalar(this._editorId, scalarSelection.anchor, scalarSelection.head)
|
|
352
|
+
: getNativeModule().editorToggleBlockquote(this._editorId);
|
|
353
|
+
const update = parseEditorUpdateJson(json);
|
|
354
|
+
if (update)
|
|
355
|
+
this._lastSelection = update.selection;
|
|
356
|
+
return update;
|
|
357
|
+
}
|
|
358
|
+
/** Set the document selection by anchor and head positions. */
|
|
359
|
+
setSelection(anchor, head) {
|
|
360
|
+
this.assertNotDestroyed();
|
|
361
|
+
getNativeModule().editorSetSelection(this._editorId, anchor, head);
|
|
362
|
+
this._lastSelection = { type: 'text', anchor, head };
|
|
363
|
+
}
|
|
364
|
+
/** Get the current selection from the Rust engine (synchronous native call).
|
|
365
|
+
* Always returns the live selection, not a stale cache. */
|
|
366
|
+
getSelection() {
|
|
367
|
+
if (this._destroyed)
|
|
368
|
+
return { type: 'text', anchor: 0, head: 0 };
|
|
369
|
+
try {
|
|
370
|
+
const json = getNativeModule().editorGetSelection(this._editorId);
|
|
371
|
+
const sel = JSON.parse(json);
|
|
372
|
+
this._lastSelection = sel;
|
|
373
|
+
return sel;
|
|
374
|
+
}
|
|
375
|
+
catch {
|
|
376
|
+
return this._lastSelection;
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
/** Update the cached selection from native events (scalar offsets).
|
|
380
|
+
* Called by the React component when native selection change events arrive. */
|
|
381
|
+
updateSelectionFromNative(anchor, head) {
|
|
382
|
+
if (this._destroyed)
|
|
383
|
+
return;
|
|
384
|
+
this._lastSelection = { type: 'text', anchor, head };
|
|
385
|
+
}
|
|
386
|
+
/** Get the current full state from Rust (render elements, selection, etc.). */
|
|
387
|
+
getCurrentState() {
|
|
388
|
+
this.assertNotDestroyed();
|
|
389
|
+
const json = getNativeModule().editorGetCurrentState(this._editorId);
|
|
390
|
+
const update = parseEditorUpdateJson(json);
|
|
391
|
+
if (update)
|
|
392
|
+
this._lastSelection = update.selection;
|
|
393
|
+
return update;
|
|
394
|
+
}
|
|
395
|
+
/** Split the block at a position (Enter key). */
|
|
396
|
+
splitBlock(pos) {
|
|
397
|
+
this.assertNotDestroyed();
|
|
398
|
+
const json = getNativeModule().editorSplitBlock(this._editorId, pos);
|
|
399
|
+
const update = parseEditorUpdateJson(json);
|
|
400
|
+
if (update)
|
|
401
|
+
this._lastSelection = update.selection;
|
|
402
|
+
return update;
|
|
403
|
+
}
|
|
404
|
+
/** Insert HTML content at the current selection. */
|
|
405
|
+
insertContentHtml(html) {
|
|
406
|
+
this.assertNotDestroyed();
|
|
407
|
+
const json = getNativeModule().editorInsertContentHtml(this._editorId, html);
|
|
408
|
+
const update = parseEditorUpdateJson(json);
|
|
409
|
+
if (update)
|
|
410
|
+
this._lastSelection = update.selection;
|
|
411
|
+
return update;
|
|
412
|
+
}
|
|
413
|
+
/** Insert JSON content at the current selection. */
|
|
414
|
+
insertContentJson(doc) {
|
|
415
|
+
this.assertNotDestroyed();
|
|
416
|
+
const json = getNativeModule().editorInsertContentJson(this._editorId, JSON.stringify(doc));
|
|
417
|
+
const update = parseEditorUpdateJson(json);
|
|
418
|
+
if (update)
|
|
419
|
+
this._lastSelection = update.selection;
|
|
420
|
+
return update;
|
|
421
|
+
}
|
|
422
|
+
/** Insert JSON content at an explicit scalar selection. */
|
|
423
|
+
insertContentJsonAtSelectionScalar(scalarAnchor, scalarHead, doc) {
|
|
424
|
+
this.assertNotDestroyed();
|
|
425
|
+
const json = getNativeModule().editorInsertContentJsonAtSelectionScalar(this._editorId, scalarAnchor, scalarHead, JSON.stringify(doc));
|
|
426
|
+
const update = parseEditorUpdateJson(json);
|
|
427
|
+
if (update)
|
|
428
|
+
this._lastSelection = update.selection;
|
|
429
|
+
return update;
|
|
430
|
+
}
|
|
431
|
+
/** Replace entire document with HTML via transaction (preserves undo history). */
|
|
432
|
+
replaceHtml(html) {
|
|
433
|
+
this.assertNotDestroyed();
|
|
434
|
+
const json = getNativeModule().editorReplaceHtml(this._editorId, html);
|
|
435
|
+
const update = parseEditorUpdateJson(json);
|
|
436
|
+
if (update)
|
|
437
|
+
this._lastSelection = update.selection;
|
|
438
|
+
return update;
|
|
439
|
+
}
|
|
440
|
+
/** Replace entire document with JSON via transaction (preserves undo history). */
|
|
441
|
+
replaceJson(doc) {
|
|
442
|
+
this.assertNotDestroyed();
|
|
443
|
+
const json = getNativeModule().editorReplaceJson(this._editorId, JSON.stringify(doc));
|
|
444
|
+
const update = parseEditorUpdateJson(json);
|
|
445
|
+
if (update)
|
|
446
|
+
this._lastSelection = update.selection;
|
|
447
|
+
return update;
|
|
448
|
+
}
|
|
449
|
+
/** Undo the last operation. Returns update or null if nothing to undo. */
|
|
450
|
+
undo() {
|
|
451
|
+
this.assertNotDestroyed();
|
|
452
|
+
const json = getNativeModule().editorUndo(this._editorId);
|
|
453
|
+
const update = parseEditorUpdateJson(json);
|
|
454
|
+
if (update)
|
|
455
|
+
this._lastSelection = update.selection;
|
|
456
|
+
return update;
|
|
457
|
+
}
|
|
458
|
+
/** Redo the last undone operation. Returns update or null if nothing to redo. */
|
|
459
|
+
redo() {
|
|
460
|
+
this.assertNotDestroyed();
|
|
461
|
+
const json = getNativeModule().editorRedo(this._editorId);
|
|
462
|
+
const update = parseEditorUpdateJson(json);
|
|
463
|
+
if (update)
|
|
464
|
+
this._lastSelection = update.selection;
|
|
465
|
+
return update;
|
|
466
|
+
}
|
|
467
|
+
/** Check if undo is available. */
|
|
468
|
+
canUndo() {
|
|
469
|
+
this.assertNotDestroyed();
|
|
470
|
+
return getNativeModule().editorCanUndo(this._editorId);
|
|
471
|
+
}
|
|
472
|
+
/** Check if redo is available. */
|
|
473
|
+
canRedo() {
|
|
474
|
+
this.assertNotDestroyed();
|
|
475
|
+
return getNativeModule().editorCanRedo(this._editorId);
|
|
476
|
+
}
|
|
477
|
+
/** Toggle a list type on the current selection. Wraps if not in list, unwraps if already in that list type. */
|
|
478
|
+
toggleList(listType) {
|
|
479
|
+
this.assertNotDestroyed();
|
|
480
|
+
const isActive = this.getCurrentState()?.activeState?.nodes?.[listType] === true;
|
|
481
|
+
const scalarSelection = this.currentScalarSelection();
|
|
482
|
+
const json = isActive
|
|
483
|
+
? scalarSelection
|
|
484
|
+
? getNativeModule().editorUnwrapFromListAtSelectionScalar(this._editorId, scalarSelection.anchor, scalarSelection.head)
|
|
485
|
+
: getNativeModule().editorUnwrapFromList(this._editorId)
|
|
486
|
+
: scalarSelection
|
|
487
|
+
? getNativeModule().editorWrapInListAtSelectionScalar(this._editorId, scalarSelection.anchor, scalarSelection.head, listType)
|
|
488
|
+
: getNativeModule().editorWrapInList(this._editorId, listType);
|
|
489
|
+
const update = parseEditorUpdateJson(json);
|
|
490
|
+
if (update)
|
|
491
|
+
this._lastSelection = update.selection;
|
|
492
|
+
return update;
|
|
493
|
+
}
|
|
494
|
+
/** Unwrap the current list item back to a paragraph. */
|
|
495
|
+
unwrapFromList() {
|
|
496
|
+
this.assertNotDestroyed();
|
|
497
|
+
const scalarSelection = this.currentScalarSelection();
|
|
498
|
+
const json = scalarSelection
|
|
499
|
+
? getNativeModule().editorUnwrapFromListAtSelectionScalar(this._editorId, scalarSelection.anchor, scalarSelection.head)
|
|
500
|
+
: getNativeModule().editorUnwrapFromList(this._editorId);
|
|
501
|
+
const update = parseEditorUpdateJson(json);
|
|
502
|
+
if (update)
|
|
503
|
+
this._lastSelection = update.selection;
|
|
504
|
+
return update;
|
|
505
|
+
}
|
|
506
|
+
/** Indent the current list item into a nested list. */
|
|
507
|
+
indentListItem() {
|
|
508
|
+
this.assertNotDestroyed();
|
|
509
|
+
const scalarSelection = this.currentScalarSelection();
|
|
510
|
+
const json = scalarSelection
|
|
511
|
+
? getNativeModule().editorIndentListItemAtSelectionScalar(this._editorId, scalarSelection.anchor, scalarSelection.head)
|
|
512
|
+
: getNativeModule().editorIndentListItem(this._editorId);
|
|
513
|
+
const update = parseEditorUpdateJson(json);
|
|
514
|
+
if (update)
|
|
515
|
+
this._lastSelection = update.selection;
|
|
516
|
+
return update;
|
|
517
|
+
}
|
|
518
|
+
/** Outdent the current list item to the parent list level. */
|
|
519
|
+
outdentListItem() {
|
|
520
|
+
this.assertNotDestroyed();
|
|
521
|
+
const scalarSelection = this.currentScalarSelection();
|
|
522
|
+
const json = scalarSelection
|
|
523
|
+
? getNativeModule().editorOutdentListItemAtSelectionScalar(this._editorId, scalarSelection.anchor, scalarSelection.head)
|
|
524
|
+
: getNativeModule().editorOutdentListItem(this._editorId);
|
|
525
|
+
const update = parseEditorUpdateJson(json);
|
|
526
|
+
if (update)
|
|
527
|
+
this._lastSelection = update.selection;
|
|
528
|
+
return update;
|
|
529
|
+
}
|
|
530
|
+
/** Insert a void node (e.g. 'horizontalRule') at the current selection. */
|
|
531
|
+
insertNode(nodeType) {
|
|
532
|
+
this.assertNotDestroyed();
|
|
533
|
+
const scalarSelection = this.currentScalarSelection();
|
|
534
|
+
const json = scalarSelection
|
|
535
|
+
? getNativeModule().editorInsertNodeAtSelectionScalar(this._editorId, scalarSelection.anchor, scalarSelection.head, nodeType)
|
|
536
|
+
: getNativeModule().editorInsertNode(this._editorId, nodeType);
|
|
537
|
+
const update = parseEditorUpdateJson(json);
|
|
538
|
+
if (update)
|
|
539
|
+
this._lastSelection = update.selection;
|
|
540
|
+
return update;
|
|
541
|
+
}
|
|
542
|
+
assertNotDestroyed() {
|
|
543
|
+
if (this._destroyed) {
|
|
544
|
+
throw new Error(ERR_DESTROYED);
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
currentScalarSelection() {
|
|
548
|
+
const selection = this._lastSelection;
|
|
549
|
+
const nativeModule = getNativeModule();
|
|
550
|
+
if (selection.type === 'text') {
|
|
551
|
+
const anchor = selection.anchor ?? 0;
|
|
552
|
+
const head = selection.head ?? anchor;
|
|
553
|
+
return {
|
|
554
|
+
anchor: nativeModule.editorDocToScalar(this._editorId, anchor),
|
|
555
|
+
head: nativeModule.editorDocToScalar(this._editorId, head),
|
|
556
|
+
};
|
|
557
|
+
}
|
|
558
|
+
if (selection.type === 'node' && typeof selection.pos === 'number') {
|
|
559
|
+
const scalar = nativeModule.editorDocToScalar(this._editorId, selection.pos);
|
|
560
|
+
return { anchor: scalar, head: scalar };
|
|
561
|
+
}
|
|
562
|
+
return null;
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
exports.NativeEditorBridge = NativeEditorBridge;
|
|
566
|
+
class NativeCollaborationBridge {
|
|
567
|
+
constructor(sessionId) {
|
|
568
|
+
this._destroyed = false;
|
|
569
|
+
this._sessionId = sessionId;
|
|
570
|
+
}
|
|
571
|
+
static create(config) {
|
|
572
|
+
const { initialEncodedState, ...normalizedConfig } = config ?? {};
|
|
573
|
+
const id = getNativeModule().collaborationSessionCreate(JSON.stringify(normalizedConfig));
|
|
574
|
+
const bridge = new NativeCollaborationBridge(id);
|
|
575
|
+
if (initialEncodedState != null) {
|
|
576
|
+
try {
|
|
577
|
+
bridge.replaceEncodedState(initialEncodedState);
|
|
578
|
+
}
|
|
579
|
+
catch (error) {
|
|
580
|
+
bridge.destroy();
|
|
581
|
+
throw error;
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
return bridge;
|
|
585
|
+
}
|
|
586
|
+
get sessionId() {
|
|
587
|
+
return this._sessionId;
|
|
588
|
+
}
|
|
589
|
+
get isDestroyed() {
|
|
590
|
+
return this._destroyed;
|
|
591
|
+
}
|
|
592
|
+
destroy() {
|
|
593
|
+
if (this._destroyed)
|
|
594
|
+
return;
|
|
595
|
+
this._destroyed = true;
|
|
596
|
+
getNativeModule().collaborationSessionDestroy(this._sessionId);
|
|
597
|
+
}
|
|
598
|
+
getDocumentJson() {
|
|
599
|
+
this.assertNotDestroyed();
|
|
600
|
+
return parseDocumentJSON(getNativeModule().collaborationSessionGetDocumentJson(this._sessionId));
|
|
601
|
+
}
|
|
602
|
+
getEncodedState() {
|
|
603
|
+
this.assertNotDestroyed();
|
|
604
|
+
return parseByteArrayJson(getNativeModule().collaborationSessionGetEncodedState(this._sessionId));
|
|
605
|
+
}
|
|
606
|
+
getEncodedStateBase64() {
|
|
607
|
+
return encodeCollaborationStateBase64(this.getEncodedState());
|
|
608
|
+
}
|
|
609
|
+
getPeers() {
|
|
610
|
+
this.assertNotDestroyed();
|
|
611
|
+
return parseCollaborationPeersJson(getNativeModule().collaborationSessionGetPeersJson(this._sessionId));
|
|
612
|
+
}
|
|
613
|
+
start() {
|
|
614
|
+
this.assertNotDestroyed();
|
|
615
|
+
return parseCollaborationResultJson(getNativeModule().collaborationSessionStart(this._sessionId));
|
|
616
|
+
}
|
|
617
|
+
applyLocalDocumentJson(doc) {
|
|
618
|
+
this.assertNotDestroyed();
|
|
619
|
+
return parseCollaborationResultJson(getNativeModule().collaborationSessionApplyLocalDocumentJson(this._sessionId, JSON.stringify(doc)));
|
|
620
|
+
}
|
|
621
|
+
applyEncodedState(encodedState) {
|
|
622
|
+
this.assertNotDestroyed();
|
|
623
|
+
return parseCollaborationResultJson(getNativeModule().collaborationSessionApplyEncodedState(this._sessionId, JSON.stringify(normalizeEncodedStateInput(encodedState))));
|
|
624
|
+
}
|
|
625
|
+
replaceEncodedState(encodedState) {
|
|
626
|
+
this.assertNotDestroyed();
|
|
627
|
+
return parseCollaborationResultJson(getNativeModule().collaborationSessionReplaceEncodedState(this._sessionId, JSON.stringify(normalizeEncodedStateInput(encodedState))));
|
|
628
|
+
}
|
|
629
|
+
handleMessage(bytes) {
|
|
630
|
+
this.assertNotDestroyed();
|
|
631
|
+
return parseCollaborationResultJson(getNativeModule().collaborationSessionHandleMessage(this._sessionId, JSON.stringify(Array.from(bytes))));
|
|
632
|
+
}
|
|
633
|
+
setLocalAwareness(state) {
|
|
634
|
+
this.assertNotDestroyed();
|
|
635
|
+
return parseCollaborationResultJson(getNativeModule().collaborationSessionSetLocalAwareness(this._sessionId, JSON.stringify(state)));
|
|
636
|
+
}
|
|
637
|
+
clearLocalAwareness() {
|
|
638
|
+
this.assertNotDestroyed();
|
|
639
|
+
return parseCollaborationResultJson(getNativeModule().collaborationSessionClearLocalAwareness(this._sessionId));
|
|
640
|
+
}
|
|
641
|
+
assertNotDestroyed() {
|
|
642
|
+
if (this._destroyed) {
|
|
643
|
+
throw new Error(ERR_DESTROYED);
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
exports.NativeCollaborationBridge = NativeCollaborationBridge;
|