UNPKG

@portabletext/editor

Version:

Portable Text Editor made in React

155 lines (130 loc) 3.88 kB
import type {Path} from '@sanity/types' import {Editor, Node, Range, Text, Transforms} from 'slate' import {parseAnnotation} from '../internal-utils/parse-blocks' import type {BehaviorOperationImplementation} from './behavior.operations' /** * @public */ export type AddedAnnotationPaths = { /** * @deprecated An annotation may be applied to multiple blocks, resulting * in multiple `markDef`'s being created. Use `markDefPaths` instead. */ markDefPath: Path markDefPaths: Array<Path> /** * @deprecated Does not return anything meaningful since an annotation * can span multiple blocks and spans. If references the span closest * to the focus point of the selection. */ spanPath: Path } export const addAnnotationOperationImplementation: BehaviorOperationImplementation< 'annotation.add', AddedAnnotationPaths | undefined > = ({context, operation}) => { const parsedAnnotation = parseAnnotation({ annotation: { _type: operation.annotation.name, ...operation.annotation.value, }, context, options: {refreshKeys: false, validateFields: true}, }) if (!parsedAnnotation) { throw new Error( `Failed to parse annotation ${JSON.stringify(operation.annotation)}`, ) } const editor = operation.editor if (!editor.selection || Range.isCollapsed(editor.selection)) { return } let paths: AddedAnnotationPaths | undefined = undefined let spanPath: Path | undefined let markDefPath: Path | undefined const markDefPaths: Path[] = [] const selectedBlocks = Editor.nodes(editor, { at: editor.selection, match: (node) => editor.isTextBlock(node), reverse: Range.isBackward(editor.selection), }) let blockIndex = 0 for (const [block, blockPath] of selectedBlocks) { if (block.children.length === 0) { continue } if (block.children.length === 1 && block.children[0].text === '') { continue } // Make sure we don't generate more keys than needed const annotationKey = blockIndex === 0 ? parsedAnnotation._key : context.keyGenerator() const markDefs = block.markDefs ?? [] const existingMarkDef = markDefs.find( (markDef) => markDef._type === parsedAnnotation._type && markDef._key === annotationKey, ) if (existingMarkDef === undefined) { Transforms.setNodes( editor, { markDefs: [ ...markDefs, { ...parsedAnnotation, _key: annotationKey, }, ], }, {at: blockPath}, ) markDefPath = [{_key: block._key}, 'markDefs', {_key: annotationKey}] if (Range.isBackward(editor.selection)) { markDefPaths.unshift(markDefPath) } else { markDefPaths.push(markDefPath) } } Transforms.setNodes(editor, {}, {match: Text.isText, split: true}) const children = Node.children(editor, blockPath) for (const [span, path] of children) { if (!editor.isTextSpan(span)) { continue } if (!Range.includes(editor.selection, path)) { continue } const marks = span.marks ?? [] const existingSameTypeAnnotations = marks.filter((mark) => markDefs.some( (markDef) => markDef._key === mark && markDef._type === parsedAnnotation._type, ), ) Transforms.setNodes( editor, { marks: [ ...marks.filter( (mark) => !existingSameTypeAnnotations.includes(mark), ), annotationKey, ], }, {at: path}, ) spanPath = [{_key: block._key}, 'children', {_key: span._key}] } blockIndex++ } if (markDefPath && spanPath) { paths = { markDefPath, markDefPaths, spanPath, } } return paths }