UNPKG

@portabletext/editor

Version:

Portable Text Editor made in React

394 lines (333 loc) 12.3 kB
import {Editor, Path, Point, Range, Transforms, type Descendant} from 'slate' import {DOMEditor} from 'slate-dom' import type {EditorSchema} from '../editor/editor-schema' import {parseBlock} from '../internal-utils/parse-blocks' import { getFocusBlock, getFocusChild, getLastBlock, getSelectionEndBlock, getSelectionStartBlock, } from '../internal-utils/slate-utils' import {isEqualToEmptyEditor, toSlateValue} from '../internal-utils/values' import type {PortableTextSlateEditor} from '../types/editor' import type {BehaviorOperationImplementation} from './behavior.operations' export const insertBlockOperationImplementation: BehaviorOperationImplementation< 'insert.block' > = ({context, operation}) => { const parsedBlock = parseBlock({ block: operation.block, context, options: {refreshKeys: false, validateFields: true}, }) if (!parsedBlock) { throw new Error(`Failed to parse block ${JSON.stringify(operation.block)}`) } const fragment = toSlateValue([parsedBlock], {schemaTypes: context.schema})[0] if (!fragment) { throw new Error( `Failed to convert block to Slate fragment ${JSON.stringify(parsedBlock)}`, ) } insertBlock({ block: fragment, placement: operation.placement, select: operation.select ?? 'start', editor: operation.editor, schema: context.schema, }) } export function insertBlock({ block, placement, select, editor, schema, }: { block: Descendant placement: 'auto' | 'after' | 'before' select: 'start' | 'end' | 'none' editor: PortableTextSlateEditor schema: EditorSchema }) { const [startBlock, startBlockPath] = getSelectionStartBlock({editor}) const [endBlock, endBlockPath] = getSelectionEndBlock({editor}) if ( !editor.selection || !startBlock || !startBlockPath || !endBlock || !endBlockPath ) { if (select !== 'none') { DOMEditor.focus(editor) } const [lastBlock, lastBlockPath] = getLastBlock({editor}) if (placement === 'before') { Transforms.insertNodes(editor, [block], {at: [0]}) if (select === 'start') { Transforms.select(editor, Editor.start(editor, [0])) } else if (select === 'end') { Transforms.select(editor, Editor.end(editor, [0])) } } else if (placement === 'after') { const nextPath = lastBlockPath ? [lastBlockPath[0] + 1] : [0] Transforms.insertNodes(editor, [block], {at: nextPath}) if (select === 'start') { Transforms.select(editor, Editor.start(editor, nextPath)) } else if (select === 'end') { Transforms.select(editor, Editor.end(editor, nextPath)) } } else { // placement === 'auto' if (lastBlock && isEqualToEmptyEditor([lastBlock], schema)) { // And if the last block was an empty text block, let's remove // that too Transforms.removeNodes(editor, {at: lastBlockPath}) Transforms.insertNodes(editor, [block], { at: lastBlockPath, select: false, }) Transforms.deselect(editor) if (select === 'start') { Transforms.select(editor, Editor.start(editor, lastBlockPath)) } else if (select === 'end') { Transforms.select(editor, Editor.end(editor, lastBlockPath)) } return } if ( editor.isTextBlock(block) && lastBlock && editor.isTextBlock(lastBlock) ) { const selectionBefore = Editor.end(editor, lastBlockPath) Transforms.insertFragment(editor, [block], { at: Editor.end(editor, lastBlockPath), }) if (select === 'start') { Transforms.select(editor, selectionBefore) } else if (select === 'none') { Transforms.deselect(editor) } return } const nextPath = lastBlockPath ? [lastBlockPath[0] + 1] : [0] Transforms.insertNodes(editor, [block], {at: nextPath, select: false}) if (select === 'start') { Transforms.select(editor, Editor.start(editor, nextPath)) } else if (select === 'end') { Transforms.select(editor, Editor.end(editor, nextPath)) } } } else { if (placement === 'before') { const currentSelection = editor.selection const selectionStartPoint = Range.start(currentSelection) Transforms.insertNodes(editor, [block], { at: [selectionStartPoint.path[0]], select: false, }) if (select === 'start') { Transforms.select( editor, Editor.start(editor, [selectionStartPoint.path[0]]), ) } else if (select === 'end') { Transforms.select( editor, Editor.end(editor, [selectionStartPoint.path[0]]), ) } } else if (placement === 'after') { const currentSelection = editor.selection const selectionEndPoint = Range.end(currentSelection) const nextPath = [selectionEndPoint.path[0] + 1] Transforms.insertNodes(editor, [block], { at: nextPath, select: false, }) if (select === 'start') { Transforms.select(editor, Editor.start(editor, nextPath)) } else if (select === 'end') { Transforms.select(editor, Editor.end(editor, nextPath)) } } else { // placement === 'auto' const currentSelection = editor.selection const endBlockEndPoint = Editor.start(editor, endBlockPath) if (Range.isExpanded(currentSelection) && !editor.isTextBlock(block)) { Transforms.delete(editor, {at: currentSelection}) const newSelection = editor.selection const [focusBlock, focusBlockPath] = getFocusBlock({editor}) Transforms.insertNodes(editor, [block], { voids: true, }) const adjustedSelection = newSelection.anchor.offset === 0 ? Range.transform(newSelection, { type: 'insert_node', node: block, path: [newSelection.anchor.path[0]], }) : newSelection if (select === 'none' && adjustedSelection) { Transforms.select(editor, adjustedSelection) } if (focusBlock && isEqualToEmptyEditor([focusBlock], schema)) { Transforms.removeNodes(editor, {at: focusBlockPath}) } return } if (editor.isTextBlock(endBlock) && editor.isTextBlock(block)) { const selectionStartPoint = Range.start(currentSelection) if (isEqualToEmptyEditor([endBlock], schema)) { const currentSelection = editor.selection Transforms.insertNodes(editor, [block], { at: endBlockPath, select: false, }) Transforms.removeNodes(editor, {at: Path.next(endBlockPath)}) if (select === 'start') { Transforms.select(editor, selectionStartPoint) } else if (select === 'end') { Transforms.select(editor, Editor.end(editor, endBlockPath)) } else { Transforms.select(editor, currentSelection) } return } if (select === 'end') { Transforms.insertFragment(editor, [block], { voids: true, }) return } Transforms.insertFragment(editor, [block], { at: currentSelection, voids: true, }) if (select === 'start') { Transforms.select(editor, selectionStartPoint) } else { if (!Point.equals(selectionStartPoint, endBlockEndPoint)) { Transforms.select(editor, selectionStartPoint) } } } else { if (!editor.isTextBlock(endBlock)) { Transforms.insertNodes(editor, [block], {select: false}) const nextPath = [endBlockPath[0] + 1] if (select === 'start') { Transforms.select(editor, Editor.start(editor, nextPath)) } else if (select === 'end') { Transforms.select(editor, Editor.end(editor, nextPath)) } } else { const endBlockStartPoint = Editor.start(editor, endBlockPath) const endBlockEndPoint = Editor.end(editor, endBlockPath) const selectionStartPoint = Range.start(currentSelection) const selectionEndPoint = Range.end(currentSelection) if ( Range.isCollapsed(currentSelection) && Point.equals(selectionStartPoint, endBlockStartPoint) ) { Transforms.insertNodes(editor, [block], { at: endBlockPath, select: false, }) if (select === 'start' || select === 'end') { Transforms.select(editor, Editor.start(editor, endBlockPath)) } if (isEqualToEmptyEditor([endBlock], schema)) { Transforms.removeNodes(editor, {at: Path.next(endBlockPath)}) } } else if ( Range.isCollapsed(currentSelection) && Point.equals(selectionEndPoint, endBlockEndPoint) ) { const nextPath = [endBlockPath[0] + 1] Transforms.insertNodes(editor, [block], { at: nextPath, select: false, }) if (select === 'start' || select === 'end') { Transforms.select(editor, Editor.start(editor, nextPath)) } } else if ( Range.isExpanded(currentSelection) && Point.equals(selectionStartPoint, endBlockStartPoint) && Point.equals(selectionEndPoint, endBlockEndPoint) ) { Transforms.insertFragment(editor, [block], { at: currentSelection, }) if (select === 'start') { Transforms.select(editor, Editor.start(editor, endBlockPath)) } else if (select === 'end') { Transforms.select(editor, Editor.end(editor, endBlockPath)) } } else if ( Range.isExpanded(currentSelection) && Point.equals(selectionStartPoint, endBlockStartPoint) ) { Transforms.insertFragment(editor, [block], { at: currentSelection, }) if (select === 'start') { Transforms.select(editor, Editor.start(editor, endBlockPath)) } else if (select === 'end') { Transforms.select(editor, Editor.end(editor, endBlockPath)) } } else if ( Range.isExpanded(currentSelection) && Point.equals(selectionEndPoint, endBlockEndPoint) ) { Transforms.insertFragment(editor, [block], { at: currentSelection, }) if (select === 'start') { Transforms.select( editor, Editor.start(editor, Path.next(endBlockPath)), ) } else if (select === 'end') { Transforms.select( editor, Editor.end(editor, Path.next(endBlockPath)), ) } } else { const currentSelection = editor.selection const [focusChild] = getFocusChild({editor}) if (focusChild && editor.isTextSpan(focusChild)) { Transforms.splitNodes(editor, { at: currentSelection, }) Transforms.insertFragment(editor, [block], { at: currentSelection, }) if (select === 'start' || select === 'end') { Transforms.select(editor, [endBlockPath[0] + 1]) } else { Transforms.select(editor, currentSelection) } } else { const nextPath = [endBlockPath[0] + 1] Transforms.insertNodes(editor, [block], { at: nextPath, select: false, }) Transforms.select(editor, currentSelection) if (select === 'start') { Transforms.select(editor, Editor.start(editor, nextPath)) } else if (select === 'end') { Transforms.select(editor, Editor.end(editor, nextPath)) } } } } } } } }