@apollohg/react-native-rich-text-editor 2.0.0 → 2.0.2
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 +8 -116
- package/THIRD_PARTY_NOTICES.md +1 -1
- package/android/build.gradle +3 -0
- package/android/src/main/java/com/apollohg/editor/EditorEditTextImages.kt +25 -0
- package/android/src/main/java/com/apollohg/editor/EditorV2RenderValidation.kt +1 -0
- package/android/src/main/java/com/apollohg/editor/ImageResizeOverlayView.kt +101 -33
- package/android/src/main/java/com/apollohg/editor/RenderMediaSpans.kt +15 -3
- package/android/src/main/java/com/apollohg/editor/RichTextEditorView.kt +14 -0
- package/android/src/main/java/com/apollohg/editor/viewer/AndroidProseLayoutEngine.kt +1 -1
- package/app.plugin.js +42 -39
- package/dist/AtomHost.js +18 -11
- package/dist/EditorAddon.js +4 -2
- package/dist/EditorAddonNormalization.js +17 -9
- package/dist/EditorMentionThemeNormalization.js +6 -3
- package/dist/EditorMentionThemeValidation.js +28 -11
- package/dist/EditorStyleSheetNormalization.js +85 -38
- package/dist/EditorStyleSheetTypes.d.ts +1 -1
- package/dist/EditorTheme.d.ts +4 -0
- package/dist/EditorTheme.js +5 -3
- package/dist/EditorToolbarComponent.d.ts +1 -1
- package/dist/EditorToolbarComponent.js +1 -1
- package/dist/EditorToolbarRegistry.js +5 -5
- package/dist/EditorToolbarTypes.d.ts +12 -12
- package/dist/EditorToolbarVisuals.js +1 -1
- package/dist/ExternalTextComposition.js +89 -43
- package/dist/ImageLoadingPolicy.d.ts +1 -1
- package/dist/NativeEditorBoundaryError.d.ts +7 -7
- package/dist/NativeEditorBoundaryError.js +9 -5
- package/dist/NativeEditorCollaborationTransport.d.ts +7 -7
- package/dist/NativeEditorCollaborationTransport.js +28 -15
- package/dist/NativeEditorCreateConfig.js +11 -6
- package/dist/NativeEditorCreateJson.js +51 -25
- package/dist/NativeEditorCreateValidation.js +11 -6
- package/dist/NativeEditorDocumentBridge.d.ts +1 -1
- package/dist/NativeEditorDocumentBridge.js +32 -18
- package/dist/NativeEditorDocumentHandle.d.ts +4 -4
- package/dist/NativeEditorDocumentHandle.js +1 -1
- package/dist/NativeEditorLocalAwareness.js +25 -13
- package/dist/NativeEditorNativeModule.d.ts +8 -1
- package/dist/NativeEditorNativeModule.js +7 -5
- package/dist/NativeEditorRenderNormalization.js +66 -25
- package/dist/NativeEditorResultNormalization.js +53 -29
- package/dist/NativeEditorTypes.d.ts +10 -10
- package/dist/NativeEditorV2Decimal.js +6 -3
- package/dist/NativeProseViewer.js +11 -3
- package/dist/ResourceLimits.d.ts +2 -2
- package/dist/ResourceLimits.js +7 -2
- package/dist/RichTextEditor.js +1 -1
- package/dist/RichTextEditorNativeTypes.d.ts +1 -1
- package/dist/RichTextEditorNativeView.d.ts +1 -3
- package/dist/RichTextEditorSerialization.js +24 -14
- package/dist/RichTextEditorTypes.d.ts +1 -1
- package/dist/YjsCollaboration.d.ts +7 -7
- package/dist/YjsCollaboration.js +42 -26
- package/dist/addons.d.ts +4 -4
- package/dist/addons.js +3 -3
- package/dist/atomInstances.js +6 -3
- package/dist/atomUpdates.js +7 -4
- package/dist/atoms.js +20 -12
- package/dist/attributeValidation.js +55 -24
- package/dist/contentExpression.js +80 -42
- package/dist/listMarkers.d.ts +1 -1
- package/dist/listMarkers.js +1 -1
- package/dist/schemaDefinition.d.ts +1 -1
- package/dist/schemaDefinition.js +23 -14
- package/dist/schemaDocument.js +1 -1
- package/dist/schemaDocumentConstruction.js +25 -14
- package/dist/schemaNormalization.js +51 -29
- package/dist/schemaPresets.d.ts +1 -1
- package/dist/schemaPresets.js +9 -4
- package/dist/schemaResolution.js +46 -25
- package/dist/specs/PreparedProseViewerNativeComponent.d.ts +4 -0
- package/dist/useEditorToolbarInteractions.d.ts +2 -2
- package/dist/useEditorToolbarInteractions.js +45 -23
- package/dist/useEditorToolbarItems.d.ts +1 -1
- package/dist/useEditorToolbarItems.js +4 -3
- package/dist/useEditorToolbarPresentation.d.ts +3 -3
- package/dist/useEditorToolbarPresentation.js +4 -4
- package/dist/useEditorToolbarState.d.ts +1 -1
- package/dist/useEditorToolbarState.js +1 -1
- package/dist/useFocusPreservingFrames.js +11 -8
- package/dist/useNativeEditor.d.ts +1 -1
- package/dist/useNativeEditor.js +21 -11
- package/dist/useRichTextEditorCommands.d.ts +2 -2
- package/dist/useRichTextEditorCommands.js +59 -29
- package/dist/useRichTextEditorEvents.d.ts +3 -3
- package/dist/useRichTextEditorEvents.js +75 -32
- package/dist/useRichTextEditorMentions.d.ts +4 -4
- package/dist/useRichTextEditorMentions.js +48 -29
- package/dist/useRichTextEditorPresentation.d.ts +4 -4
- package/dist/useRichTextEditorPresentation.js +25 -23
- package/dist/useRichTextEditorState.d.ts +3 -3
- package/dist/useRichTextEditorState.js +20 -15
- package/dist/useRichTextEditorUpdates.d.ts +1 -1
- package/dist/useRichTextEditorUpdates.js +99 -41
- package/dist/useViewerAtoms.js +63 -36
- 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/EditorInteractionOverlays.swift +114 -122
- package/ios/EditorLayoutManager.swift +13 -3
- package/ios/EditorStyleSheet.swift +12 -1
- package/ios/EditorTextView+Images.swift +32 -10
- package/ios/EditorTextView.swift +1 -1
- package/ios/EditorV2Adapter+RenderValidation.swift +2 -2
- package/ios/NativeToolbar+ButtonStyle.swift +17 -5
- package/ios/NativeToolbar+Diagnostics.swift +15 -0
- package/ios/RenderBridge+Atoms.swift +0 -1
- package/ios/RenderBridge+Attachments.swift +0 -29
- package/ios/RichTextEditorView.swift +4 -12
- package/ios/Viewer/CoreTextProseLayoutEngine+AttributedText.swift +3 -2
- package/package.json +11 -3
- package/react-native.config.js +1 -1
- package/rust/android/arm64-v8a/libeditor_core.so +0 -0
- package/rust/android/armeabi-v7a/libeditor_core.so +0 -0
- package/rust/android/x86/libeditor_core.so +0 -0
- package/rust/android/x86_64/libeditor_core.so +0 -0
- package/src/AtomHost.tsx +58 -28
- package/src/EditorAddon.ts +18 -8
- package/src/EditorAddonNormalization.ts +40 -11
- package/src/EditorMentionThemeNormalization.ts +15 -4
- package/src/EditorMentionThemeValidation.ts +51 -14
- package/src/EditorStyleSheet.ts +1 -0
- package/src/EditorStyleSheetNormalization.ts +195 -64
- package/src/EditorStyleSheetTypes.ts +1 -1
- package/src/EditorTheme.ts +13 -5
- package/src/EditorToolbarComponent.tsx +2 -1
- package/src/EditorToolbarRegistry.tsx +23 -13
- package/src/EditorToolbarTypes.tsx +12 -12
- package/src/EditorToolbarVisuals.tsx +6 -3
- package/src/EditorUpdateRevision.ts +1 -0
- package/src/ExternalTextComposition.ts +259 -49
- package/src/ImageLoadingPolicy.ts +3 -1
- package/src/NativeEditorBoundaryError.ts +38 -11
- package/src/NativeEditorCollaborationTransport.ts +108 -27
- package/src/NativeEditorCreateConfig.ts +92 -19
- package/src/NativeEditorCreateJson.ts +163 -26
- package/src/NativeEditorCreateValidation.ts +33 -12
- package/src/NativeEditorDocumentBridge.ts +111 -19
- package/src/NativeEditorDocumentHandle.ts +9 -4
- package/src/NativeEditorLocalAwareness.ts +70 -15
- package/src/NativeEditorNativeModule.ts +13 -9
- package/src/NativeEditorRenderNormalization.ts +128 -29
- package/src/NativeEditorResultNormalization.ts +143 -32
- package/src/NativeEditorTypes.ts +12 -11
- package/src/NativeEditorV2Decimal.ts +12 -3
- package/src/NativeProseViewer.tsx +46 -16
- package/src/ResourceLimits.ts +15 -8
- package/src/RichTextEditor.tsx +3 -1
- package/src/RichTextEditorNativeTypes.tsx +1 -1
- package/src/RichTextEditorNativeView.tsx +2 -8
- package/src/RichTextEditorSerialization.tsx +70 -24
- package/src/RichTextEditorTypes.tsx +1 -1
- package/src/YjsCollaboration.ts +143 -47
- package/src/addons.ts +12 -8
- package/src/atomInstances.ts +25 -5
- package/src/atomUpdates.ts +23 -6
- package/src/atoms.ts +85 -18
- package/src/attributeValidation.ts +97 -33
- package/src/contentExpression.ts +218 -63
- package/src/listMarkers.ts +1 -1
- package/src/schemaDefinition.ts +82 -20
- package/src/schemaDocument.ts +15 -1
- package/src/schemaDocumentConstruction.ts +80 -16
- package/src/schemaNormalization.ts +155 -41
- package/src/schemaPresets.ts +37 -31
- package/src/schemaResolution.ts +126 -27
- package/src/specs/PreparedProseViewerNativeComponent.ts +4 -2
- package/src/useEditorToolbarInteractions.tsx +85 -36
- package/src/useEditorToolbarItems.tsx +50 -22
- package/src/useEditorToolbarPresentation.tsx +81 -58
- package/src/useEditorToolbarState.tsx +8 -6
- package/src/useFocusPreservingFrames.ts +43 -15
- package/src/useNativeEditor.ts +111 -51
- package/src/useRichTextEditorCommands.tsx +127 -54
- package/src/useRichTextEditorEvents.tsx +144 -42
- package/src/useRichTextEditorMentions.tsx +118 -37
- package/src/useRichTextEditorPresentation.tsx +128 -114
- package/src/useRichTextEditorState.tsx +76 -40
- package/src/useRichTextEditorUpdates.tsx +207 -56
- package/src/useViewerAtoms.tsx +162 -66
package/src/YjsCollaboration.ts
CHANGED
|
@@ -20,13 +20,13 @@ import type { RemoteSelectionDecoration } from './NativeRichTextEditor';
|
|
|
20
20
|
/**
|
|
21
21
|
* Transport lifecycle, projected from the Rust transport state.
|
|
22
22
|
*
|
|
23
|
-
* - `idle`
|
|
24
|
-
* - `disconnected`
|
|
25
|
-
* - `connecting`
|
|
26
|
-
* - `handshaking`
|
|
27
|
-
* - `synchronized`
|
|
28
|
-
* - `incompatible`
|
|
29
|
-
* - `destroyed`
|
|
23
|
+
* - `idle` : no transport configured; the handle is detached.
|
|
24
|
+
* - `disconnected` : configured but not currently connected.
|
|
25
|
+
* - `connecting` : opening the physical socket.
|
|
26
|
+
* - `handshaking` : socket open, Yjs sync in progress.
|
|
27
|
+
* - `synchronized` : sync complete; edits flow in both directions.
|
|
28
|
+
* - `incompatible` : the server's document cannot be reconciled with this one.
|
|
29
|
+
* - `destroyed` : the handle was destroyed.
|
|
30
30
|
*/
|
|
31
31
|
export type YjsTransportStatus =
|
|
32
32
|
| 'idle'
|
|
@@ -178,26 +178,37 @@ function asError(value: unknown, fallbackMessage: string): Error {
|
|
|
178
178
|
}
|
|
179
179
|
|
|
180
180
|
function isStrictlyNewerSequence(candidate: string, current: string | null): boolean {
|
|
181
|
-
if (current === null)
|
|
182
|
-
|
|
181
|
+
if (current === null) {
|
|
182
|
+
return true;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (candidate.length !== current.length) {
|
|
186
|
+
return candidate.length > current.length;
|
|
187
|
+
}
|
|
188
|
+
|
|
183
189
|
return candidate > current;
|
|
184
190
|
}
|
|
185
191
|
|
|
186
192
|
function peersToRemoteSelections(
|
|
187
193
|
peers: readonly NativeEditorPeerInfo[]
|
|
188
194
|
): RemoteSelectionDecoration[] {
|
|
189
|
-
return peers.flatMap(
|
|
190
|
-
if (peer.isLocal || peer.cursor == null)
|
|
195
|
+
return peers.flatMap(peer => {
|
|
196
|
+
if (peer.isLocal || peer.cursor == null) {
|
|
197
|
+
return [];
|
|
198
|
+
}
|
|
199
|
+
|
|
191
200
|
const applicationState =
|
|
192
201
|
peer.state && typeof peer.state.state === 'object' && peer.state.state !== null
|
|
193
202
|
? (peer.state.state as Record<string, unknown>)
|
|
194
203
|
: null;
|
|
204
|
+
|
|
195
205
|
const user =
|
|
196
206
|
applicationState &&
|
|
197
207
|
typeof applicationState.user === 'object' &&
|
|
198
208
|
applicationState.user !== null
|
|
199
209
|
? (applicationState.user as Record<string, unknown>)
|
|
200
210
|
: null;
|
|
211
|
+
|
|
201
212
|
return [
|
|
202
213
|
{
|
|
203
214
|
clientId: peer.clientId,
|
|
@@ -221,14 +232,20 @@ function peersToRemoteSelections(
|
|
|
221
232
|
|
|
222
233
|
/**
|
|
223
234
|
* Narrow a caller selection to the text-only cursor awareness carries.
|
|
224
|
-
* Anything else
|
|
235
|
+
* Anything else : a node or all-document selection, or an absent one : is
|
|
225
236
|
* an explicit "no cursor", never a silently retained stale position.
|
|
226
237
|
*/
|
|
227
238
|
function normalizeAwarenessSelection(
|
|
228
239
|
selection: Selection | undefined
|
|
229
240
|
): NativeEditorLocalAwarenessSelection | undefined {
|
|
230
|
-
if (selection === undefined || selection.type !== 'text')
|
|
231
|
-
|
|
241
|
+
if (selection === undefined || selection.type !== 'text') {
|
|
242
|
+
return undefined;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
if (selection.anchor === undefined || selection.head === undefined) {
|
|
246
|
+
return undefined;
|
|
247
|
+
}
|
|
248
|
+
|
|
232
249
|
return createNativeEditorLocalAwarenessSelection(selection.anchor, selection.head);
|
|
233
250
|
}
|
|
234
251
|
|
|
@@ -240,7 +257,10 @@ function normalizeAwarenessSelection(
|
|
|
240
257
|
function copyTransportConfig(
|
|
241
258
|
config: NativeCollaborationTransportConfig | null | undefined
|
|
242
259
|
): NativeCollaborationTransportConfig | null {
|
|
243
|
-
if (config == null)
|
|
260
|
+
if (config == null) {
|
|
261
|
+
return null;
|
|
262
|
+
}
|
|
263
|
+
|
|
244
264
|
return {
|
|
245
265
|
url: config.url,
|
|
246
266
|
connect: config.connect,
|
|
@@ -273,16 +293,19 @@ function mergeAwarenessPartial(
|
|
|
273
293
|
partial: Partial<LocalAwarenessState>
|
|
274
294
|
): NativeEditorLocalAwarenessIntent {
|
|
275
295
|
const state: Record<string, unknown> = { ...base.state };
|
|
296
|
+
|
|
276
297
|
if (partial.user != null) {
|
|
277
298
|
const baseUser =
|
|
278
299
|
state.user != null && typeof state.user === 'object'
|
|
279
300
|
? (state.user as Record<string, unknown>)
|
|
280
301
|
: {};
|
|
302
|
+
|
|
281
303
|
state.user = copyLocalAwarenessUser({
|
|
282
304
|
...baseUser,
|
|
283
305
|
...partial.user,
|
|
284
|
-
}
|
|
306
|
+
});
|
|
285
307
|
}
|
|
308
|
+
|
|
286
309
|
const next: NativeEditorLocalAwarenessIntent = {
|
|
287
310
|
state,
|
|
288
311
|
focused:
|
|
@@ -290,28 +313,40 @@ function mergeAwarenessPartial(
|
|
|
290
313
|
? partial.focused
|
|
291
314
|
: base.focused,
|
|
292
315
|
};
|
|
316
|
+
|
|
293
317
|
// Selection is stated only when the caller states it. Rust holds the
|
|
294
318
|
// local cursor as a sticky index that already tracks every document
|
|
295
|
-
// change, so an omitted key means "retain it"
|
|
319
|
+
// change, so an omitted key means "retain it" : never "resend the last
|
|
296
320
|
// position I happened to see", which the document may have invalidated.
|
|
297
321
|
if ('selection' in partial) {
|
|
298
322
|
next.selection = normalizeAwarenessSelection(partial.selection) ?? null;
|
|
299
323
|
}
|
|
324
|
+
|
|
300
325
|
return next;
|
|
301
326
|
}
|
|
302
327
|
|
|
303
328
|
class YjsCollaborationControllerImpl implements YjsCollaborationController {
|
|
304
329
|
private readonly handle: NativeEditorDocumentHandle;
|
|
330
|
+
|
|
305
331
|
private readonly documentId: string;
|
|
332
|
+
|
|
306
333
|
private readonly callbacks: MutableCallbacks;
|
|
334
|
+
|
|
307
335
|
private transport: NativeCollaborationTransportConfig | null;
|
|
336
|
+
|
|
308
337
|
private removeTransportListener: (() => void) | null = null;
|
|
338
|
+
|
|
309
339
|
private desiredAwareness: NativeEditorLocalAwarenessIntent | null = null;
|
|
340
|
+
|
|
310
341
|
/** Serialized form of the last intent Rust accepted, for dedup. */
|
|
311
342
|
private publishedAwarenessJson: string | null = null;
|
|
343
|
+
|
|
312
344
|
private lastEventSequence: string | null = null;
|
|
345
|
+
|
|
313
346
|
private destroyed = false;
|
|
347
|
+
|
|
314
348
|
private _state: YjsCollaborationState;
|
|
349
|
+
|
|
315
350
|
private _peers: NativeEditorPeerInfo[] = [];
|
|
316
351
|
|
|
317
352
|
constructor(options: YjsCollaborationOptions, callbacks: MutableCallbacks = {}) {
|
|
@@ -321,9 +356,11 @@ class YjsCollaborationControllerImpl implements YjsCollaborationController {
|
|
|
321
356
|
this.callbacks = callbacks;
|
|
322
357
|
this.transport = copyTransportConfig(options.transport);
|
|
323
358
|
this._state = this.readEngineState(this.handle.bridge.getState());
|
|
324
|
-
|
|
359
|
+
|
|
360
|
+
this.removeTransportListener = this.handle.addCollaborationTransportListener(event => {
|
|
325
361
|
this.handleTransportEvent(event);
|
|
326
362
|
});
|
|
363
|
+
|
|
327
364
|
try {
|
|
328
365
|
this.applyLocalAwarenessOption(options.localAwareness);
|
|
329
366
|
this.handle.configureCollaborationTransport(this.transport);
|
|
@@ -355,28 +392,41 @@ class YjsCollaborationControllerImpl implements YjsCollaborationController {
|
|
|
355
392
|
}
|
|
356
393
|
|
|
357
394
|
reconnect(): void {
|
|
358
|
-
if (this.destroyed || this.transport === null)
|
|
395
|
+
if (this.destroyed || this.transport === null) {
|
|
396
|
+
return;
|
|
397
|
+
}
|
|
398
|
+
|
|
359
399
|
this.handle.configureCollaborationTransport({ ...this.transport, connect: false });
|
|
360
400
|
this.transport = { ...this.transport, connect: true };
|
|
361
401
|
this.handle.configureCollaborationTransport(this.transport);
|
|
362
402
|
}
|
|
363
403
|
|
|
364
404
|
destroy(): void {
|
|
365
|
-
if (this.destroyed)
|
|
405
|
+
if (this.destroyed) {
|
|
406
|
+
return;
|
|
407
|
+
}
|
|
408
|
+
|
|
366
409
|
this.destroyed = true;
|
|
367
410
|
this.removeTransportListener?.();
|
|
368
411
|
this.removeTransportListener = null;
|
|
412
|
+
|
|
369
413
|
if (!this.handle.isDestroyed) {
|
|
370
414
|
this.handle.configureCollaborationTransport(null);
|
|
371
415
|
}
|
|
372
416
|
}
|
|
373
417
|
|
|
374
418
|
updateLocalAwareness(partial: Partial<LocalAwarenessState>): void {
|
|
375
|
-
if (this.destroyed)
|
|
419
|
+
if (this.destroyed) {
|
|
420
|
+
return;
|
|
421
|
+
}
|
|
422
|
+
|
|
376
423
|
// Presence exists only while there is a local user. Once awareness
|
|
377
424
|
// is withdrawn, focus and selection updates must not resurrect it
|
|
378
425
|
// as an anonymous entry; only a fresh user re-establishes it.
|
|
379
|
-
if (this.desiredAwareness === null && partial.user == null)
|
|
426
|
+
if (this.desiredAwareness === null && partial.user == null) {
|
|
427
|
+
return;
|
|
428
|
+
}
|
|
429
|
+
|
|
380
430
|
const base = this.desiredAwareness ?? { state: {}, focused: false };
|
|
381
431
|
this.publishAwareness(mergeAwarenessPartial(base, partial));
|
|
382
432
|
}
|
|
@@ -390,7 +440,10 @@ class YjsCollaborationControllerImpl implements YjsCollaborationController {
|
|
|
390
440
|
}
|
|
391
441
|
|
|
392
442
|
applyLocalAwarenessOption(user?: LocalAwarenessUser): void {
|
|
393
|
-
if (this.destroyed)
|
|
443
|
+
if (this.destroyed) {
|
|
444
|
+
return;
|
|
445
|
+
}
|
|
446
|
+
|
|
394
447
|
if (user == null) {
|
|
395
448
|
// Always withdraw: a fresh controller inherits whatever presence
|
|
396
449
|
// a previous one left retained natively on this shared handle.
|
|
@@ -398,13 +451,18 @@ class YjsCollaborationControllerImpl implements YjsCollaborationController {
|
|
|
398
451
|
this.handle.setLocalAwareness(null);
|
|
399
452
|
} catch (error) {
|
|
400
453
|
this.reportError(asError(error, 'Local awareness withdrawal failed'));
|
|
454
|
+
|
|
401
455
|
return;
|
|
402
456
|
}
|
|
457
|
+
|
|
403
458
|
this.desiredAwareness = null;
|
|
404
459
|
this.publishedAwarenessJson = null;
|
|
460
|
+
|
|
405
461
|
return;
|
|
406
462
|
}
|
|
463
|
+
|
|
407
464
|
const current = this.desiredAwareness;
|
|
465
|
+
|
|
408
466
|
// No `selection` key: the Rust-owned cursor carries across a user
|
|
409
467
|
// change untouched.
|
|
410
468
|
this.publishAwareness({
|
|
@@ -417,7 +475,10 @@ class YjsCollaborationControllerImpl implements YjsCollaborationController {
|
|
|
417
475
|
}
|
|
418
476
|
|
|
419
477
|
private setConnectionEnabled(connect: boolean): void {
|
|
420
|
-
if (this.destroyed || this.transport === null)
|
|
478
|
+
if (this.destroyed || this.transport === null) {
|
|
479
|
+
return;
|
|
480
|
+
}
|
|
481
|
+
|
|
421
482
|
this.transport = { ...this.transport, connect };
|
|
422
483
|
this.handle.configureCollaborationTransport(this.transport);
|
|
423
484
|
}
|
|
@@ -429,22 +490,29 @@ class YjsCollaborationControllerImpl implements YjsCollaborationController {
|
|
|
429
490
|
* `desiredAwareness` advances only after native acceptance.
|
|
430
491
|
*
|
|
431
492
|
* Presence is ambient UI state, not user data. A refusal is reported
|
|
432
|
-
* through `onError` and retried on the next change
|
|
493
|
+
* through `onError` and retried on the next change : it never escapes
|
|
433
494
|
* into a host focus, blur, or selection handler, and never fails an
|
|
434
495
|
* edit. The candidate is discarded so state stays consistent with Rust.
|
|
435
496
|
*/
|
|
436
497
|
private publishAwareness(intent: NativeEditorLocalAwarenessIntent): void {
|
|
437
498
|
let intentJson: string;
|
|
499
|
+
|
|
438
500
|
try {
|
|
439
501
|
// Serializing caller-owned application state can itself fail on
|
|
440
502
|
// a cyclic or non-encodable value, so it stays inside the guard.
|
|
441
503
|
intentJson = JSON.stringify(intent);
|
|
442
|
-
|
|
504
|
+
|
|
505
|
+
if (intentJson === this.publishedAwarenessJson) {
|
|
506
|
+
return;
|
|
507
|
+
}
|
|
508
|
+
|
|
443
509
|
this.handle.setLocalAwareness(intent);
|
|
444
510
|
} catch (error) {
|
|
445
511
|
this.reportError(asError(error, 'Local awareness publication failed'));
|
|
512
|
+
|
|
446
513
|
return;
|
|
447
514
|
}
|
|
515
|
+
|
|
448
516
|
this.desiredAwareness = intent;
|
|
449
517
|
this.publishedAwarenessJson = intentJson;
|
|
450
518
|
}
|
|
@@ -461,16 +529,24 @@ class YjsCollaborationControllerImpl implements YjsCollaborationController {
|
|
|
461
529
|
) {
|
|
462
530
|
return;
|
|
463
531
|
}
|
|
532
|
+
|
|
464
533
|
this.lastEventSequence = event.eventSequence;
|
|
534
|
+
|
|
465
535
|
if (event.kind === 'error') {
|
|
466
536
|
const error = event.error ?? new Error('Native collaboration transport failed');
|
|
467
537
|
this.setState({ lastError: error });
|
|
468
538
|
this.callbacks.onError?.(error);
|
|
539
|
+
|
|
469
540
|
return;
|
|
470
541
|
}
|
|
471
|
-
|
|
472
|
-
|
|
542
|
+
|
|
543
|
+
if (event.kind !== 'state') {
|
|
544
|
+
return;
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
this._peers = [ ...event.peers ];
|
|
473
548
|
this.callbacks.onPeersChange?.(this._peers);
|
|
549
|
+
|
|
474
550
|
try {
|
|
475
551
|
this.setState({
|
|
476
552
|
...this.readEngineState(event.state, this._state),
|
|
@@ -488,14 +564,16 @@ class YjsCollaborationControllerImpl implements YjsCollaborationController {
|
|
|
488
564
|
previous?: YjsCollaborationState
|
|
489
565
|
): YjsCollaborationState {
|
|
490
566
|
const awaitingRemote = engineState.documentState === 'AwaitRemote';
|
|
567
|
+
|
|
491
568
|
const documentJson =
|
|
492
569
|
!awaitingRemote &&
|
|
493
570
|
previous?.documentRevision === engineState.documentRevision &&
|
|
494
571
|
previous.documentJson !== null
|
|
495
572
|
? previous.documentJson
|
|
496
573
|
: awaitingRemote
|
|
497
|
-
|
|
498
|
-
|
|
574
|
+
? null
|
|
575
|
+
: this.handle.bridge.getDocumentJson();
|
|
576
|
+
|
|
499
577
|
return {
|
|
500
578
|
documentId: this.documentId,
|
|
501
579
|
status: mapTransportState(engineState.transportState),
|
|
@@ -551,17 +629,20 @@ export function createYjsCollaborationController(
|
|
|
551
629
|
export function useYjsCollaboration(options: YjsCollaborationOptions): UseYjsCollaborationResult {
|
|
552
630
|
_assertNativeEditorDocumentHandle(options.handle);
|
|
553
631
|
const callbacksRef = useRef<MutableCallbacks>({});
|
|
632
|
+
|
|
554
633
|
callbacksRef.current = {
|
|
555
634
|
onPeersChange: options.onPeersChange,
|
|
556
635
|
onStateChange: options.onStateChange,
|
|
557
636
|
onError: options.onError,
|
|
558
637
|
};
|
|
638
|
+
|
|
559
639
|
const controllerRef = useRef<YjsCollaborationControllerImpl | null>(null);
|
|
560
640
|
const localAwarenessKey = localAwarenessDependencyKey(options.localAwareness);
|
|
561
641
|
const transportUrl = options.transport?.url ?? null;
|
|
562
642
|
const transportConnect = options.transport?.connect ?? false;
|
|
563
643
|
const transportProtocolAdapterRef = useRef<NativeCollaborationProtocolAdapter | null>(null);
|
|
564
644
|
transportProtocolAdapterRef.current = options.transport?.protocolAdapter ?? null;
|
|
645
|
+
|
|
565
646
|
const transportProtocolAdapterMetadata = JSON.stringify(
|
|
566
647
|
options.transport?.protocolAdapter == null
|
|
567
648
|
? null
|
|
@@ -571,18 +652,23 @@ export function useYjsCollaboration(options: YjsCollaborationOptions): UseYjsCol
|
|
|
571
652
|
terminalCloseCodes: options.transport.protocolAdapter.terminalCloseCodes,
|
|
572
653
|
}
|
|
573
654
|
);
|
|
655
|
+
|
|
574
656
|
const stableTransportProtocolAdapter = useMemo<NativeCollaborationProtocolAdapter | null>(() => {
|
|
575
657
|
const current = transportProtocolAdapterRef.current;
|
|
576
|
-
|
|
658
|
+
|
|
659
|
+
if (current === null) {
|
|
660
|
+
return null;
|
|
661
|
+
}
|
|
662
|
+
|
|
577
663
|
return {
|
|
578
|
-
protocols: [...current.protocols],
|
|
664
|
+
protocols: [ ...current.protocols ],
|
|
579
665
|
...(current.timeoutMillis === undefined
|
|
580
666
|
? {}
|
|
581
667
|
: { timeoutMillis: current.timeoutMillis }),
|
|
582
668
|
...(current.terminalCloseCodes === undefined
|
|
583
669
|
? {}
|
|
584
|
-
: { terminalCloseCodes: [...current.terminalCloseCodes] }),
|
|
585
|
-
onOpen:
|
|
670
|
+
: { terminalCloseCodes: [ ...current.terminalCloseCodes ] }),
|
|
671
|
+
onOpen: context =>
|
|
586
672
|
transportProtocolAdapterRef.current?.onOpen(context) ?? {
|
|
587
673
|
action: 'reject',
|
|
588
674
|
},
|
|
@@ -594,18 +680,21 @@ export function useYjsCollaboration(options: YjsCollaborationOptions): UseYjsCol
|
|
|
594
680
|
// Static metadata changes require a new native socket descriptor.
|
|
595
681
|
// Callback identity alone is read through the ref and never reconnects.
|
|
596
682
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
597
|
-
}, [transportProtocolAdapterMetadata]);
|
|
598
|
-
|
|
683
|
+
}, [ transportProtocolAdapterMetadata ]);
|
|
684
|
+
|
|
685
|
+
const [ state, setState ] = useState<YjsCollaborationState>({
|
|
599
686
|
documentId: options.documentId,
|
|
600
687
|
status: 'idle',
|
|
601
688
|
isConnected: false,
|
|
602
689
|
documentJson: null,
|
|
603
690
|
documentRevision: null,
|
|
604
691
|
});
|
|
605
|
-
|
|
692
|
+
|
|
693
|
+
const [ peers, setPeers ] = useState<NativeEditorPeerInfo[]>([]);
|
|
606
694
|
|
|
607
695
|
useEffect(() => {
|
|
608
696
|
let controller: YjsCollaborationControllerImpl;
|
|
697
|
+
|
|
609
698
|
try {
|
|
610
699
|
controller = new YjsCollaborationControllerImpl(
|
|
611
700
|
{
|
|
@@ -622,23 +711,25 @@ export function useYjsCollaboration(options: YjsCollaborationOptions): UseYjsCol
|
|
|
622
711
|
},
|
|
623
712
|
},
|
|
624
713
|
{
|
|
625
|
-
onStateChange:
|
|
714
|
+
onStateChange: nextState => {
|
|
626
715
|
setState({ ...nextState });
|
|
627
716
|
callbacksRef.current.onStateChange?.(nextState);
|
|
628
717
|
},
|
|
629
|
-
onPeersChange:
|
|
630
|
-
setPeers([...nextPeers]);
|
|
718
|
+
onPeersChange: nextPeers => {
|
|
719
|
+
setPeers([ ...nextPeers ]);
|
|
631
720
|
callbacksRef.current.onPeersChange?.(nextPeers);
|
|
632
721
|
},
|
|
633
|
-
onError:
|
|
722
|
+
onError: error => callbacksRef.current.onError?.(error),
|
|
634
723
|
}
|
|
635
724
|
);
|
|
725
|
+
|
|
636
726
|
controllerRef.current = controller;
|
|
637
727
|
setState({ ...controller.state });
|
|
638
|
-
setPeers([...controller.peers]);
|
|
728
|
+
setPeers([ ...controller.peers ]);
|
|
639
729
|
} catch (error) {
|
|
640
730
|
const nextError = asError(error, 'Native collaboration initialization failed');
|
|
641
731
|
controllerRef.current = null;
|
|
732
|
+
|
|
642
733
|
setState({
|
|
643
734
|
documentId: options.documentId,
|
|
644
735
|
status: 'idle',
|
|
@@ -647,9 +738,11 @@ export function useYjsCollaboration(options: YjsCollaborationOptions): UseYjsCol
|
|
|
647
738
|
documentRevision: null,
|
|
648
739
|
lastError: nextError,
|
|
649
740
|
});
|
|
741
|
+
|
|
650
742
|
setPeers([]);
|
|
651
743
|
callbacksRef.current.onError?.(nextError);
|
|
652
744
|
}
|
|
745
|
+
|
|
653
746
|
return () => {
|
|
654
747
|
controllerRef.current?.destroy();
|
|
655
748
|
controllerRef.current = null;
|
|
@@ -657,21 +750,24 @@ export function useYjsCollaboration(options: YjsCollaborationOptions): UseYjsCol
|
|
|
657
750
|
// The controller owns one URL/handle binding. Connection intent is
|
|
658
751
|
// updated independently below without recreating the observer.
|
|
659
752
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
660
|
-
}, [options.documentId, options.handle, stableTransportProtocolAdapter, transportUrl]);
|
|
753
|
+
}, [ options.documentId, options.handle, stableTransportProtocolAdapter, transportUrl ]);
|
|
661
754
|
|
|
662
755
|
useEffect(() => {
|
|
663
756
|
controllerRef.current?.applyLocalAwarenessOption(options.localAwareness);
|
|
664
757
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
665
|
-
}, [localAwarenessKey]);
|
|
758
|
+
}, [ localAwarenessKey ]);
|
|
666
759
|
|
|
667
760
|
useEffect(() => {
|
|
668
|
-
if (transportUrl === null)
|
|
761
|
+
if (transportUrl === null) {
|
|
762
|
+
return;
|
|
763
|
+
}
|
|
764
|
+
|
|
669
765
|
if (transportConnect) {
|
|
670
766
|
controllerRef.current?.connect();
|
|
671
767
|
} else {
|
|
672
768
|
controllerRef.current?.disconnect();
|
|
673
769
|
}
|
|
674
|
-
}, [transportConnect, stableTransportProtocolAdapter, transportUrl]);
|
|
770
|
+
}, [ transportConnect, stableTransportProtocolAdapter, transportUrl ]);
|
|
675
771
|
|
|
676
772
|
return {
|
|
677
773
|
state,
|
|
@@ -680,7 +776,7 @@ export function useYjsCollaboration(options: YjsCollaborationOptions): UseYjsCol
|
|
|
680
776
|
connect: () => controllerRef.current?.connect(),
|
|
681
777
|
disconnect: () => controllerRef.current?.disconnect(),
|
|
682
778
|
reconnect: () => controllerRef.current?.reconnect(),
|
|
683
|
-
updateLocalAwareness:
|
|
779
|
+
updateLocalAwareness: partial => controllerRef.current?.updateLocalAwareness(partial),
|
|
684
780
|
editorBindings: {
|
|
685
781
|
documentHandle: options.handle,
|
|
686
782
|
documentRevision: state.documentRevision,
|
package/src/addons.ts
CHANGED
|
@@ -40,7 +40,7 @@ export interface MentionQueryChangeEvent {
|
|
|
40
40
|
anchor: number;
|
|
41
41
|
head: number;
|
|
42
42
|
};
|
|
43
|
-
/** False once the query closed
|
|
43
|
+
/** False once the query closed : supply an empty suggestion list. */
|
|
44
44
|
isActive: boolean;
|
|
45
45
|
/** Engine document revision the query was computed against. */
|
|
46
46
|
documentVersion?: string;
|
|
@@ -64,7 +64,7 @@ export interface MentionSelectEvent {
|
|
|
64
64
|
export interface MentionSelectionAttrsEvent {
|
|
65
65
|
trigger: string;
|
|
66
66
|
suggestion: MentionSuggestion;
|
|
67
|
-
/** Attributes resolved so far
|
|
67
|
+
/** Attributes resolved so far : the suggestion's own, plus `label` and `mentionSuggestionChar`. */
|
|
68
68
|
attrs: Record<string, unknown>;
|
|
69
69
|
/** Mark attributes active at the insertion point. */
|
|
70
70
|
markAttrs: ReadonlyActiveState['markAttrs'];
|
|
@@ -83,7 +83,7 @@ export type MentionThemeResolveEvent = MentionSelectionAttrsEvent;
|
|
|
83
83
|
/**
|
|
84
84
|
* Mentions for `RichTextEditor`. The schema belongs to the document
|
|
85
85
|
* handle, so create the handle with {@link withMentionsSchema} applied to
|
|
86
|
-
* your schema
|
|
86
|
+
* your schema : this config alone does not add the `mention` node.
|
|
87
87
|
* (`RichTextViewer` adds it for you.)
|
|
88
88
|
*
|
|
89
89
|
* The host owns the suggestion list: react to `onQueryChange` by filtering
|
|
@@ -218,14 +218,15 @@ export function mentionNodeSpec(): NodeSpec {
|
|
|
218
218
|
* ```
|
|
219
219
|
*/
|
|
220
220
|
export function withMentionsSchema(schema: SchemaDefinition): SchemaDefinition {
|
|
221
|
-
const hasMentionNode = schema.nodes.some(
|
|
221
|
+
const hasMentionNode = schema.nodes.some(node => node.name === MENTION_NODE_NAME);
|
|
222
|
+
|
|
222
223
|
if (hasMentionNode) {
|
|
223
224
|
return schema;
|
|
224
225
|
}
|
|
225
226
|
|
|
226
227
|
return {
|
|
227
228
|
...schema,
|
|
228
|
-
nodes: [...schema.nodes, mentionNodeSpec()],
|
|
229
|
+
nodes: [ ...schema.nodes, mentionNodeSpec() ],
|
|
229
230
|
};
|
|
230
231
|
}
|
|
231
232
|
|
|
@@ -237,8 +238,10 @@ export function normalizeNativeEditorAddons(
|
|
|
237
238
|
}
|
|
238
239
|
|
|
239
240
|
const trigger = addons.mentions.trigger?.trim() || DEFAULT_MENTION_TRIGGER;
|
|
240
|
-
|
|
241
|
+
|
|
242
|
+
const suggestions = (addons.mentions.suggestions ?? []).map(suggestion => {
|
|
241
243
|
const label = suggestion.label?.trim() || suggestion.title;
|
|
244
|
+
|
|
242
245
|
const attrs = {
|
|
243
246
|
label,
|
|
244
247
|
mentionSuggestionChar: trigger,
|
|
@@ -276,6 +279,7 @@ export function serializeNormalizedEditorAddons(
|
|
|
276
279
|
addons?: NormalizedEditorAddons
|
|
277
280
|
): string | undefined {
|
|
278
281
|
const normalized = normalizeNativeEditorAddons(addons);
|
|
282
|
+
|
|
279
283
|
return normalized ? JSON.stringify(normalized) : undefined;
|
|
280
284
|
}
|
|
281
285
|
|
|
@@ -288,7 +292,7 @@ export interface MentionFragmentOptions {
|
|
|
288
292
|
/**
|
|
289
293
|
* Build a document fragment holding one mention node, ready for
|
|
290
294
|
* `insertContentJson`. Use it to insert a mention outside the suggestion
|
|
291
|
-
* flow
|
|
295
|
+
* flow : from a picker, say.
|
|
292
296
|
*
|
|
293
297
|
* @param attrs Attributes for the mention node, e.g. `{ label, id }`.
|
|
294
298
|
* @param descriptor Document node name to wrap the fragment in. Defaults to `doc`.
|
|
@@ -304,7 +308,7 @@ export function buildMentionFragmentJson(
|
|
|
304
308
|
type: MENTION_NODE_NAME,
|
|
305
309
|
attrs,
|
|
306
310
|
},
|
|
307
|
-
...(options?.trailingSpace ? [{ type: 'text', text: ' ' }] : []),
|
|
311
|
+
...(options?.trailingSpace ? [ { type: 'text', text: ' ' } ] : []),
|
|
308
312
|
],
|
|
309
313
|
descriptor
|
|
310
314
|
);
|
package/src/atomInstances.ts
CHANGED
|
@@ -56,8 +56,10 @@ export function collectAtomInstanceBlocks(
|
|
|
56
56
|
const instanceBlocks: AtomInstance[][] = [];
|
|
57
57
|
const instances: AtomInstance[] = [];
|
|
58
58
|
let hasOnlyStableKeys = true;
|
|
59
|
+
|
|
59
60
|
for (const block of renderBlocks) {
|
|
60
61
|
const blockInstances: AtomInstance[] = [];
|
|
62
|
+
|
|
61
63
|
for (const element of block) {
|
|
62
64
|
if (
|
|
63
65
|
element.type !== 'voidBlock' ||
|
|
@@ -66,28 +68,35 @@ export function collectAtomInstanceBlocks(
|
|
|
66
68
|
) {
|
|
67
69
|
continue;
|
|
68
70
|
}
|
|
71
|
+
|
|
69
72
|
const occurrence = occurrences.get(element.nodeType) ?? 0;
|
|
70
73
|
occurrences.set(element.nodeType, occurrence + 1);
|
|
74
|
+
|
|
71
75
|
if (
|
|
72
76
|
!registeredTypes.has(element.nodeType) &&
|
|
73
77
|
NATIVE_VOID_BLOCK_TYPES.has(element.nodeType)
|
|
74
78
|
) {
|
|
75
79
|
continue;
|
|
76
80
|
}
|
|
81
|
+
|
|
77
82
|
const hasStableKey = typeof element.atomId === 'string';
|
|
83
|
+
|
|
78
84
|
const instance = {
|
|
79
|
-
key: hasStableKey ? element.atomId
|
|
85
|
+
key: hasStableKey ? element.atomId : `${element.nodeType}:${occurrence}`,
|
|
80
86
|
hasStableKey,
|
|
81
87
|
nodeType: element.nodeType,
|
|
82
88
|
attrs: element.attrs ?? EMPTY_ATOM_ATTRS,
|
|
83
89
|
docPos: element.docPos,
|
|
84
90
|
};
|
|
91
|
+
|
|
85
92
|
hasOnlyStableKeys &&= hasStableKey;
|
|
86
93
|
blockInstances.push(instance);
|
|
87
94
|
instances.push(instance);
|
|
88
95
|
}
|
|
96
|
+
|
|
89
97
|
instanceBlocks.push(blockInstances);
|
|
90
98
|
}
|
|
99
|
+
|
|
91
100
|
return { instanceBlocks, instances, hasOnlyStableKeys };
|
|
92
101
|
}
|
|
93
102
|
|
|
@@ -114,10 +123,21 @@ export function applyRenderPatch<Element>(
|
|
|
114
123
|
}
|
|
115
124
|
|
|
116
125
|
export function atomSelected(selection: Selection, docPos: number): boolean {
|
|
117
|
-
if (selection.type === 'all')
|
|
118
|
-
|
|
126
|
+
if (selection.type === 'all') {
|
|
127
|
+
return true;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (selection.type === 'node') {
|
|
131
|
+
return selection.pos === docPos;
|
|
132
|
+
}
|
|
133
|
+
|
|
119
134
|
const { anchor, head } = selection;
|
|
120
|
-
|
|
121
|
-
|
|
135
|
+
|
|
136
|
+
if (anchor == null || head == null) {
|
|
137
|
+
return false;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const [ from, to ] = anchor <= head ? [ anchor, head ] : [ head, anchor ];
|
|
141
|
+
|
|
122
142
|
return from <= docPos && docPos + 1 <= to;
|
|
123
143
|
}
|