UNPKG

marshall-y-slate

Version:
215 lines (187 loc) 5.47 kB
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; };