UNPKG

@portabletext/editor

Version:

Portable Text Editor made in React

223 lines (203 loc) 5.88 kB
import {Editor, Range, Text, Transforms} from 'slate' import {slateRangeToSelection} from '../internal-utils/slate-utils' import {toSlateRange} from '../internal-utils/to-slate-range' import {fromSlateValue} from '../internal-utils/values' import {KEY_TO_VALUE_ELEMENT} from '../internal-utils/weakMaps' import * as selectors from '../selectors' import * as utils from '../utils' import type {BehaviorOperationImplementation} from './behavior.operations' export const decoratorAddOperationImplementation: BehaviorOperationImplementation< 'decorator.add' > = ({context, operation}) => { const editor = operation.editor const mark = operation.decorator const value = fromSlateValue( editor.children, context.schema.block.name, KEY_TO_VALUE_ELEMENT.get(editor), ) const manualAnchor = operation.at?.anchor ? utils.blockOffsetToSpanSelectionPoint({ context: { ...context, value, }, blockOffset: operation.at.anchor, direction: 'backward', }) : undefined const manualFocus = operation.at?.focus ? utils.blockOffsetToSpanSelectionPoint({ context: { ...context, value, }, blockOffset: operation.at.focus, direction: 'forward', }) : undefined const manualSelection = manualAnchor && manualFocus ? { anchor: manualAnchor, focus: manualFocus, } : undefined const selection = manualSelection ? (toSlateRange({ context: { schema: context.schema, value: operation.editor.value, selection: manualSelection, }, blockIndexMap: operation.editor.blockIndexMap, }) ?? editor.selection) : editor.selection if (!selection) { return } const editorSelection = slateRangeToSelection({ schema: context.schema, editor, range: selection, }) const anchorOffset = editorSelection ? utils.selectionPointToBlockOffset({ context: { ...context, value, }, selectionPoint: editorSelection.anchor, }) : undefined const focusOffset = editorSelection ? utils.selectionPointToBlockOffset({ context: { ...context, value, }, selectionPoint: editorSelection.focus, }) : undefined if (!anchorOffset || !focusOffset) { throw new Error('Unable to find anchor or focus offset') } if (Range.isExpanded(selection)) { // Split if needed Transforms.setNodes( editor, {}, {at: selection, match: Text.isText, split: true, hanging: true}, ) // The value might have changed after splitting const newValue = fromSlateValue( editor.children, context.schema.block.name, KEY_TO_VALUE_ELEMENT.get(editor), ) // We need to find the new selection from the original offsets because the // split operation might have changed the value. const newSelection = utils.blockOffsetsToSelection({ context: { ...context, value: newValue, }, offsets: {anchor: anchorOffset, focus: focusOffset}, backward: editorSelection?.backward, }) const trimmedSelection = selectors.getTrimmedSelection({ blockIndexMap: editor.blockIndexMap, context: { converters: [], keyGenerator: context.keyGenerator, readOnly: false, schema: context.schema, selection: newSelection, value: newValue, }, decoratorState: editor.decoratorState, }) if (!trimmedSelection) { throw new Error('Unable to find trimmed selection') } const newRange = toSlateRange({ context: { schema: context.schema, value: operation.editor.value, selection: trimmedSelection, }, blockIndexMap: operation.editor.blockIndexMap, }) if (!newRange) { throw new Error('Unable to find new selection') } // Use new selection to find nodes to decorate const splitTextNodes = Range.isRange(newRange) ? [ ...Editor.nodes(editor, { at: newRange, match: (node) => Text.isText(node), }), ] : [] for (const [node, path] of splitTextNodes) { const marks = [ ...(Array.isArray(node.marks) ? node.marks : []).filter( (eMark: string) => eMark !== mark, ), mark, ] Transforms.setNodes( editor, {marks}, {at: path, match: Text.isText, split: true, hanging: true}, ) } } else { const selectedSpan = Array.from( Editor.nodes(editor, { at: selection, match: (node) => editor.isTextSpan(node), }), )?.at(0) if (!selectedSpan) { return } const [block, blockPath] = Editor.node(editor, selection, { depth: 1, }) const lonelyEmptySpan = editor.isTextBlock(block) && block.children.length === 1 && editor.isTextSpan(block.children[0]) && block.children[0].text === '' ? block.children[0] : undefined if (lonelyEmptySpan) { const existingMarks = lonelyEmptySpan.marks ?? [] const existingMarksWithoutDecorator = existingMarks.filter( (existingMark) => existingMark !== mark, ) Transforms.setNodes( editor, { marks: existingMarks.length === existingMarksWithoutDecorator.length ? [...existingMarks, mark] : existingMarksWithoutDecorator, }, { at: blockPath, match: (node) => editor.isTextSpan(node), }, ) } else { editor.decoratorState[mark] = true } } if (editor.selection) { // Reselect const selection = editor.selection editor.selection = {...selection} } }