UNPKG

@portabletext/editor

Version:

Portable Text Editor made in React

147 lines (126 loc) 4.46 kB
import {isEqual} from 'lodash' import {Editor, Node, Path, Transforms} from 'slate' import type {SlateTextBlock, VoidElement} from '../types/slate' import type {BehaviorActionImplementation} from './behavior.actions' export const splitBlockActionImplementation: BehaviorActionImplementation< 'split.block' > = ({context, action}) => { const keyGenerator = context.keyGenerator const schema = context.schema const editor = action.editor if (!editor.selection) { return } const anchorBlockPath = editor.selection.anchor.path.slice(0, 1) const focusBlockPath = editor.selection.focus.path.slice(0, 1) const focusBlock = Node.descendant(editor, focusBlockPath) as | SlateTextBlock | VoidElement if (editor.isTextBlock(focusBlock)) { const selectionAcrossBlocks = anchorBlockPath[0] !== focusBlockPath[0] if (!selectionAcrossBlocks) { Transforms.splitNodes(editor, { at: editor.selection, always: true, }) const [nextBlock, nextBlockPath] = Editor.node( editor, Path.next(focusBlockPath), {depth: 1}, ) const nextChild = Node.child(nextBlock, 0) const firstChildIsInlineObject = !editor.isTextSpan(nextChild) if (firstChildIsInlineObject) { // If the first child in the next block is an inline object then we // add an empty span right before it to a place to put the cursor. // This is a Slate constraint that we have to adhere to. Transforms.insertNodes( editor, { _key: context.keyGenerator(), _type: 'span', text: '', marks: [], }, { at: [nextBlockPath[0], 0], }, ) } Transforms.setSelection(editor, { anchor: {path: [...nextBlockPath, 0], offset: 0}, focus: {path: [...nextBlockPath, 0], offset: 0}, }) /** * Assign new keys to markDefs that are now split across two blocks */ if ( editor.isTextBlock(nextBlock) && nextBlock.markDefs && nextBlock.markDefs.length > 0 ) { const newMarkDefKeys = new Map<string, string>() const prevNodeSpans = Array.from(Node.children(editor, focusBlockPath)) .map((entry) => entry[0]) .filter((node) => editor.isTextSpan(node)) const children = Node.children(editor, nextBlockPath) for (const [child, childPath] of children) { if (!editor.isTextSpan(child)) { continue } const marks = child.marks ?? [] // Go through the marks of the span and figure out if any of // them refer to annotations that are also present in the // previous block for (const mark of marks) { if ( schema.decorators.some((decorator) => decorator.name === mark) ) { continue } if ( prevNodeSpans.some((prevNodeSpan) => prevNodeSpan.marks?.includes(mark), ) && !newMarkDefKeys.has(mark) ) { // This annotation is both present in the previous block // and this block, so let's assign a new key to it newMarkDefKeys.set(mark, keyGenerator()) } } const newMarks = marks.map((mark) => newMarkDefKeys.get(mark) ?? mark) // No need to update the marks if they are the same if (!isEqual(marks, newMarks)) { Transforms.setNodes( editor, {marks: newMarks}, { at: childPath, }, ) } } // Time to update all the markDefs that need a new key because // they've been split across blocks const newMarkDefs = nextBlock.markDefs.map((markDef) => ({ ...markDef, _key: newMarkDefKeys.get(markDef._key) ?? markDef._key, })) // No need to update the markDefs if they are the same if (!isEqual(nextBlock.markDefs, newMarkDefs)) { Transforms.setNodes( editor, {markDefs: newMarkDefs}, { at: nextBlockPath, match: (node) => editor.isTextBlock(node), }, ) } } return } } Transforms.splitNodes(editor, {always: true}) }