Skip to content

Commit 86d85e2

Browse files
authored
Merge pull request #4714 from baptisteArno/shared-type-yjs
2 parents 46950b4 + a846f33 commit 86d85e2

File tree

5 files changed

+122
-12
lines changed

5 files changed

+122
-12
lines changed

.changeset/odd-dancers-explain.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@platejs/yjs': minor
3+
---
4+
5+
Add sharedType option

packages/yjs/README.md

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,72 @@ This lets you create robust setups like:
206206

207207
By default, content will be shown as soon as at least one provider is synced. If `waitForAllProviders` is set to `true`, content will only appear when all configured providers are in sync.
208208

209+
### Using Nested Y.Doc Structures
210+
211+
By default, the plugin uses `ydoc.get('content', Y.XmlText)` for storing editor content. However, you can use the `sharedType` option to work with nested structures from a parent Y.Doc:
212+
213+
```typescript
214+
import { YjsPlugin } from '@platejs/yjs';
215+
import * as Y from 'yjs';
216+
217+
// Create a parent document with multiple nested editors
218+
const parentDoc = new Y.Doc();
219+
220+
// Create nested structures for different editors
221+
const editorsMap = parentDoc.getMap('editors');
222+
const mainEditorContent = new Y.XmlText();
223+
const sidebarEditorContent = new Y.XmlText();
224+
225+
// Store them in the parent doc
226+
editorsMap.set('main', mainEditorContent);
227+
editorsMap.set('sidebar', sidebarEditorContent);
228+
229+
// You can also store other data in the parent doc
230+
const metadata = parentDoc.getMap('metadata');
231+
metadata.set('title', 'My Document');
232+
metadata.set('author', 'John Doe');
233+
metadata.set('createdAt', Date.now());
234+
235+
// Create the main editor with the nested shared type
236+
const mainEditor = createPlateEditor({
237+
plugins: [
238+
YjsPlugin.configure({
239+
ydoc: parentDoc, // Pass the parent doc for provider sync
240+
sharedType: mainEditorContent, // Use the specific nested content
241+
providers: [
242+
{
243+
type: 'webrtc',
244+
options: { roomName: 'my-document' },
245+
},
246+
],
247+
}),
248+
],
249+
});
250+
251+
// Create a sidebar editor with its own nested shared type
252+
const sidebarEditor = createPlateEditor({
253+
plugins: [
254+
YjsPlugin.configure({
255+
ydoc: parentDoc, // Same parent doc
256+
sharedType: sidebarEditorContent, // Different nested content
257+
providers: [], // Don't create new providers, parent is already synced
258+
}),
259+
],
260+
});
261+
262+
// Initialize with initial values - they'll be applied to the correct sharedTypes
263+
await mainEditor.api.yjs.init({
264+
autoConnect: true,
265+
value: [{ type: 'p', children: [{ text: 'Main editor content' }] }],
266+
});
267+
await sidebarEditor.api.yjs.init({
268+
autoConnect: false,
269+
value: [{ type: 'p', children: [{ text: 'Sidebar content' }] }],
270+
});
271+
```
272+
273+
**Important:** When using a custom `sharedType`, the initial `value` passed to `init()` will be applied directly to that specific `sharedType`, not to the default `ydoc.get('content', Y.XmlText)`. This ensures each editor's initial content goes to the correct location in your nested structure.
274+
209275
### Cursor Support
210276

211277
Collaborative cursors are enabled by default. You can customize their appearance and behavior:

packages/yjs/src/lib/BaseYjsPlugin.ts

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { YjsEditor } from '@slate-yjs/core';
1+
import { slateNodesToInsertDelta, YjsEditor } from '@slate-yjs/core';
22
import {
33
type InitOptions,
44
type Value,
@@ -42,6 +42,7 @@ export const BaseYjsPlugin = createTSlatePlugin<YjsConfig>({
4242
localOrigin: null,
4343
positionStorageOrigin: null,
4444
providers: [],
45+
sharedType: null,
4546
ydoc: null!,
4647
onConnect: () => {},
4748
onDisconnect: () => {},
@@ -196,6 +197,7 @@ export const BaseYjsPlugin = createTSlatePlugin<YjsConfig>({
196197
_providers,
197198
awareness,
198199
providers: providerConfigsOrInstances = [],
200+
sharedType: customSharedType,
199201
ydoc,
200202
} = options;
201203

@@ -221,13 +223,23 @@ export const BaseYjsPlugin = createTSlatePlugin<YjsConfig>({
221223
initialNodes = editor.api.create.value();
222224
}
223225

224-
const initialDelta = await slateToDeterministicYjsState(
225-
id ?? editor.id,
226-
initialNodes
227-
);
228-
ydoc.transact(() => {
229-
Y.applyUpdate(ydoc, initialDelta);
230-
});
226+
// Use custom sharedType if provided, otherwise use default 'content'
227+
if (customSharedType) {
228+
// Apply initial value directly to the custom shared type
229+
const delta = slateNodesToInsertDelta(initialNodes);
230+
ydoc.transact(() => {
231+
customSharedType.applyDelta(delta);
232+
});
233+
} else {
234+
// Use deterministic state for default 'content' key
235+
const initialDelta = await slateToDeterministicYjsState(
236+
id ?? editor.id,
237+
initialNodes
238+
);
239+
ydoc.transact(() => {
240+
Y.applyUpdate(ydoc, initialDelta);
241+
});
242+
}
231243
}
232244

233245
// Final providers array that will contain both configured and custom providers

packages/yjs/src/lib/providers/types.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,26 @@ export type YjsConfig = PluginConfig<
118118
* specified. Passed to `withTCursors`.
119119
*/
120120
cursors?: WithCursorsOptions | null;
121+
/**
122+
* Custom shared type to use for the editor content. If provided, this will
123+
* be used instead of the default `ydoc.get('content', Y.XmlText)`. This
124+
* allows you to use a nested Y.XmlText from a parent Y.Doc structure.
125+
*
126+
* @example
127+
* ```ts
128+
* const parentDoc = new Y.Doc();
129+
* const editorContent = parentDoc
130+
* .getMap('editors')
131+
* .get('main', Y.XmlText);
132+
*
133+
* YjsPlugin.configure({
134+
* ydoc: parentDoc,
135+
* sharedType: editorContent,
136+
* // ...
137+
* });
138+
* ```;
139+
*/
140+
sharedType?: Y.XmlText | null;
121141
/**
122142
* Shared Y.Doc instance. If not provided by the user in the initial config,
123143
* a new one will be created and assigned here by the plugin.

packages/yjs/src/lib/withPlateYjs.ts

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,18 @@ export const withPlateYjs: ExtendEditor<YjsConfig> = ({
1616
SlateEditor &
1717
YjsEditorProps;
1818

19-
const { awareness, cursors, localOrigin, positionStorageOrigin, ydoc } =
20-
getOptions();
19+
const {
20+
awareness,
21+
cursors,
22+
localOrigin,
23+
positionStorageOrigin,
24+
sharedType: customSharedType,
25+
ydoc,
26+
} = getOptions();
2127

22-
// Get the shared document type from the Y.Doc
23-
const sharedType = ydoc!.get('content', Y.XmlText) as Y.XmlText;
28+
// Use custom shared type if provided, otherwise get the default from Y.Doc
29+
const sharedType =
30+
customSharedType ?? (ydoc!.get('content', Y.XmlText) as Y.XmlText);
2431

2532
// Apply core Yjs binding first
2633
editor = withTYjs(editor, sharedType, {

0 commit comments

Comments
 (0)