Skip to content

Commit

Permalink
Sync Editor React state with Yjs.
Browse files Browse the repository at this point in the history
I hooked Yjs to the React editor state in the /playground. When a editor block changes, it currently overwrites the complete block content, instead of applying the differences. This is basically the same syncing approach as described in WordPress#17964, therefore it should allow for a fair comparison. But Yjs also allows to apply differences to the text object and is better suited to enable multiple users to work on the same paragraph.
  • Loading branch information
dmonad committed Nov 7, 2019
1 parent 497b42b commit 9895d09
Show file tree
Hide file tree
Showing 3 changed files with 126 additions and 7 deletions.
45 changes: 43 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,9 @@
"@wordpress/token-list": "file:packages/token-list",
"@wordpress/url": "file:packages/url",
"@wordpress/viewport": "file:packages/viewport",
"@wordpress/wordcount": "file:packages/wordcount"
"@wordpress/wordcount": "file:packages/wordcount",
"y-websocket": "1.0.6",
"yjs": "13.0.0-102"
},
"devDependencies": {
"@actions/core": "1.0.0",
Expand Down
84 changes: 80 additions & 4 deletions playground/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
*/
import '@wordpress/editor'; // This shouldn't be necessary

import { render, useState, Fragment } from '@wordpress/element';
import { render, useState, useEffect, Fragment } from '@wordpress/element';
import {
BlockEditorKeyboardShortcuts,
BlockEditorProvider,
Expand Down Expand Up @@ -35,8 +35,84 @@ import '@wordpress/block-library/build-style/theme.css';
import '@wordpress/format-library/build-style/style.css';
/* eslint-enable no-restricted-syntax */

/**
* External dependencies
*/
import * as Y from 'yjs';
import { WebsocketProvider } from 'y-websocket';

import _ from 'lodash';

// ydoc is the CRDT state
const ydoc = new Y.Doc();
// Connect ydoc to other clients via the Websocket provider.
const provider = new WebsocketProvider( `${ window.location.protocol === 'http:' ? 'ws:' : 'wss:' }//yjs-demos.now.sh`, 'gutenberg', ydoc );
// Define a shared type on ydoc.
// A shared type works like any other data type - but it fires synchronous
// events when it changes and is automatically synced with other clients.
const yContent = ydoc.getArray( 'gutenberg-content' );

window.debugY = {
ydoc, provider, yContent,
};

/**
* Create a diff between two editor states.
*
* @template T
* @param {Array<T>} a The old state
* @param {Array<T>} b The updated state
* @return {{index:number,remove:number,insert:Array<T>}} The diff description.
*/
const simpleDiff = ( a, b ) => {
let left = 0; // number of same characters counting from left
let right = 0; // number of same characters counting from right
while ( left < a.length && left < b.length && _.isEqual( a[ left ], b[ left ] ) ) {
left++;
}
if ( left !== a.length || left !== b.length ) {
// Only check right if a !== b
while ( right + left < a.length && right + left < b.length && _.isEqual( a[ a.length - right - 1 ], b[ b.length - right - 1 ] ) ) {
right++;
}
}
return {
index: left,
remove: a.length - left - right,
insert: b.slice( left, b.length - right ),
};
};

function App() {
const [ blocks, updateBlocks ] = useState( [] );
const [ blocks, updateBlocks ] = useState( yContent.toArray() );
useEffect( () => {
// Update react state before observer is registered.
const timeout = setTimeout( () => {
updateBlocks( yContent.toArray() );
}, 0 );
// Register type observer
const observer = ( ) => {
// update react state
updateBlocks( yContent.toArray() );
};
yContent.observeDeep( observer );
return () => {
yContent.unobserveDeep( observer );
clearTimeout( timeout );
};
} );
const updateYjsType = ( newEditorContent ) => {
// Use a very basic diff approach to calculate the differences
const currentContent = yContent.toArray();
const d = simpleDiff( currentContent, newEditorContent );
// Bundle all changes as a single transaction
// This transaction will trigger the observer call, which will
// trigger `updateBlocks`.
ydoc.transact( () => {
yContent.delete( d.index, d.remove );
yContent.insert( d.index, d.insert );
} );
};

return (
<Fragment>
Expand All @@ -51,8 +127,8 @@ function App() {
<DropZoneProvider>
<BlockEditorProvider
value={ blocks }
onInput={ updateBlocks }
onChange={ updateBlocks }
onInput={ updateYjsType }
onChange={ updateYjsType }
>
<div className="playground__sidebar">
<BlockInspector />
Expand Down

0 comments on commit 9895d09

Please sign in to comment.