UNPKG

@portabletext/editor

Version:

Portable Text Editor made in React

240 lines (228 loc) 7.01 kB
import type { PathSegment, PortableTextBlock, PortableTextChild, PortableTextObject, PortableTextTextBlock, } from '@sanity/types' import {isEqual} from 'lodash' import {Element, Text, type Descendant, type Node} from 'slate' import type {EditorSchema} from '../editor/editor-schema' export const EMPTY_MARKDEFS: PortableTextObject[] = [] export const VOID_CHILD_KEY = 'void-child' type Partial<T> = { [P in keyof T]?: T[P] } function keepObjectEquality( object: PortableTextBlock | PortableTextChild, keyMap: Record<string, PortableTextBlock | PortableTextChild>, ) { const value = keyMap[object._key] if (value && isEqual(object, value)) { return value } keyMap[object._key] = object return object } export function toSlateValue( value: PortableTextBlock[] | undefined, {schemaTypes}: {schemaTypes: EditorSchema}, keyMap: Record<string, any> = {}, ): Descendant[] { if (value && Array.isArray(value)) { return value.map((block) => { const {_type, _key, ...rest} = block const isPortableText = block && block._type === schemaTypes.block.name if (isPortableText) { const textBlock = block as PortableTextTextBlock let hasInlines = false const hasMissingStyle = typeof textBlock.style === 'undefined' const hasMissingMarkDefs = typeof textBlock.markDefs === 'undefined' const hasMissingChildren = typeof textBlock.children === 'undefined' const children = (textBlock.children || []).map((child) => { const {_type: cType, _key: cKey, ...cRest} = child // Return 'slate' version of inline object where the actual // value is stored in the `value` property. // In slate, inline objects are represented as regular // children with actual text node in order to be able to // be selected the same way as the rest of the (text) content. if (cType !== 'span') { hasInlines = true return keepObjectEquality( { _type: cType, _key: cKey, children: [ { _key: VOID_CHILD_KEY, _type: 'span', text: '', marks: [], }, ], value: cRest, __inline: true, }, keyMap, ) } // Original child object (span) return child }) // Return original block if ( !hasMissingStyle && !hasMissingMarkDefs && !hasMissingChildren && !hasInlines && Element.isElement(block) ) { // Original object return block } // TODO: remove this when we have a better way to handle missing style if (hasMissingStyle) { rest.style = schemaTypes.styles[0].name } return keepObjectEquality({_type, _key, ...rest, children}, keyMap) } return keepObjectEquality( { _type, _key, children: [ { _key: VOID_CHILD_KEY, _type: 'span', text: '', marks: [], }, ], value: rest, }, keyMap, ) }) as Descendant[] } return [] } export function fromSlateValue( value: Descendant[], textBlockType: string, keyMap: Record<string, PortableTextBlock | PortableTextChild> = {}, ): PortableTextBlock[] { return value.map((block) => { const {_key, _type} = block if (!_key || !_type) { throw new Error('Not a valid block') } if ( _type === textBlockType && 'children' in block && Array.isArray(block.children) && _key ) { let hasInlines = false const children = block.children.map((child) => { const {_type: _cType} = child if ('value' in child && _cType !== 'span') { hasInlines = true const { value: v, _key: k, _type: t, __inline: _i, children: _c, ...rest } = child return keepObjectEquality( {...rest, ...v, _key: k as string, _type: t as string}, keyMap, ) } return child }) if (!hasInlines) { return block as PortableTextBlock // Original object } return keepObjectEquality( {...block, children, _key, _type}, keyMap, ) as PortableTextBlock } const blockValue = 'value' in block && block.value return keepObjectEquality( {_key, _type, ...(typeof blockValue === 'object' ? blockValue : {})}, keyMap, ) as PortableTextBlock }) } export function isEqualToEmptyEditor( children: Descendant[] | PortableTextBlock[], schemaTypes: EditorSchema, ): boolean { return ( children === undefined || (children && Array.isArray(children) && children.length === 0) || (children && Array.isArray(children) && children.length === 1 && Element.isElement(children[0]) && children[0]._type === schemaTypes.block.name && 'style' in children[0] && children[0].style === schemaTypes.styles[0].name && !('listItem' in children[0]) && Array.isArray(children[0].children) && children[0].children.length === 1 && Text.isText(children[0].children[0]) && children[0].children[0]._type === 'span' && !children[0].children[0].marks?.join('') && children[0].children[0].text === '') ) } export function findBlockAndIndexFromPath( firstPathSegment: PathSegment, children: (Node | Partial<Node>)[], ): [Element | undefined, number | undefined] { let blockIndex = -1 const isNumber = Number.isInteger(Number(firstPathSegment)) if (isNumber) { blockIndex = Number(firstPathSegment) } else if (children) { blockIndex = children.findIndex( (blk) => Element.isElement(blk) && isEqual({_key: blk._key}, firstPathSegment), ) } if (blockIndex > -1) { return [children[blockIndex] as Element, blockIndex] } return [undefined, -1] } export function findChildAndIndexFromPath( secondPathSegment: PathSegment, block: Element, ): [Element | Text | undefined, number] { let childIndex = -1 const isNumber = Number.isInteger(Number(secondPathSegment)) if (isNumber) { childIndex = Number(secondPathSegment) } else { childIndex = block.children.findIndex((child) => isEqual({_key: child._key}, secondPathSegment), ) } if (childIndex > -1) { return [block.children[childIndex] as Element | Text, childIndex] } return [undefined, -1] } export function getValueOrInitialValue( value: unknown, initialValue: PortableTextBlock[], ): PortableTextBlock[] | undefined { if (value && Array.isArray(value) && value.length > 0) { return value } return initialValue }