marshall-y-slate
Version:
Yjs bindings for Slate.
215 lines (187 loc) • 5.47 kB
text/typescript
import { Descendant, Editor, Operation } from 'slate';
import invariant from 'tiny-invariant';
import * as Y from 'yjs';
import { applyYjsEvents } from '../apply-to-slate';
import applySlateOps from '../apply-to-yjs';
import { SharedType } from '../model';
import { toSlateDoc } from '../utils/convert';
const IS_REMOTE: WeakSet<Editor> = new WeakSet();
const IS_LOCAL: WeakSet<Editor> = new WeakSet();
const IS_UNDO: WeakSet<Editor> = new WeakSet();
const SHARED_TYPES: WeakMap<Editor, SharedType> = new WeakMap();
export interface YjsEditor extends Editor {
sharedType: SharedType;
}
export const YjsEditor = {
/**
* Set the editor value to the content of the to the editor bound shared type.
*/
synchronizeValue: (e: YjsEditor): void => {
Editor.withoutNormalizing(e, () => {
e.children = toSlateDoc(e.sharedType);
e.onChange();
});
},
/**
* Returns whether the editor currently is applying remote changes.
*/
sharedType: (editor: YjsEditor): SharedType => {
const sharedType = SHARED_TYPES.get(editor);
invariant(sharedType, 'YjsEditor without attached shared type');
return sharedType;
},
/**
* Applies a slate operations to the bound shared type.
*/
applySlateOperations: (editor: YjsEditor, operations: Operation[]): void => {
YjsEditor.asLocal(editor, () => {
try {
applySlateOps(YjsEditor.sharedType(editor), operations, editor);
} catch (error) {
const e: YjsEditor & {
onError: (errorData: {
code?: number,
name?: string,
nativeError?: any,
data?: Descendant[]
}) => void
} = editor as any;
if (e.onError) {
e.onError({ code: 10000, name: 'apply local operations', nativeError: error });
}
}
});
},
/**
* Returns whether the editor currently is applying remote changes.
*/
isRemote: (editor: YjsEditor): boolean => {
return IS_REMOTE.has(editor);
},
/**
* Performs an action as a remote operation.
*/
asRemote: (editor: YjsEditor, fn: () => void): void => {
const wasRemote = YjsEditor.isRemote(editor);
IS_REMOTE.add(editor);
fn();
if (!wasRemote) {
Promise.resolve().then(() => IS_REMOTE.delete(editor));
}
},
/**
* Returns whether the editor currently is applying remote changes.
*/
isUndo: (editor: YjsEditor): boolean => {
return IS_UNDO.has(editor);
},
/**
* Performs an action as a remote operation.
*/
asUndo: (editor: YjsEditor, fn: () => void): void => {
const wasUndo = YjsEditor.isUndo(editor);
IS_UNDO.add(editor);
fn();
if (!wasUndo) {
Promise.resolve().then(() => IS_UNDO.delete(editor));
}
},
/**
* Apply Yjs events to slate
*/
applyYjsEvents: (editor: YjsEditor, events: Y.YEvent[]): void => {
if (YjsEditor.isUndo(editor)) {
try {
applyYjsEvents(editor, events);
} catch (error) {
const e: YjsEditor & {
onError: (errorData: {
code?: number,
name?: string,
nativeError?: any,
data?: Descendant[]
}) => void
} = editor as any;
if (e.onError) {
e.onError({ code: 10001, name: 'apply yjs undo events', nativeError: error });
}
}
} else {
YjsEditor.asRemote(editor, () => {
try {
applyYjsEvents(editor, events);
} catch (error) {
const e: YjsEditor & {
onError: (errorData: {
code?: number,
name?: string,
nativeError?: any,
data?: Descendant[]
}) => void
} = editor as any;
if (e.onError) {
e.onError({ code: 10002, name: 'apply yjs remote events', nativeError: error });
}
}
});
}
},
/**
* Performs an action as a local operation.
*/
asLocal: (editor: YjsEditor, fn: () => void): void => {
const wasLocal = YjsEditor.isLocal(editor);
IS_LOCAL.add(editor);
fn();
if (!wasLocal) {
IS_LOCAL.delete(editor);
}
},
/**
* Returns whether the editor currently is applying a remote change to the yjs doc.
*/
isLocal: (editor: YjsEditor): boolean => {
return IS_LOCAL.has(editor);
}
};
export function withYjs<T extends Editor>(
editor: T,
sharedType: SharedType,
{ isSynchronizeValue = true }: WithYjsOptions = {}
): T & YjsEditor {
const e = editor as T & YjsEditor;
let isInitialized = false;
e.sharedType = sharedType;
SHARED_TYPES.set(editor, sharedType);
if (isSynchronizeValue) {
setTimeout(() => {
YjsEditor.synchronizeValue(e);
isInitialized = true;
});
}
sharedType.observeDeep((events) => {
if (!YjsEditor.isLocal(e)) {
const isNormalizing = Editor.isNormalizing(editor);
Editor.setNormalizing(e, false);
if (!isInitialized) {
e.children = e.sharedType.toJSON();
e.onChange();
isInitialized = true;
} else {
YjsEditor.applyYjsEvents(e, events);
}
Editor.setNormalizing(e, isNormalizing);
}
});
const { onChange } = editor;
e.onChange = () => {
if (!YjsEditor.isRemote(e) && !YjsEditor.isUndo(e) && isInitialized) {
YjsEditor.applySlateOperations(e, e.operations);
}
onChange();
};
return e;
}
export type WithYjsOptions = {
isSynchronizeValue?: boolean;
};