UNPKG

@portabletext/editor

Version:

Portable Text Editor made in React

279 lines (256 loc) 8.03 kB
import {insert, setIfMissing, unset, type Patch} from '@portabletext/patches' import {Editor, type Descendant, type Operation} from 'slate' import {createApplyPatch} from '../../internal-utils/applyPatch' import {debugWithName} from '../../internal-utils/debug' import { insertNodePatch, insertTextPatch, mergeNodePatch, moveNodePatch, removeNodePatch, removeTextPatch, setNodePatch, splitNodePatch, } from '../../internal-utils/operation-to-patches' import {fromSlateValue, isEqualToEmptyEditor} from '../../internal-utils/values' import { IS_PROCESSING_REMOTE_CHANGES, KEY_TO_VALUE_ELEMENT, } from '../../internal-utils/weakMaps' import {withRemoteChanges} from '../../internal-utils/withChanges' import { isPatching, PATCHING, withoutPatching, } from '../../internal-utils/withoutPatching' import type {PortableTextSlateEditor} from '../../types/editor' import type {EditorActor} from '../editor-machine' import type {RelayActor} from '../relay-machine' import {getCurrentOperationId} from '../with-applying-behavior-operations' import {withoutSaving} from './createWithUndoRedo' const debug = debugWithName('plugin:withPatches') const debugVerbose = false interface Options { editorActor: EditorActor relayActor: RelayActor subscriptions: Array<() => () => void> } export function createWithPatches({ editorActor, relayActor, subscriptions, }: Options): (editor: PortableTextSlateEditor) => PortableTextSlateEditor { // The previous editor children are needed to figure out the _key of deleted nodes // The editor.children would no longer contain that information if the node is already deleted. let previousChildren: Descendant[] const applyPatch = createApplyPatch(editorActor.getSnapshot().context.schema) return function withPatches(editor: PortableTextSlateEditor) { IS_PROCESSING_REMOTE_CHANGES.set(editor, false) PATCHING.set(editor, true) previousChildren = [...editor.children] const {apply} = editor let bufferedPatches: Patch[] = [] const handleBufferedRemotePatches = () => { if (bufferedPatches.length === 0) { return } const patches = bufferedPatches bufferedPatches = [] let changed = false withRemoteChanges(editor, () => { Editor.withoutNormalizing(editor, () => { withoutPatching(editor, () => { withoutSaving(editor, () => { for (const patch of patches) { if (debug.enabled) debug(`Handling remote patch ${JSON.stringify(patch)}`) try { changed = applyPatch(editor, patch) } catch (error) { console.error( `Applying patch ${JSON.stringify(patch)} failed due to: ${error.message}`, ) } } }) }) }) if (changed) { editor.normalize() editor.onChange() } }) } const handlePatches = ({patches}: {patches: Patch[]}) => { const remotePatches = patches.filter((p) => p.origin !== 'local') if (remotePatches.length === 0) { return } bufferedPatches = bufferedPatches.concat(remotePatches) handleBufferedRemotePatches() } subscriptions.push(() => { debug('Subscribing to remote patches') const sub = editorActor.on('patches', handlePatches) return () => { debug('Unsubscribing to remote patches') sub.unsubscribe() } }) editor.apply = (operation: Operation): void | Editor => { let patches: Patch[] = [] // Update previous children here before we apply previousChildren = editor.children const editorWasEmpty = isEqualToEmptyEditor( previousChildren, editorActor.getSnapshot().context.schema, ) // Apply the operation apply(operation) const editorIsEmpty = isEqualToEmptyEditor( editor.children, editorActor.getSnapshot().context.schema, ) if (!isPatching(editor)) { if (debugVerbose && debug.enabled) debug( `Editor is not producing patch for operation ${operation.type}`, operation, ) return editor } // If the editor was empty and now isn't, insert the placeholder into it. if ( editorWasEmpty && !editorIsEmpty && operation.type !== 'set_selection' ) { patches.push(insert(previousChildren, 'before', [0])) } switch (operation.type) { case 'insert_text': patches = [ ...patches, ...insertTextPatch( editorActor.getSnapshot().context.schema, editor.children, operation, previousChildren, ), ] break case 'remove_text': patches = [ ...patches, ...removeTextPatch( editorActor.getSnapshot().context.schema, editor.children, operation, previousChildren, ), ] break case 'remove_node': patches = [ ...patches, ...removeNodePatch( editorActor.getSnapshot().context.schema, previousChildren, operation, ), ] break case 'split_node': patches = [ ...patches, ...splitNodePatch( editorActor.getSnapshot().context.schema, editor.children, operation, previousChildren, ), ] break case 'insert_node': patches = [ ...patches, ...insertNodePatch( editorActor.getSnapshot().context.schema, editor.children, operation, previousChildren, ), ] break case 'set_node': patches = [ ...patches, ...setNodePatch( editorActor.getSnapshot().context.schema, editor.children, operation, ), ] break case 'merge_node': patches = [ ...patches, ...mergeNodePatch( editorActor.getSnapshot().context.schema, editor.children, operation, previousChildren, ), ] break case 'move_node': patches = [ ...patches, ...moveNodePatch( editorActor.getSnapshot().context.schema, previousChildren, operation, ), ] break default: // Do nothing } // Unset the value if a operation made the editor empty if ( !editorWasEmpty && editorIsEmpty && ['merge_node', 'set_node', 'remove_text', 'remove_node'].includes( operation.type, ) ) { patches = [...patches, unset([])] relayActor.send({ type: 'unset', previousValue: fromSlateValue( previousChildren, editorActor.getSnapshot().context.schema.block.name, KEY_TO_VALUE_ELEMENT.get(editor), ), }) } // Prepend patches with setIfMissing if going from empty editor to something involving a patch. if (editorWasEmpty && patches.length > 0) { patches = [setIfMissing([], []), ...patches] } // Emit all patches if (patches.length > 0) { for (const patch of patches) { editorActor.send({ type: 'internal.patch', patch: {...patch, origin: 'local'}, operationId: getCurrentOperationId(editor), value: editor.value, }) } } return editor } return editor } }