From 6202a8c05f51f75a499e7be18be5243b982246f2 Mon Sep 17 00:00:00 2001 From: Per-Kristian Nordnes Date: Thu, 15 Dec 2022 15:27:22 +0100 Subject: [PATCH] refactor(portable-text-editor): update types and add render callbacks to the PTE This will make the PTE use the updated types for PT-conentent and for the block schema itself. This will also add two new render callbacks 'renderListItem' and 'renderStyle' to complete them. --- .../src/editor/DraggableBlock.tsx | 5 +- .../src/editor/DraggableChild.tsx | 8 +- .../src/editor/Editable.tsx | 65 +++--- .../src/editor/Element.tsx | 200 ++++++++++-------- .../portable-text-editor/src/editor/Leaf.tsx | 135 +++++------- .../src/editor/PortableTextEditor.tsx | 60 +++--- .../__tests__/PortableTextEditor.test.tsx | 3 +- .../__tests__/PortableTextEditorTester.tsx | 18 +- .../hooks/usePortableTextEditorValue.ts | 2 +- .../src/editor/nodes/DefaultAnnotation.tsx | 4 +- .../src/editor/nodes/DefaultObject.tsx | 8 +- .../src/editor/nodes/TextBlock.tsx | 41 ---- ...ortableTextMarkModelNormalization.test.tsx | 8 +- .../editor/plugins/createWithEditableAPI.ts | 119 +++++------ .../src/editor/plugins/createWithHotKeys.ts | 35 ++- .../editor/plugins/createWithInsertData.ts | 62 +++--- .../editor/plugins/createWithObjectKeys.ts | 10 +- .../src/editor/plugins/createWithPatches.ts | 20 +- .../plugins/createWithPlaceholderBlock.ts | 11 +- .../createWithPortableTextBlockStyle.ts | 20 +- .../plugins/createWithPortableTextLists.ts | 21 +- .../createWithPortableTextMarkModel.ts | 27 ++- .../createWithPortableTextSelections.ts | 18 +- .../editor/plugins/createWithSchemaTypes.ts | 54 ++--- .../src/editor/plugins/createWithUtils.ts | 9 +- .../src/editor/plugins/index.ts | 32 ++- .../@sanity/portable-text-editor/src/index.ts | 3 - .../portable-text-editor/src/types/editor.ts | 175 ++++++++++----- .../src/types/portableText.ts | 71 ------- .../portable-text-editor/src/types/schema.ts | 35 --- .../portable-text-editor/src/types/slate.ts | 13 +- .../__tests__/operationToPatches.test.ts | 22 +- .../utils/__tests__/patchToOperations.test.ts | 47 ++-- .../src/utils/__tests__/values.test.ts | 26 ++- ...tures.ts => getPortableTextMemberTypes.ts} | 57 ++--- .../src/utils/operationToPatches.ts | 88 ++++---- .../src/utils/patchToOperations.ts | 82 ++++--- .../portable-text-editor/src/utils/paths.ts | 13 +- .../portable-text-editor/src/utils/ranges.ts | 10 +- .../src/utils/selection.ts | 9 +- .../src/utils/validateValue.ts | 80 ++++--- .../portable-text-editor/src/utils/values.ts | 26 +-- .../src/utils/weakMaps.ts | 4 +- 43 files changed, 824 insertions(+), 932 deletions(-) delete mode 100644 packages/@sanity/portable-text-editor/src/editor/nodes/TextBlock.tsx delete mode 100644 packages/@sanity/portable-text-editor/src/types/portableText.ts delete mode 100644 packages/@sanity/portable-text-editor/src/types/schema.ts rename packages/@sanity/portable-text-editor/src/utils/{getPortableTextFeatures.ts => getPortableTextMemberTypes.ts} (65%) diff --git a/packages/@sanity/portable-text-editor/src/editor/DraggableBlock.tsx b/packages/@sanity/portable-text-editor/src/editor/DraggableBlock.tsx index 4ee83f9baaf..5845d1127a5 100644 --- a/packages/@sanity/portable-text-editor/src/editor/DraggableBlock.tsx +++ b/packages/@sanity/portable-text-editor/src/editor/DraggableBlock.tsx @@ -3,7 +3,6 @@ import {Element as SlateElement, Transforms, Path, Editor} from 'slate' import {ReactEditor, useSlateStatic} from '@sanity/slate-react' import {debugWithName} from '../utils/debug' import { - IS_DRAGGING_CHILD_ELEMENT, IS_DRAGGING_ELEMENT_TARGET, IS_DRAGGING_BLOCK_ELEMENT, IS_DRAGGING, @@ -13,14 +12,14 @@ import { const debug = debugWithName('components:DraggableBlock') const debugRenders = false -type ElementProps = { +export interface DraggableBlockProps { children: React.ReactNode element: SlateElement readOnly: boolean blockRef: React.MutableRefObject } -export const DraggableBlock = ({children, element, readOnly, blockRef}: ElementProps) => { +export const DraggableBlock = ({children, element, readOnly, blockRef}: DraggableBlockProps) => { const editor = useSlateStatic() const dragGhostRef: React.MutableRefObject = useRef() const [isDragOver, setIsDragOver] = useState(false) diff --git a/packages/@sanity/portable-text-editor/src/editor/DraggableChild.tsx b/packages/@sanity/portable-text-editor/src/editor/DraggableChild.tsx index 8173212d1f2..72254542644 100644 --- a/packages/@sanity/portable-text-editor/src/editor/DraggableChild.tsx +++ b/packages/@sanity/portable-text-editor/src/editor/DraggableChild.tsx @@ -1,5 +1,5 @@ import React, {ReactElement, useRef, useMemo, useCallback} from 'react' -import {Element as SlateElement, Transforms, Editor} from 'slate' +import {Element as SlateElement, Transforms, Editor, Text} from 'slate' import {ReactEditor, useSlateStatic} from '@sanity/slate-react' import {debugWithName} from '../utils/debug' import {IS_DRAGGING, IS_DRAGGING_ELEMENT_RANGE, IS_DRAGGING_CHILD_ELEMENT} from '../utils/weakMaps' @@ -12,13 +12,13 @@ declare global { } } -type ElementProps = { +export interface DraggableChildProps { children: ReactElement - element: SlateElement + element: Text | SlateElement readOnly: boolean } -export const DraggableChild = ({children, element, readOnly}: ElementProps) => { +export const DraggableChild = ({children, element, readOnly}: DraggableChildProps) => { const editor = useSlateStatic() const dragGhostRef: React.MutableRefObject = useRef() const isVoid = useMemo(() => Editor.isVoid(editor, element), [editor, element]) diff --git a/packages/@sanity/portable-text-editor/src/editor/Editable.tsx b/packages/@sanity/portable-text-editor/src/editor/Editable.tsx index 3774a31f72d..667153ef429 100644 --- a/packages/@sanity/portable-text-editor/src/editor/Editable.tsx +++ b/packages/@sanity/portable-text-editor/src/editor/Editable.tsx @@ -1,6 +1,11 @@ -import {BaseRange, Transforms} from 'slate' +import {BaseRange, Transforms, Text} from 'slate' import React, {useCallback, useMemo, useEffect, forwardRef} from 'react' -import {Editable as SlateEditable, ReactEditor} from '@sanity/slate-react' +import { + Editable as SlateEditable, + ReactEditor, + RenderElementProps, + RenderLeafProps, +} from '@sanity/slate-react' import { EditorSelection, OnBeforeInputFn, @@ -11,6 +16,8 @@ import { RenderBlockFunction, RenderChildFunction, RenderDecoratorFunction, + RenderListItemFunction, + RenderStyleFunction, ScrollSelectionIntoViewFunction, } from '../types/editor' import {HotkeyOptions} from '../types/options' @@ -63,7 +70,9 @@ export type PortableTextEditableProps = { renderBlock?: RenderBlockFunction renderChild?: RenderChildFunction renderDecorator?: RenderDecoratorFunction + renderListItem?: RenderListItemFunction renderPlaceholder?: () => React.ReactNode + renderStyle?: RenderStyleFunction scrollSelectionIntoView?: ScrollSelectionIntoViewFunction selection?: EditorSelection spellCheck?: boolean @@ -84,7 +93,9 @@ export const PortableTextEditable = forwardRef(function PortableTextEditable( renderBlock, renderChild, renderDecorator, + renderListItem, renderPlaceholder, + renderStyle, selection: propsSelection, scrollSelectionIntoView, spellCheck, @@ -95,21 +106,18 @@ export const PortableTextEditable = forwardRef(function PortableTextEditable( const readOnly = usePortableTextEditorReadOnlyStatus() const ref = useForwardedRef(forwardedRef) - const { - change$, - keyGenerator, - portableTextFeatures, - slateInstance: slateEditor, - } = portableTextEditor + const {change$, keyGenerator, types, slateInstance: slateEditor} = portableTextEditor + + const blockTypeName = types.block.name // React/UI-spesific plugins const withInsertData = useMemo( - () => createWithInsertData(change$, portableTextFeatures, keyGenerator), - [change$, keyGenerator, portableTextFeatures] + () => createWithInsertData(change$, types, keyGenerator), + [change$, keyGenerator, types] ) const withHotKeys = useMemo( - () => createWithHotkeys(portableTextFeatures, keyGenerator, portableTextEditor, hotkeys), - [hotkeys, keyGenerator, portableTextEditor, portableTextFeatures] + () => createWithHotkeys(types, keyGenerator, portableTextEditor, hotkeys), + [hotkeys, keyGenerator, portableTextEditor, types] ) // Output a minimal React editor inside Editable when in readOnly mode. @@ -125,21 +133,23 @@ export const PortableTextEditable = forwardRef(function PortableTextEditable( }, [readOnly, slateEditor, withHotKeys, withInsertData]) const renderElement = useCallback( - (eProps: any) => ( + (eProps: RenderElementProps) => ( ), - [portableTextFeatures, spellCheck, readOnly, renderBlock, renderChild] + [types, spellCheck, readOnly, renderBlock, renderChild, renderListItem, renderStyle] ) const renderLeaf = useCallback( - (lProps: any) => { + (lProps: RenderLeafProps & {leaf: Text & {placeholder?: boolean}}) => { if (renderPlaceholder && lProps.leaf.placeholder && lProps.text.text === '') { return ( <> @@ -149,7 +159,7 @@ export const PortableTextEditable = forwardRef(function PortableTextEditable( { @@ -246,8 +256,7 @@ export const PortableTextEditable = forwardRef(function PortableTextEditable( event, value: PortableTextEditor.getValue(portableTextEditor), path: slateEditor.selection?.focus.path || [], - portableTextFeatures, // New key added in v.2.23.2 - type: portableTextFeatures.types.portableText, // For legacy support + types, }) ) }) @@ -260,7 +269,7 @@ export const PortableTextEditable = forwardRef(function PortableTextEditable( return } if (result && result.insert) { - slateEditor.insertFragment(toSlateValue(result.insert, {portableTextFeatures})) + slateEditor.insertFragment(toSlateValue(result.insert, {types})) change$.next({type: 'loading', isLoading: false}) return } @@ -272,7 +281,7 @@ export const PortableTextEditable = forwardRef(function PortableTextEditable( return error }) }, - [change$, onPaste, portableTextEditor, portableTextFeatures, slateEditor] + [change$, onPaste, portableTextEditor, types, slateEditor] ) const handleOnFocus = useCallback(() => { @@ -310,7 +319,7 @@ export const PortableTextEditable = forwardRef(function PortableTextEditable( }, [portableTextEditor, scrollSelectionIntoView]) const decorate = useCallback(() => { - if (isEqualToEmptyEditor(slateEditor.children, portableTextFeatures)) { + if (isEqualToEmptyEditor(slateEditor.children, types)) { return [ { anchor: { @@ -326,7 +335,7 @@ export const PortableTextEditable = forwardRef(function PortableTextEditable( ] } return EMPTY_DECORATORS - }, [portableTextFeatures, slateEditor.children]) + }, [types, slateEditor.children]) // The editor const slateEditable = useMemo( diff --git a/packages/@sanity/portable-text-editor/src/editor/Element.tsx b/packages/@sanity/portable-text-editor/src/editor/Element.tsx index 10c7b16802b..d675adf3c8c 100644 --- a/packages/@sanity/portable-text-editor/src/editor/Element.tsx +++ b/packages/@sanity/portable-text-editor/src/editor/Element.tsx @@ -1,56 +1,52 @@ -import React, {ReactElement, FunctionComponent, useRef} from 'react' +import React, {ReactElement, FunctionComponent, useRef, useMemo} from 'react' import {Element as SlateElement, Editor, Range} from 'slate' -import {Path} from '@sanity/types' -import {useSelected, useSlateStatic, ReactEditor} from '@sanity/slate-react' -import {PortableTextBlock, PortableTextFeatures} from '../types/portableText' -import {RenderAttributes, RenderBlockFunction, RenderChildFunction} from '../types/editor' +import {Path, PortableTextChild, PortableTextObject, PortableTextTextBlock} from '@sanity/types' +import {useSelected, useSlateStatic, ReactEditor, RenderElementProps} from '@sanity/slate-react' +import { + BlockRenderProps, + PortableTextMemberTypes, + RenderBlockFunction, + RenderChildFunction, + RenderListItemFunction, + RenderStyleFunction, +} from '../types/editor' import {fromSlateValue} from '../utils/values' import {debugWithName} from '../utils/debug' import {KEY_TO_VALUE_ELEMENT} from '../utils/weakMaps' -import TextBlock from './nodes/TextBlock' import ObjectNode from './nodes/DefaultObject' -import {DefaultBlockObject} from './nodes/index' +import {DefaultBlockObject, DefaultListItem, DefaultListItemInner} from './nodes/index' import {DraggableBlock} from './DraggableBlock' import {DraggableChild} from './DraggableChild' const debug = debugWithName('components:Element') const debugRenders = false - -export interface ElementAttributes { - 'data-slate-node': 'element' - 'data-slate-void'?: true - 'data-slate-inline'?: true - contentEditable?: false - dir?: 'rtl' - ref: any -} - -type ElementProps = { - attributes: ElementAttributes +const EMPTY_ANNOTATIONS: PortableTextObject[] = [] +interface ElementProps { + attributes: RenderElementProps['attributes'] children: ReactElement element: SlateElement - portableTextFeatures: PortableTextFeatures + types: PortableTextMemberTypes readOnly: boolean renderBlock?: RenderBlockFunction renderChild?: RenderChildFunction + renderListItem?: RenderListItemFunction + renderStyle?: RenderStyleFunction spellCheck?: boolean } const inlineBlockStyle = {display: 'inline-block'} -const defaultRender = (value: PortableTextBlock) => { - return -} - -// eslint-disable-next-line max-statements +// eslint-disable-next-line max-statements, complexity export const Element: FunctionComponent = ({ attributes, children, element, - portableTextFeatures, + types, readOnly, renderBlock, renderChild, + renderListItem, + renderStyle, spellCheck, }) => { const editor = useSlateStatic() @@ -58,8 +54,18 @@ export const Element: FunctionComponent = ({ const blockRef = useRef(null) const inlineBlockObjectRef = useRef(null) const focused = (selected && editor.selection && Range.isCollapsed(editor.selection)) || false + + const value = useMemo( + () => fromSlateValue([element], types.block.name, KEY_TO_VALUE_ELEMENT.get(editor))[0], + [editor, element, types.block.name] + ) + + let renderedBlock = children + let className + const blockPath: Path = useMemo(() => [{_key: element._key}], [element]) + if (typeof element._type !== 'string') { throw new Error(`Expected element to have a _type property`) } @@ -72,9 +78,7 @@ export const Element: FunctionComponent = ({ if (editor.isInline(element)) { const path = ReactEditor.findPath(editor, element) const [block] = Editor.node(editor, path, {depth: 1}) - const type = portableTextFeatures.types.inlineObjects.find( - (_type) => _type.name === element._type - ) + const type = types.inlineObjects.find((_type) => _type.name === element._type) if (!type) { throw new Error('Could not find type for inline block element') } @@ -96,25 +100,17 @@ export const Element: FunctionComponent = ({ contentEditable={false} > {renderChild && - renderChild( - fromSlateValue( - [element], - portableTextFeatures.types.block.name, - KEY_TO_VALUE_ELEMENT.get(editor) - )[0], + renderChild({ + annotations: EMPTY_ANNOTATIONS, // These inline objects currently doesn't support annotations. This is a limitation of the current PT spec/model. + children: , + value: value as PortableTextChild, type, - {focused, selected, path: elmPath}, - defaultRender, - inlineBlockObjectRef - )} - {!renderChild && - defaultRender( - fromSlateValue( - [element], - portableTextFeatures.types.block.name, - KEY_TO_VALUE_ELEMENT.get(editor) - )[0] - )} + focused, + selected, + path: elmPath, + editorElementRef: inlineBlockObjectRef, + })} + {!renderChild && } @@ -123,44 +119,75 @@ export const Element: FunctionComponent = ({ throw new Error('Block not found!') } - const renderAttribs: RenderAttributes = {focused, selected, path: [{_key: element._key}]} - // If not inline, it's either a block (text) or a block object (non-text) // NOTE: text blocks aren't draggable with DraggableBlock (yet?) - if (element._type === portableTextFeatures.types.block.name) { + if (element._type === types.block.name) { className = `pt-block pt-text-block` const isListItem = 'listItem' in element - const hasStyle = 'style' in element if (debugRenders) { debug(`Render ${element._key} (text block)`) } - if (hasStyle) { - renderAttribs.style = element.style || 'normal' - className = `pt-block pt-text-block pt-text-block-style-${element.style}` + const style = ('style' in element && element.style) || 'normal' + className = `pt-block pt-text-block pt-text-block-style-${style}` + const blockStyleType = types.styles.find((item) => item.value === style) + if (renderStyle && blockStyleType) { + renderedBlock = renderStyle({ + block: element as PortableTextTextBlock, + children, + focused, + selected, + value: style, + path: blockPath, + type: blockStyleType, + editorElementRef: blockRef, + }) } + let level if (isListItem) { - renderAttribs.listItem = element.listItem - if (Number.isInteger(element.level)) { - renderAttribs.level = element.level - } else { - renderAttribs.level = 1 + if (typeof element.level === 'number') { + level = element.level } - className += ` pt-list-item pt-list-item-${renderAttribs.listItem} pt-list-item-level-${renderAttribs.level}` + className += ` pt-list-item pt-list-item-${element.listItem} pt-list-item-level-${level || 1}` } - const textBlock = ( - - {children} - - ) - const propsOrDefaultRendered = renderBlock - ? renderBlock( - fromSlateValue([element], element._type, KEY_TO_VALUE_ELEMENT.get(editor))[0], - portableTextFeatures.types.block, - renderAttribs, - () => textBlock, - blockRef + if (editor.isListBlock(value) && isListItem && element.listItem) { + const listType = types.lists.find((item) => item.value === element.listItem) + if (renderListItem && listType) { + renderedBlock = renderListItem({ + block: value, + children: renderedBlock, + focused, + selected, + value: element.listItem, + path: blockPath, + type: listType, + level: value.level || 1, + editorElementRef: blockRef, + }) + } else { + renderedBlock = ( + + {renderedBlock} + ) - : textBlock + } + } + const renderProps: BlockRenderProps = { + children: renderedBlock, + editorElementRef: blockRef, + focused, + level, + listItem: isListItem ? element.listItem : undefined, + path: blockPath, + selected, + style, + type: types.block, + value, + } + + const propsOrDefaultRendered = renderBlock ? renderBlock(renderProps) : children return (
@@ -169,7 +196,7 @@ export const Element: FunctionComponent = ({
) } - const type = portableTextFeatures.types.blockObjects.find((_type) => _type.name === element._type) + const type = types.blockObjects.find((_type) => _type.name === element._type) if (!type) { throw new Error(`Could not find schema type for block element of _type ${element._type}`) } @@ -177,13 +204,18 @@ export const Element: FunctionComponent = ({ debug(`Render ${element._key} (object block)`) } className = 'pt-block pt-object-block' - const block = fromSlateValue( - [element], - portableTextFeatures.types.block.name, - KEY_TO_VALUE_ELEMENT.get(editor) - )[0] + const block = fromSlateValue([element], types.block.name, KEY_TO_VALUE_ELEMENT.get(editor))[0] const renderedBlockFromProps = - renderBlock && renderBlock(block, type, renderAttribs, defaultRender, blockRef) + renderBlock && + renderBlock({ + children: , + value: block, + type, + selected, + focused, + path: blockPath, + editorElementRef: blockRef, + }) return (
{children} @@ -195,13 +227,7 @@ export const Element: FunctionComponent = ({ )} {!renderedBlockFromProps && ( - {defaultRender( - fromSlateValue( - [element], - portableTextFeatures.types.block.name, - KEY_TO_VALUE_ELEMENT.get(editor) - )[0] - )} + )} diff --git a/packages/@sanity/portable-text-editor/src/editor/Leaf.tsx b/packages/@sanity/portable-text-editor/src/editor/Leaf.tsx index 873d59e3165..205b0da0b84 100644 --- a/packages/@sanity/portable-text-editor/src/editor/Leaf.tsx +++ b/packages/@sanity/portable-text-editor/src/editor/Leaf.tsx @@ -1,27 +1,25 @@ -import React, {ReactElement, SyntheticEvent, useCallback} from 'react' -import {Element, Range, Text} from 'slate' -import {useSelected, useSlateStatic} from '@sanity/slate-react' +import React, {ReactElement} from 'react' +import {Range, Text} from 'slate' +import {RenderLeafProps, useSelected, useSlateStatic} from '@sanity/slate-react' import {uniq} from 'lodash' -import {PortableTextBlock, PortableTextFeatures, TextBlock} from '../types/portableText' +import {PortableTextObject, PortableTextTextBlock} from '@sanity/types' import { RenderChildFunction, - RenderDecoratorFunction, + PortableTextMemberTypes, RenderAnnotationFunction, + RenderDecoratorFunction, } from '../types/editor' import {debugWithName} from '../utils/debug' import {DefaultAnnotation} from './nodes/DefaultAnnotation' import {DraggableChild} from './DraggableChild' -import {ElementAttributes} from './Element' const debug = debugWithName('components:Leaf') const debugRenders = false -type LeafProps = { - attributes: ElementAttributes +interface LeafProps extends RenderLeafProps { children: ReactElement keyGenerator: () => string - leaf: Element - portableTextFeatures: PortableTextFeatures + types: PortableTextMemberTypes renderAnnotation?: RenderAnnotationFunction renderChild?: RenderChildFunction renderDecorator?: RenderDecoratorFunction @@ -31,45 +29,31 @@ type LeafProps = { export const Leaf = (props: LeafProps) => { const editor = useSlateStatic() const selected = useSelected() - const {attributes, children, leaf, portableTextFeatures, keyGenerator, renderChild, readOnly} = - props + const {attributes, children, leaf, types, keyGenerator, renderChild, readOnly} = props const spanRef = React.useRef(null) let returnedChildren = children const focused = (selected && editor.selection && Range.isCollapsed(editor.selection)) || false - const handleMouseDown = useCallback( - (event: SyntheticEvent) => { - // Slate will deselect this when it is already selected and clicked again, so prevent that. 2020/05/04 - if (focused) { - event.stopPropagation() - event.preventDefault() - } - }, - [focused] - ) - if (Text.isText(leaf) && leaf._type === portableTextFeatures.types.span.name) { - const blockElement = children.props.parent as TextBlock | undefined - const path = blockElement ? [{_key: blockElement._key}, 'children', {_key: leaf._key}] : [] - const decoratorValues = portableTextFeatures.decorators.map((dec) => dec.value) + + // Render text nodes + if (Text.isText(leaf) && leaf._type === types.span.name) { + const block = children.props.parent as PortableTextTextBlock | undefined + const path = block ? [{_key: block._key}, 'children', {_key: leaf._key}] : [] + const decoratorValues = types.decorators.map((dec) => dec.value) const marks: string[] = uniq( (Array.isArray(leaf.marks) ? leaf.marks : []).filter((mark) => decoratorValues.includes(mark)) ) marks.forEach((mark) => { - const type = portableTextFeatures.decorators.find((dec) => dec.value === mark) - if (type) { - // TODO: look into this API! - if (type?.blockEditor?.render) { - const CustomComponent = type?.blockEditor?.render - returnedChildren = {returnedChildren} - } - if (props.renderDecorator) { - returnedChildren = props.renderDecorator( - mark, - type, - {focused, selected, path}, - () => <>{returnedChildren}, - spanRef - ) - } + const type = types.decorators.find((dec) => dec.value === mark) + if (type && props.renderDecorator) { + returnedChildren = props.renderDecorator({ + children: returnedChildren, + editorElementRef: spanRef, + focused, + path, + selected, + type, + value: mark, + }) } }) const annotationMarks = Array.isArray(leaf.marks) ? leaf.marks : [] @@ -77,62 +61,55 @@ export const Leaf = (props: LeafProps) => { .map( (mark) => !decoratorValues.includes(mark) && - blockElement && - blockElement.markDefs && - (blockElement.markDefs.find((def) => def._key === mark) as PortableTextBlock | undefined) + block && + block.markDefs && + block.markDefs.find((def) => def._key === mark) ) - .filter(Boolean) as PortableTextBlock[] + .filter(Boolean) as PortableTextObject[] - if (annotations.length > 0) { + if (block && annotations.length > 0) { annotations.forEach((annotation) => { - const type = portableTextFeatures.types.annotations.find((t) => t.name === annotation._type) - // TODO: look into this API! - const CustomComponent = (type as any)?.blockEditor?.render - const defaultRender = (): JSX.Element => - // TODO: annotation should be an own prop here, keeping for backward compability (2020/05/18). - CustomComponent ? ( - - {returnedChildren} - - ) : ( - <>{returnedChildren} - ) - + const type = types.annotations.find((t) => t.name === annotation._type) if (type) { if (props.renderAnnotation) { returnedChildren = ( - - {props.renderAnnotation( - annotation, + + {props.renderAnnotation({ + block, + children: returnedChildren, + editorElementRef: spanRef, + focused, + path, + selected, type, - {focused, selected, path, annotations}, - defaultRender, - spanRef - )} + value: annotation, + })} ) } else { returnedChildren = ( - - {defaultRender()} - + {returnedChildren} ) } } }) } - if (blockElement && renderChild) { - const child = blockElement.children.find((_child) => _child._key === leaf._key) // Ensure object equality + if (block && renderChild) { + const child = block.children.find((_child) => _child._key === leaf._key) // Ensure object equality if (child) { - returnedChildren = renderChild( - child, - portableTextFeatures.types.span, - {focused, selected, path, annotations}, - () => returnedChildren, - spanRef - ) + const defaultRendered = <>{returnedChildren} + returnedChildren = renderChild({ + children: defaultRendered, + value: child, + type: types.span, + focused, + selected, + path, + annotations, + editorElementRef: spanRef, + }) } } } diff --git a/packages/@sanity/portable-text-editor/src/editor/PortableTextEditor.tsx b/packages/@sanity/portable-text-editor/src/editor/PortableTextEditor.tsx index 7a41a182fc2..f172fae29de 100644 --- a/packages/@sanity/portable-text-editor/src/editor/PortableTextEditor.tsx +++ b/packages/@sanity/portable-text-editor/src/editor/PortableTextEditor.tsx @@ -1,5 +1,15 @@ import React, {PropsWithChildren} from 'react' -import {ArraySchemaType, Path} from '@sanity/types' +import { + ArrayDefinition, + ArraySchemaType, + BlockSchemaType, + ObjectSchemaType, + Path, + PortableTextBlock, + PortableTextChild, + PortableTextObject, + SpanSchemaType, +} from '@sanity/types' import {Subscription, Subject, defer, of, EMPTY, Observable, OperatorFunction} from 'rxjs' import {concatMap, share, switchMap, tap} from 'rxjs/operators' import {randomKey} from '@sanity/util/content' @@ -7,9 +17,7 @@ import {createEditor, Descendant, Transforms} from 'slate' import {debounce, isEqual, throttle} from 'lodash' import {Slate, withReact} from '@sanity/slate-react' import {compileType} from '../utils/schema' -import {getPortableTextFeatures} from '../utils/getPortableTextFeatures' -import {PortableTextBlock, PortableTextFeatures, PortableTextChild} from '../types/portableText' -import {RawType, Type} from '../types/schema' +import {getPortableTextMemberTypes} from '../utils/getPortableTextMemberTypes' import type {Patch} from '../types/patch' import { EditorSelection, @@ -20,6 +28,7 @@ import { PatchObservable, PortableTextSlateEditor, EditableAPIDeleteOptions, + PortableTextMemberTypes, } from '../types/editor' import {validateValue} from '../utils/validateValue' import {debugWithName} from '../utils/debug' @@ -53,9 +62,9 @@ export type PortableTextEditorProps = PropsWithChildren<{ onChange: (change: EditorChange) => void /** - * (Compiled or raw JSON) schema type for the portable text field + * Schema type for the portable text field */ - type: ArraySchemaType | RawType + type: ArraySchemaType | ArrayDefinition /** * Maximum number of blocks to allow within the editor @@ -100,7 +109,7 @@ export class PortableTextEditor extends React.Component< public change$: EditorChanges = new Subject() public keyGenerator: () => string public maxBlocks: number | undefined - public portableTextFeatures: PortableTextFeatures + public types: PortableTextMemberTypes public readOnly: boolean public slateInstance: PortableTextSlateEditor public type: ArraySchemaType @@ -134,7 +143,7 @@ export class PortableTextEditor extends React.Component< this.change$.next({type: 'loading', isLoading: true}) // Get the block types feature set (lookup table) - this.portableTextFeatures = getPortableTextFeatures(this.type) + this.types = getPortableTextMemberTypes(this.type) // Setup keyGenerator (either from props, or default) this.keyGenerator = props.keyGenerator || defaultKeyGenerator @@ -189,7 +198,7 @@ export class PortableTextEditor extends React.Component< this.readOnly = Boolean(props.readOnly) || false // Validate the incoming value if (props.value) { - const validation = validateValue(props.value, this.portableTextFeatures, this.keyGenerator) + const validation = validateValue(props.value, this.types, this.keyGenerator) if (props.value && !validation.valid) { this.change$.next({type: 'loading', isLoading: false}) this.change$.next({ @@ -209,8 +218,10 @@ export class PortableTextEditor extends React.Component< this.state = { ...this.state, initialValue: toSlateValue( - getValueOrInitialValue(props.value, [this.slateInstance.createPlaceholderBlock()]), - {portableTextFeatures: this.portableTextFeatures}, + getValueOrInitialValue(props.value, [ + this.slateInstance.createPlaceholderBlock(), + ] as PortableTextBlock[]), + {types: this.types}, KEY_TO_SLATE_ELEMENT.get(this.slateInstance) ), } @@ -293,16 +304,13 @@ export class PortableTextEditor extends React.Component< return } // If the editor is empty and there is a new value, just set that value directly. - if ( - isEqualToEmptyEditor(this.slateInstance.children, this.portableTextFeatures) && - this.props.value - ) { + if (isEqualToEmptyEditor(this.slateInstance.children, this.types) && this.props.value) { const oldSel = this.slateInstance.selection Transforms.deselect(this.slateInstance) this.slateInstance.children = toSlateValue( val, { - portableTextFeatures: this.portableTextFeatures, + types: this.types, }, KEY_TO_SLATE_ELEMENT.get(this.slateInstance) ) @@ -317,7 +325,7 @@ export class PortableTextEditor extends React.Component< const isEqualToValue = !(val || []).some((blk, index) => { const compareBlock = toSlateValue( [blk], - {portableTextFeatures: this.portableTextFeatures}, + {types: this.types}, KEY_TO_SLATE_ELEMENT.get(this.slateInstance) )[0] if (!isEqual(compareBlock, this.slateInstance.children[index])) { @@ -331,7 +339,7 @@ export class PortableTextEditor extends React.Component< } // Value is different - validate it. debug('Validating') - const validation = validateValue(val, this.portableTextFeatures, this.keyGenerator) + const validation = validateValue(val, this.types, this.keyGenerator) if (val && !validation.valid) { this.change$.next({ type: 'invalidValue', @@ -348,7 +356,7 @@ export class PortableTextEditor extends React.Component< const slateValueFromProps = toSlateValue( val, { - portableTextFeatures: this.portableTextFeatures, + types: this.types, }, KEY_TO_SLATE_ELEMENT.get(this.slateInstance) ) @@ -378,12 +386,12 @@ export class PortableTextEditor extends React.Component< }) // Static API methods - static activeAnnotations = (editor: PortableTextEditor): PortableTextBlock[] => { + static activeAnnotations = (editor: PortableTextEditor): PortableTextObject[] => { return editor && editor.editable ? editor.editable.activeAnnotations() : [] } static addAnnotation = ( editor: PortableTextEditor, - type: Type, + type: ObjectSchemaType, value?: {[prop: string]: unknown} ): {spanPath: Path; markDefPath: Path} | undefined => editor.editable?.addAnnotation(type, value) static blur = (editor: PortableTextEditor): void => { @@ -415,8 +423,8 @@ export class PortableTextEditor extends React.Component< static focusChild = (editor: PortableTextEditor): PortableTextChild | undefined => { return editor.editable?.focusChild() } - static getPortableTextFeatures = (editor: PortableTextEditor) => { - return editor.portableTextFeatures + static getTypes = (editor: PortableTextEditor) => { + return editor.types } static getSelection = (editor: PortableTextEditor) => { return editor.editable ? editor.editable.getSelection() : null @@ -438,7 +446,7 @@ export class PortableTextEditor extends React.Component< editor.editable?.isMarkActive(mark) static insertChild = ( editor: PortableTextEditor, - type: Type, + type: SpanSchemaType | ObjectSchemaType, value?: {[prop: string]: unknown} ): Path | undefined => { debug(`Host inserting child`) @@ -446,7 +454,7 @@ export class PortableTextEditor extends React.Component< } static insertBlock = ( editor: PortableTextEditor, - type: Type, + type: BlockSchemaType | ObjectSchemaType, value?: {[prop: string]: unknown} ): Path | undefined => { return editor.editable?.insertBlock(type, value) @@ -467,7 +475,7 @@ export class PortableTextEditor extends React.Component< debug(`Host setting selection`, selection) editor.editable?.select(selection) } - static removeAnnotation = (editor: PortableTextEditor, type: Type) => + static removeAnnotation = (editor: PortableTextEditor, type: ObjectSchemaType) => editor.editable?.removeAnnotation(type) static toggleBlockStyle = (editor: PortableTextEditor, blockStyle: string) => { debug(`Host is toggling block style`) diff --git a/packages/@sanity/portable-text-editor/src/editor/__tests__/PortableTextEditor.test.tsx b/packages/@sanity/portable-text-editor/src/editor/__tests__/PortableTextEditor.test.tsx index d8b7bf9d2e3..c296d74732f 100644 --- a/packages/@sanity/portable-text-editor/src/editor/__tests__/PortableTextEditor.test.tsx +++ b/packages/@sanity/portable-text-editor/src/editor/__tests__/PortableTextEditor.test.tsx @@ -6,8 +6,9 @@ import '@testing-library/jest-dom/extend-expect' import React from 'react' import {render, waitFor} from '@testing-library/react' +import {PortableTextBlock} from '@sanity/types' import {PortableTextEditor} from '../PortableTextEditor' -import {EditorSelection, PortableTextBlock} from '../..' +import {EditorSelection} from '../..' import {PortableTextEditorTester, type} from './PortableTextEditorTester' const helloBlock: PortableTextBlock = { diff --git a/packages/@sanity/portable-text-editor/src/editor/__tests__/PortableTextEditorTester.tsx b/packages/@sanity/portable-text-editor/src/editor/__tests__/PortableTextEditorTester.tsx index e3a6a8d1268..b4fccff39f3 100644 --- a/packages/@sanity/portable-text-editor/src/editor/__tests__/PortableTextEditorTester.tsx +++ b/packages/@sanity/portable-text-editor/src/editor/__tests__/PortableTextEditorTester.tsx @@ -1,22 +1,22 @@ import React, {ForwardedRef, forwardRef, useCallback, useEffect} from 'react' import Schema from '@sanity/schema' -import {RawType} from '../../types/schema' +import {defineArrayMember, defineField} from '@sanity/types' import {PortableTextEditor, PortableTextEditable} from '../../index' import type {PortableTextEditorProps, PortableTextEditableProps} from '../../index' -const imageType: RawType = { +const imageType = defineField({ type: 'image', name: 'blockImage', -} +}) -const someObject: RawType = { +const someObject = defineField({ type: 'object', name: 'someObject', fields: [{type: 'string', name: 'color'}], -} +}) -const blockType: RawType = { +const blockType = defineField({ type: 'block', name: 'myTestBlockType', styles: [ @@ -30,13 +30,13 @@ const blockType: RawType = { {title: 'Quote', value: 'blockquote'}, ], of: [someObject, imageType], -} +}) -const portableTextType: RawType = { +const portableTextType = defineArrayMember({ type: 'array', name: 'body', of: [blockType, someObject], -} +}) const schema = Schema.compile({ name: 'test', diff --git a/packages/@sanity/portable-text-editor/src/editor/hooks/usePortableTextEditorValue.ts b/packages/@sanity/portable-text-editor/src/editor/hooks/usePortableTextEditorValue.ts index 650b3c53eab..18d312d9ccf 100644 --- a/packages/@sanity/portable-text-editor/src/editor/hooks/usePortableTextEditorValue.ts +++ b/packages/@sanity/portable-text-editor/src/editor/hooks/usePortableTextEditorValue.ts @@ -1,5 +1,5 @@ +import {PortableTextBlock} from '@sanity/types' import {createContext, useContext} from 'react' -import {PortableTextBlock} from '../../types/portableText' /** * A React context for sharing the editor value. diff --git a/packages/@sanity/portable-text-editor/src/editor/nodes/DefaultAnnotation.tsx b/packages/@sanity/portable-text-editor/src/editor/nodes/DefaultAnnotation.tsx index 04d993e09b1..e277931a32a 100644 --- a/packages/@sanity/portable-text-editor/src/editor/nodes/DefaultAnnotation.tsx +++ b/packages/@sanity/portable-text-editor/src/editor/nodes/DefaultAnnotation.tsx @@ -1,8 +1,8 @@ +import {PortableTextObject} from '@sanity/types' import React, {useCallback} from 'react' -import {PortableTextBlock} from '../../types/portableText' type Props = { - annotation: PortableTextBlock + annotation: PortableTextObject children: React.ReactNode } export function DefaultAnnotation(props: Props) { diff --git a/packages/@sanity/portable-text-editor/src/editor/nodes/DefaultObject.tsx b/packages/@sanity/portable-text-editor/src/editor/nodes/DefaultObject.tsx index 1b59feb9661..c9b588efef7 100644 --- a/packages/@sanity/portable-text-editor/src/editor/nodes/DefaultObject.tsx +++ b/packages/@sanity/portable-text-editor/src/editor/nodes/DefaultObject.tsx @@ -1,12 +1,16 @@ +import {PortableTextBlock, PortableTextChild} from '@sanity/types' import React from 'react' -import {PortableTextBlock, PortableTextChild} from '../../types/portableText' type Props = { value: PortableTextBlock | PortableTextChild } const DefaultObject = (props: Props): JSX.Element => { - return
{JSON.stringify(props.value, null, 2)}
+ return ( +
+
{JSON.stringify(props.value, null, 2)}
+
+ ) } export default DefaultObject diff --git a/packages/@sanity/portable-text-editor/src/editor/nodes/TextBlock.tsx b/packages/@sanity/portable-text-editor/src/editor/nodes/TextBlock.tsx deleted file mode 100644 index bfb84a59e7e..00000000000 --- a/packages/@sanity/portable-text-editor/src/editor/nodes/TextBlock.tsx +++ /dev/null @@ -1,41 +0,0 @@ -import React from 'react' -import {PortableTextBlock, PortableTextFeatures} from '../../types/portableText' -import {DefaultListItem, DefaultListItemInner} from '.' - -type Props = { - children: JSX.Element - block: PortableTextBlock - portableTextFeatures: PortableTextFeatures -} -export default function TextBlock(props: Props) { - const {portableTextFeatures, children, block} = props - const style = block.style || portableTextFeatures.styles[0].value - // Should we render a custom style? - // TODO: Look into this API. This is legacy support for older Sanity Studio versions via the type - let CustomStyle - const blockStyle = - portableTextFeatures && style - ? portableTextFeatures.styles.find((item) => item.value === style) - : undefined - if (blockStyle) { - CustomStyle = blockStyle.blockEditor && blockStyle.blockEditor.render - } - - let renderedBlock = children - if ('listItem' in block && block.listItem) { - renderedBlock = ( - - {renderedBlock} - - ) - } - return ( - <> - {!CustomStyle && renderedBlock} - {CustomStyle && {renderedBlock}} - - ) -} diff --git a/packages/@sanity/portable-text-editor/src/editor/plugins/__tests__/withPortableTextMarkModelNormalization.test.tsx b/packages/@sanity/portable-text-editor/src/editor/plugins/__tests__/withPortableTextMarkModelNormalization.test.tsx index 106fd8ec25f..a80f0f3e97d 100644 --- a/packages/@sanity/portable-text-editor/src/editor/plugins/__tests__/withPortableTextMarkModelNormalization.test.tsx +++ b/packages/@sanity/portable-text-editor/src/editor/plugins/__tests__/withPortableTextMarkModelNormalization.test.tsx @@ -3,10 +3,10 @@ */ // eslint-disable-next-line import/no-unassigned-import import '@testing-library/jest-dom/extend-expect' -import {act} from 'react-dom/test-utils' import {render, waitFor} from '@testing-library/react' import React from 'react' +import {ObjectSchemaType} from '@sanity/types' import {PortableTextEditor} from '../../PortableTextEditor' import {PortableTextEditorTester, type} from '../../../editor/__tests__/PortableTextEditorTester' import {EditorSelection} from '../../../types/editor' @@ -92,10 +92,8 @@ describe('plugin:withPortableTextMarksModel: normalization', () => { focus: {path: [{_key: '5fc57af23597'}, 'children', {_key: '11c8c9f783a8'}], offset: 4}, anchor: {path: [{_key: '5fc57af23597'}, 'children', {_key: '11c8c9f783a8'}], offset: 0}, }) - const linkType = PortableTextEditor.getPortableTextFeatures( - editorRef.current - // eslint-disable-next-line max-nested-callbacks - ).annotations.find((a) => a.type.name === 'link')?.type + // eslint-disable-next-line max-nested-callbacks + const linkType = editorRef.current.types.annotations.find((a) => a.name === 'link') if (!linkType) { throw new Error('No link type found') } diff --git a/packages/@sanity/portable-text-editor/src/editor/plugins/createWithEditableAPI.ts b/packages/@sanity/portable-text-editor/src/editor/plugins/createWithEditableAPI.ts index 8d5b7a9b104..8c9b9292aaf 100644 --- a/packages/@sanity/portable-text-editor/src/editor/plugins/createWithEditableAPI.ts +++ b/packages/@sanity/portable-text-editor/src/editor/plugins/createWithEditableAPI.ts @@ -1,12 +1,18 @@ import {Text, Range, Transforms, Editor, Element as SlateElement, Node} from 'slate' -import {Path} from '@sanity/types' +import { + ObjectSchemaType, + Path, + PortableTextBlock, + PortableTextChild, + PortableTextObject, + SchemaType, +} from '@sanity/types' import {ReactEditor} from '@sanity/slate-react' import {DOMNode} from '@sanity/slate-react/dist/utils/dom' -import {Type} from '../../types/schema' -import {PortableTextBlock, PortableTextChild, PortableTextFeatures} from '../../types/portableText' import { EditableAPIDeleteOptions, EditorSelection, + PortableTextMemberTypes, PortableTextSlateEditor, } from '../../types/editor' import {toSlateValue, fromSlateValue} from '../../utils/values' @@ -20,7 +26,7 @@ const debug = debugWithName('API:editable') export function createWithEditableAPI( portableTextEditor: PortableTextEditor, - portableTextFeatures: PortableTextFeatures, + types: PortableTextMemberTypes, keyGenerator: () => string ) { return function withEditableAPI(editor: PortableTextSlateEditor): PortableTextSlateEditor { @@ -86,11 +92,7 @@ export function createWithEditableAPI( }) )[0] || [undefined] if (block) { - return fromSlateValue( - [block], - portableTextFeatures.types.block.name, - KEY_TO_VALUE_ELEMENT.get(editor) - )[0] + return fromSlateValue([block], types.block.name, KEY_TO_VALUE_ELEMENT.get(editor))[0] } } catch (err) { return undefined @@ -112,14 +114,17 @@ export function createWithEditableAPI( if (node && !Editor.isBlock(editor, node)) { const pseudoBlock: PortableTextBlock = { _key: 'pseudo', - _type: portableTextFeatures.types.block.name, + _type: types.block.name, children: [node], } - return fromSlateValue( + const blocks = fromSlateValue( [pseudoBlock], - portableTextFeatures.types.block.name, + types.block.name, KEY_TO_VALUE_ELEMENT.get(editor) - )[0].children[0] + ) + if (editor.isTextBlock(blocks[0])) { + return blocks[0].children[0] + } } } catch (err) { return undefined @@ -127,7 +132,7 @@ export function createWithEditableAPI( } return undefined }, - insertChild: (type: Type, value?: {[prop: string]: any}): Path => { + insertChild: (type: SchemaType, value?: {[prop: string]: any}): Path => { if (!editor.selection) { throw new Error('The editor has no selection') } @@ -147,7 +152,7 @@ export function createWithEditableAPI( [ { _key: keyGenerator(), - _type: portableTextFeatures.types.block.name, + _type: types.block.name, children: [ { _key: keyGenerator(), @@ -164,17 +169,13 @@ export function createWithEditableAPI( editor.onChange() return ( toPortableTextRange( - fromSlateValue( - editor.children, - portableTextFeatures.types.block.name, - KEY_TO_VALUE_ELEMENT.get(editor) - ), + fromSlateValue(editor.children, types.block.name, KEY_TO_VALUE_ELEMENT.get(editor)), editor.selection, - portableTextFeatures + types )?.focus.path || [] ) }, - insertBlock: (type: Type, value?: {[prop: string]: any}): Path => { + insertBlock: (type: SchemaType, value?: {[prop: string]: any}): Path => { if (!editor.selection) { throw new Error('The editor has no selection') } @@ -192,13 +193,9 @@ export function createWithEditableAPI( editor.onChange() return ( toPortableTextRange( - fromSlateValue( - editor.children, - portableTextFeatures.types.block.name, - KEY_TO_VALUE_ELEMENT.get(editor) - ), + fromSlateValue(editor.children, types.block.name, KEY_TO_VALUE_ELEMENT.get(editor)), editor.selection, - portableTextFeatures + types )?.focus.path || [] ) }, @@ -221,10 +218,7 @@ export function createWithEditableAPI( } }, isVoid: (element: PortableTextBlock | PortableTextChild) => { - return ![ - portableTextFeatures.types.block.name, - portableTextFeatures.types.span.name, - ].includes(element._type) + return ![types.block.name, types.span.name].includes(element._type) }, findByPath: ( path: Path @@ -237,19 +231,18 @@ export function createWithEditableAPI( const [block, blockPath] = Editor.node(editor, slatePath.focus.path.slice(0, 1)) if (block && blockPath && typeof block._key === 'string') { if (path.length === 1 && slatePath.focus.path.length === 1) { - return [ - fromSlateValue([block], portableTextFeatures.types.block.name)[0], - [{_key: block._key}], - ] + return [fromSlateValue([block], types.block.name)[0], [{_key: block._key}]] } const ptBlock = fromSlateValue( [block], - portableTextFeatures.types.block.name, + types.block.name, KEY_TO_VALUE_ELEMENT.get(editor) )[0] - const ptChild = ptBlock.children[slatePath.focus.path[1]] - if (ptChild) { - return [ptChild, [{_key: block._key}, 'children', {_key: ptChild._key}]] + if (editor.isTextBlock(ptBlock)) { + const ptChild = ptBlock.children[slatePath.focus.path[1]] + if (ptChild) { + return [ptChild, [{_key: block._key}, 'children', {_key: ptChild._key}]] + } } } } @@ -270,12 +263,12 @@ export function createWithEditableAPI( } return node }, - activeAnnotations: (): PortableTextBlock[] => { + activeAnnotations: (): PortableTextObject[] => { if (!editor.selection || editor.selection.focus.path.length < 2) { return [] } try { - const activeAnnotations: PortableTextBlock[] = [] + const activeAnnotations: PortableTextObject[] = [] const spans = Editor.nodes(editor, { at: editor.selection, match: (node) => @@ -287,7 +280,7 @@ export function createWithEditableAPI( for (const [span, path] of spans) { const [block] = Editor.node(editor, path, {depth: 1}) if (editor.isTextBlock(block)) { - block.markDefs.forEach((def) => { + block.markDefs?.forEach((def) => { if ( Text.isText(span) && span.marks && @@ -305,22 +298,22 @@ export function createWithEditableAPI( } }, addAnnotation: ( - type: Type, - value?: {[prop: string]: PortableTextBlock} + type: ObjectSchemaType, + value?: {[prop: string]: unknown} ): {spanPath: Path; markDefPath: Path} | undefined => { const {selection} = editor if (selection) { const [block] = Editor.node(editor, selection.focus, {depth: 1}) - if ( - SlateElement.isElement(block) && - block._type === portableTextFeatures.types.block.name - ) { + if (SlateElement.isElement(block) && block._type === types.block.name) { const annotationKey = keyGenerator() if (editor.isTextBlock(block)) { Transforms.setNodes( editor, { - markDefs: [...block.markDefs, {_type: type.name, _key: annotationKey, ...value}], + markDefs: [ + ...(block.markDefs || []), + {_type: type.name, _key: annotationKey, ...value} as PortableTextObject, + ], }, {at: selection.focus} ) @@ -342,7 +335,7 @@ export function createWithEditableAPI( }, { at: editor.selection, - match: (n) => n._type === portableTextFeatures.types.span.name, + match: (n) => n._type === types.span.name, } ) editor.onChange() @@ -353,11 +346,11 @@ export function createWithEditableAPI( const newSelection = toPortableTextRange( fromSlateValue( editor.children, - portableTextFeatures.types.block.name, + types.block.name, KEY_TO_VALUE_ELEMENT.get(editor) ), editor.selection, - portableTextFeatures + types ) // eslint-disable-next-line max-depth if (newSelection && typeof block._key === 'string') { @@ -411,7 +404,7 @@ export function createWithEditableAPI( } debug(`Deleting children touched by selection`) return ( - node._type === portableTextFeatures.types.span.name || // Text children + node._type === types.span.name || // Text children (!editor.isTextBlock(node) && SlateElement.isElement(node)) // inline blocks ) }, @@ -428,7 +421,7 @@ export function createWithEditableAPI( } } }, - removeAnnotation: (type: Type): void => { + removeAnnotation: (type: ObjectSchemaType): void => { let {selection} = editor debug('Removing annotation', type) if (selection) { @@ -466,7 +459,7 @@ export function createWithEditableAPI( const [block] = Editor.node(editor, path, {depth: 1}) if (editor.isTextBlock(block)) { block.markDefs - .filter((def) => def._type === type.name) + ?.filter((def) => def._type === type.name) .forEach((def) => { if ( Text.isText(span) && @@ -499,24 +492,16 @@ export function createWithEditableAPI( return existing } ptRange = toPortableTextRange( - fromSlateValue( - editor.children, - portableTextFeatures.types.block.name, - KEY_TO_VALUE_ELEMENT.get(editor) - ), + fromSlateValue(editor.children, types.block.name, KEY_TO_VALUE_ELEMENT.get(editor)), editor.selection, - portableTextFeatures + types ) SLATE_TO_PORTABLE_TEXT_RANGE.set(editor.selection, ptRange) } return ptRange }, getValue: () => { - return fromSlateValue( - editor.children, - portableTextFeatures.types.block.name, - KEY_TO_VALUE_ELEMENT.get(editor) - ) + return fromSlateValue(editor.children, types.block.name, KEY_TO_VALUE_ELEMENT.get(editor)) }, isCollapsedSelection: () => { return !!editor.selection && Range.isCollapsed(editor.selection) diff --git a/packages/@sanity/portable-text-editor/src/editor/plugins/createWithHotKeys.ts b/packages/@sanity/portable-text-editor/src/editor/plugins/createWithHotKeys.ts index 429f7a6a3c9..9d7444abc76 100644 --- a/packages/@sanity/portable-text-editor/src/editor/plugins/createWithHotKeys.ts +++ b/packages/@sanity/portable-text-editor/src/editor/plugins/createWithHotKeys.ts @@ -3,8 +3,8 @@ import {Editor, Transforms, Path, Range} from 'slate' import isHotkey from 'is-hotkey' import {ReactEditor} from '@sanity/slate-react' -import {PortableTextFeatures} from '../../types/portableText' -import {PortableTextSlateEditor} from '../../types/editor' +import {isPortableTextSpan, isPortableTextTextBlock} from '@sanity/types' +import {PortableTextMemberTypes, PortableTextSlateEditor} from '../../types/editor' import {HotkeyOptions} from '../../types/options' import {debugWithName} from '../../utils/debug' import {toSlateValue} from '../../utils/values' @@ -27,7 +27,7 @@ const DEFAULT_HOTKEYS: HotkeyOptions = { * */ export function createWithHotkeys( - portableTextFeatures: PortableTextFeatures, + types: PortableTextMemberTypes, keyGenerator: () => string, portableTextEditor: PortableTextEditor, hotkeysFromOptions?: HotkeyOptions @@ -38,7 +38,7 @@ export function createWithHotkeys( toSlateValue( [ { - _type: portableTextFeatures.types.block.name, + _type: types.block.name, _key: keyGenerator(), style: 'normal', markDefs: [], @@ -154,8 +154,29 @@ export function createWithHotkeys( } // Tab for lists - if (isTab || isShiftTab) { - if (editor.pteIncrementBlockLevels(isShiftTab)) { + // Only steal tab when we are on a plain text span or we are at the start of the line (fallback if the whole block is annotated or contains a single inline object) + // Otherwise tab is reserved for accessability for buttons etc. + if ((isTab || isShiftTab) && editor.selection) { + const [focusChild] = Editor.node(editor, editor.selection.focus, {depth: 2}) + const [focusBlock] = isPortableTextSpan(focusChild) + ? Editor.node(editor, editor.selection.focus, {depth: 1}) + : [] + const hasAnnotationFocus = + focusChild && + isPortableTextTextBlock(focusBlock) && + isPortableTextSpan(focusChild) && + (focusChild.marks || ([] as string[])).filter((m) => + (focusBlock.markDefs || []).map((def) => def._key).includes(m) + ).length > 0 + const [start] = Range.edges(editor.selection) + const atStartOfNode = Editor.isStart(editor, start, start.path) + + if ( + focusChild && + isPortableTextSpan(focusChild) && + (!hasAnnotationFocus || atStartOfNode) && + editor.pteIncrementBlockLevels(isShiftTab) + ) { event.preventDefault() } } @@ -180,7 +201,7 @@ export function createWithHotkeys( if ( editor.isTextBlock(focusBlock) && focusBlock.style && - focusBlock.style !== portableTextFeatures.styles[0].value + focusBlock.style !== types.styles[0].value ) { const [, end] = Range.edges(editor.selection) const endAtEndOfNode = Editor.isEnd(editor, end, end.path) diff --git a/packages/@sanity/portable-text-editor/src/editor/plugins/createWithInsertData.ts b/packages/@sanity/portable-text-editor/src/editor/plugins/createWithInsertData.ts index f8ae6870397..9c2afe0a79c 100644 --- a/packages/@sanity/portable-text-editor/src/editor/plugins/createWithInsertData.ts +++ b/packages/@sanity/portable-text-editor/src/editor/plugins/createWithInsertData.ts @@ -1,8 +1,8 @@ -import {Node, Element, Transforms, Editor, Descendant, Range, Text} from 'slate' +import {Node, Transforms, Editor, Descendant, Range} from 'slate' import {htmlToBlocks, normalizeBlock} from '@sanity/block-tools' import {ReactEditor} from '@sanity/slate-react' -import {PortableTextFeatures, PortableTextBlock, PortableTextChild} from '../../types/portableText' -import {EditorChanges, PortableTextSlateEditor} from '../../types/editor' +import {PortableTextBlock, PortableTextChild} from '@sanity/types' +import {EditorChanges, PortableTextMemberTypes, PortableTextSlateEditor} from '../../types/editor' import {fromSlateValue, toSlateValue} from '../../utils/values' import {validateValue} from '../../utils/validateValue' import {debugWithName} from '../../utils/debug' @@ -15,33 +15,29 @@ const debug = debugWithName('plugin:withInsertData') */ export function createWithInsertData( change$: EditorChanges, - portableTextFeatures: PortableTextFeatures, + types: PortableTextMemberTypes, keyGenerator: () => string ) { return function withInsertData(editor: PortableTextSlateEditor): PortableTextSlateEditor { - const blockTypeName = portableTextFeatures.types.block.name - const spanTypeName = portableTextFeatures.types.span.name + const blockTypeName = types.block.name + const spanTypeName = types.span.name const toPlainText = (blocks: PortableTextBlock[]) => { return blocks .map((block) => { - if (block._type === blockTypeName) { + if (editor.isTextBlock(block)) { return block.children .map((child: PortableTextChild) => { if (child._type === spanTypeName) { return child.text } return `[${ - portableTextFeatures.types.inlineObjects.find((t) => t.name === child._type) - ?.title || 'Object' + types.inlineObjects.find((t) => t.name === child._type)?.title || 'Object' }]` }) .join('') } - return `[${ - portableTextFeatures.types.blockObjects.find((t) => t.name === block._type)?.title || - 'Object' - }]` + return `[${types.blockObjects.find((t) => t.name === block._type)?.title || 'Object'}]` }) .join('\n\n') } @@ -104,7 +100,7 @@ export function createWithInsertData( const asHTML = div.innerHTML contents.ownerDocument.body.removeChild(div) const fragment = editor.getFragment() - const portableText = fromSlateValue(fragment as Node[], portableTextFeatures.types.block.name) + const portableText = fromSlateValue(fragment, blockTypeName) const asJSON = JSON.stringify(portableText) const asPlainText = toPlainText(portableText) @@ -130,12 +126,12 @@ export function createWithInsertData( if (Array.isArray(parsed) && parsed.length > 0) { const slateValue = regenerateKeys( editor, - toSlateValue(parsed, {portableTextFeatures}), + toSlateValue(parsed, {types}), keyGenerator, spanTypeName ) // Validate the result - const validation = validateValue(parsed, portableTextFeatures, keyGenerator) + const validation = validateValue(parsed, types, keyGenerator) // Bail out if it's not valid if (!validation.valid) { const errorDescription = `${validation.resolution?.description}` @@ -173,10 +169,10 @@ export function createWithInsertData( let insertedType if (html) { - portableText = htmlToBlocks(html, portableTextFeatures.types.portableText).map((block) => + portableText = htmlToBlocks(html, types.portableText).map((block) => normalizeBlock(block, {blockTypeName}) - ) - fragment = toSlateValue(portableText, {portableTextFeatures}) + ) as PortableTextBlock[] + fragment = toSlateValue(portableText, {types}) insertedType = 'HTML' } else { // plain text @@ -187,17 +183,17 @@ export function createWithInsertData( ) .join('') const textToHtml = `${blocks}` - portableText = htmlToBlocks(textToHtml, portableTextFeatures.types.portableText).map( - (block) => normalizeBlock(block, {blockTypeName}) - ) + portableText = htmlToBlocks(textToHtml, types.portableText).map((block) => + normalizeBlock(block, {blockTypeName}) + ) as PortableTextBlock[] fragment = toSlateValue(portableText, { - portableTextFeatures, + types, }) insertedType = 'text' } // Validate the result - const validation = validateValue(portableText, portableTextFeatures, keyGenerator) + const validation = validateValue(portableText, types, keyGenerator) // Bail out if it's not valid if (!validation.valid) { @@ -232,7 +228,7 @@ export function createWithInsertData( editor.insertFragmentData = (data: DataTransfer): boolean => { const fragment = data.getData('application/x-portable-text') if (fragment) { - const parsed = JSON.parse(fragment) as Node[] + const parsed = JSON.parse(fragment) editor.insertFragment(parsed) return true } @@ -262,17 +258,17 @@ function regenerateKeys( fragment: Descendant[], keyGenerator: () => string, spanTypeName: string -) { +): Descendant[] { return fragment.map((node) => { - const newNode: Element = {...(node as Element)} + const newNode: Descendant = {...node} // Ensure the copy has new keys if (editor.isTextBlock(newNode)) { - newNode.markDefs = newNode.markDefs.map((def) => { + newNode.markDefs = (newNode.markDefs || []).map((def) => { const oldKey = def._key const newKey = keyGenerator() - if (Array.isArray(newNode.children)) { + if (editor.isTextBlock(newNode)) { newNode.children = newNode.children.map((child) => - child._type === spanTypeName && Text.isText(child) + child._type === spanTypeName && editor.isTextSpan(child) ? { ...child, marks: @@ -287,14 +283,14 @@ function regenerateKeys( return {...def, _key: newKey} }) } - const nodeWithNewKeys = {...newNode, _key: keyGenerator()} as Element + const nodeWithNewKeys = {...newNode, _key: keyGenerator()} if (editor.isTextBlock(nodeWithNewKeys)) { nodeWithNewKeys.children = nodeWithNewKeys.children.map((child) => ({ ...child, _key: keyGenerator(), })) } - return nodeWithNewKeys + return nodeWithNewKeys as Descendant }) } @@ -310,7 +306,7 @@ function mixMarkDefs(editor: PortableTextSlateEditor, fragment: any) { Transforms.setNodes( editor, { - markDefs: [...fragment[0].markDefs, ...markDefs], + markDefs: [...(fragment[0].markDefs || []), ...(markDefs || [])], }, {at: focusPath, mode: 'lowest', voids: false} ) diff --git a/packages/@sanity/portable-text-editor/src/editor/plugins/createWithObjectKeys.ts b/packages/@sanity/portable-text-editor/src/editor/plugins/createWithObjectKeys.ts index e35e6de34ee..18a99f0f948 100644 --- a/packages/@sanity/portable-text-editor/src/editor/plugins/createWithObjectKeys.ts +++ b/packages/@sanity/portable-text-editor/src/editor/plugins/createWithObjectKeys.ts @@ -1,16 +1,12 @@ import {Element, Transforms, Node, Editor} from 'slate' -import {PortableTextFeatures} from '../../types/portableText' -import {PortableTextSlateEditor} from '../../types/editor' +import {PortableTextMemberTypes, PortableTextSlateEditor} from '../../types/editor' import {isPreservingKeys, PRESERVE_KEYS} from '../../utils/withPreserveKeys' /** * This plugin makes sure that every new node in the editor get a new _key prop when created * */ -export function createWithObjectKeys( - portableTextFeatures: PortableTextFeatures, - keyGenerator: () => string -) { +export function createWithObjectKeys(types: PortableTextMemberTypes, keyGenerator: () => string) { return function withKeys(editor: PortableTextSlateEditor): PortableTextSlateEditor { PRESERVE_KEYS.set(editor, false) const {apply, normalizeNode} = editor @@ -35,7 +31,7 @@ export function createWithObjectKeys( } editor.normalizeNode = (entry) => { const [node, path] = entry - if (Element.isElement(node) && node._type === portableTextFeatures.types.block.name) { + if (Element.isElement(node) && node._type === types.block.name) { // Set key on block itself if (!node._key) { Transforms.setNodes(editor, {_key: keyGenerator()}, {at: path}) diff --git a/packages/@sanity/portable-text-editor/src/editor/plugins/createWithPatches.ts b/packages/@sanity/portable-text-editor/src/editor/plugins/createWithPatches.ts index 39c50f8f535..081305025f3 100644 --- a/packages/@sanity/portable-text-editor/src/editor/plugins/createWithPatches.ts +++ b/packages/@sanity/portable-text-editor/src/editor/plugins/createWithPatches.ts @@ -14,12 +14,12 @@ import { SplitNodeOperation, } from 'slate' import {debounce} from 'lodash' +import {PortableTextBlock} from '@sanity/types' import {insert, setIfMissing, unset} from '../../patch/PatchEvent' import type {Patch} from '../../types/patch' import {fromSlateValue, isEqualToEmptyEditor} from '../../utils/values' -import {PortableTextBlock, PortableTextFeatures} from '../../types/portableText' -import {EditorChange, PortableTextSlateEditor} from '../../types/editor' +import {EditorChange, PortableTextMemberTypes, PortableTextSlateEditor} from '../../types/editor' import {debugWithName} from '../../utils/debug' import {PATCHING, isPatching, withoutPatching} from '../../utils/withoutPatching' import {KEY_TO_VALUE_ELEMENT} from '../../utils/weakMaps' @@ -76,7 +76,7 @@ export interface PatchFunctions { interface Options { patchFunctions: PatchFunctions change$: Subject - portableTextFeatures: PortableTextFeatures + types: PortableTextMemberTypes syncValue: () => void incomingPatches$?: Observable<{ patches: Patch[] @@ -87,7 +87,7 @@ interface Options { export function createWithPatches({ patchFunctions, change$, - portableTextFeatures, + types, syncValue, incomingPatches$, }: Options): [ @@ -98,7 +98,7 @@ export function createWithPatches({ // The editor.children would no longer contain that information if the node is already deleted. let previousChildren: Descendant[] - const patchToOperations = createPatchToOperations(portableTextFeatures, defaultKeyGenerator) + const patchToOperations = createPatchToOperations(types, defaultKeyGenerator) let patchSubscription: Subscription const cleanupFn = () => { if (patchSubscription) { @@ -160,12 +160,12 @@ export function createWithPatches({ // Update previous children here before we apply previousChildren = editor.children - const editorWasEmpty = isEqualToEmptyEditor(previousChildren, portableTextFeatures) + const editorWasEmpty = isEqualToEmptyEditor(previousChildren, types) // Apply the operation apply(operation) - const editorIsEmpty = isEqualToEmptyEditor(editor.children, portableTextFeatures) + const editorIsEmpty = isEqualToEmptyEditor(editor.children, types) if (!isPatching(editor)) { debug(`Editor is not producing patch for operation ${operation.type}`, operation) @@ -177,9 +177,7 @@ export function createWithPatches({ if (editorWasEmpty && operation.type !== 'set_selection') { patches.push(setIfMissing([], [])) previousChildren.forEach((c, index) => { - patches.push( - insert(fromSlateValue([c], portableTextFeatures.types.block.name), 'before', [index]) - ) + patches.push(insert(fromSlateValue([c], types.block.name), 'before', [index])) }) } switch (operation.type) { @@ -243,7 +241,7 @@ export function createWithPatches({ type: 'unset', previousValue: fromSlateValue( previousChildren, - portableTextFeatures.types.block.name, + types.block.name, KEY_TO_VALUE_ELEMENT.get(editor) ), }) diff --git a/packages/@sanity/portable-text-editor/src/editor/plugins/createWithPlaceholderBlock.ts b/packages/@sanity/portable-text-editor/src/editor/plugins/createWithPlaceholderBlock.ts index 31a91e32bbf..956e90c8238 100644 --- a/packages/@sanity/portable-text-editor/src/editor/plugins/createWithPlaceholderBlock.ts +++ b/packages/@sanity/portable-text-editor/src/editor/plugins/createWithPlaceholderBlock.ts @@ -1,6 +1,5 @@ import {Transforms, Descendant} from 'slate' -import {PortableTextSlateEditor} from '../../types/editor' -import {PortableTextFeatures} from '../../types/portableText' +import {PortableTextMemberTypes, PortableTextSlateEditor} from '../../types/editor' import {debugWithName} from '../../utils/debug' import {withoutPatching} from '../../utils/withoutPatching' import {withoutSaving} from './createWithUndoRedo' @@ -8,7 +7,7 @@ import {withoutSaving} from './createWithUndoRedo' const debug = debugWithName('plugin:withPlaceholderBlock') interface Options { - portableTextFeatures: PortableTextFeatures + types: PortableTextMemberTypes keyGenerator: () => string } /** @@ -16,15 +15,15 @@ interface Options { * */ export function createWithPlaceholderBlock({ - portableTextFeatures, + types, keyGenerator, }: Options): (editor: PortableTextSlateEditor) => PortableTextSlateEditor { return function withPlaceholderBlock(editor: PortableTextSlateEditor): PortableTextSlateEditor { editor.createPlaceholderBlock = (): Descendant => { return { - _type: portableTextFeatures.types.block.name, + _type: types.block.name, _key: keyGenerator(), - style: portableTextFeatures.styles[0].value, + style: types.styles[0].value, markDefs: [], children: [ { diff --git a/packages/@sanity/portable-text-editor/src/editor/plugins/createWithPortableTextBlockStyle.ts b/packages/@sanity/portable-text-editor/src/editor/plugins/createWithPortableTextBlockStyle.ts index 926289aca92..595d9062d77 100644 --- a/packages/@sanity/portable-text-editor/src/editor/plugins/createWithPortableTextBlockStyle.ts +++ b/packages/@sanity/portable-text-editor/src/editor/plugins/createWithPortableTextBlockStyle.ts @@ -1,7 +1,6 @@ import {Subject} from 'rxjs' -import {Editor, Transforms, Element, Path, Text as SlateText} from 'slate' -import {PortableTextBlock, PortableTextFeatures} from '../../types/portableText' -import {EditorChange, PortableTextSlateEditor} from '../../types/editor' +import {Editor, Transforms, Element, Path, Text as SlateText, Node} from 'slate' +import {EditorChange, PortableTextMemberTypes, PortableTextSlateEditor} from '../../types/editor' import {debugWithName} from '../../utils/debug' import {toPortableTextRange} from '../../utils/ranges' import {fromSlateValue} from '../../utils/values' @@ -9,10 +8,10 @@ import {fromSlateValue} from '../../utils/values' const debug = debugWithName('plugin:withPortableTextBlockStyle') export function createWithPortableTextBlockStyle( - portableTextFeatures: PortableTextFeatures, + types: PortableTextMemberTypes, change$: Subject ): (editor: PortableTextSlateEditor) => PortableTextSlateEditor { - const defaultStyle = portableTextFeatures.styles[0].value + const defaultStyle = types.styles[0].value return function withPortableTextBlockStyle( editor: PortableTextSlateEditor ): PortableTextSlateEditor { @@ -63,14 +62,13 @@ export function createWithPortableTextBlockStyle( const selectedBlocks = [ ...Editor.nodes(editor, { at: editor.selection, - match: (node) => - Element.isElement(node) && node._type === portableTextFeatures.types.block.name, + match: (node) => Element.isElement(node) && node._type === types.block.name, }), ] selectedBlocks.forEach(([node, path]) => { if (editor.isTextBlock(node) && node.style === blockStyle) { debug(`Unsetting block style '${blockStyle}'`) - Transforms.setNodes(editor, {...node, style: defaultStyle} as PortableTextBlock, { + Transforms.setNodes(editor, {...node, style: defaultStyle} as Partial, { at: path, }) } else { @@ -84,7 +82,7 @@ export function createWithPortableTextBlockStyle( { ...node, style: blockStyle || defaultStyle, - } as PortableTextBlock, + } as Partial, {at: path} ) } @@ -94,9 +92,9 @@ export function createWithPortableTextBlockStyle( change$.next({ type: 'selection', selection: toPortableTextRange( - fromSlateValue(editor.children, portableTextFeatures.types.block.name), + fromSlateValue(editor.children, types.block.name), editor.selection, - portableTextFeatures + types ), }) editor.onChange() diff --git a/packages/@sanity/portable-text-editor/src/editor/plugins/createWithPortableTextLists.ts b/packages/@sanity/portable-text-editor/src/editor/plugins/createWithPortableTextLists.ts index fd841633a43..a2663ffef95 100644 --- a/packages/@sanity/portable-text-editor/src/editor/plugins/createWithPortableTextLists.ts +++ b/packages/@sanity/portable-text-editor/src/editor/plugins/createWithPortableTextLists.ts @@ -1,12 +1,11 @@ -import {Editor, Transforms, Element, Text} from 'slate' -import {PortableTextBlock, PortableTextFeatures, TextBlock} from '../../types/portableText' -import {PortableTextSlateEditor} from '../../types/editor' +import {Editor, Transforms, Element, Text, Node} from 'slate' +import {PortableTextMemberTypes, PortableTextSlateEditor} from '../../types/editor' import {debugWithName} from '../../utils/debug' const debug = debugWithName('plugin:withPortableTextLists') const MAX_LIST_LEVEL = 10 -export function createWithPortableTextLists(portableTextFeatures: PortableTextFeatures) { +export function createWithPortableTextLists(types: PortableTextMemberTypes) { return function withPortableTextLists(editor: PortableTextSlateEditor): PortableTextSlateEditor { editor.pteToggleListItem = (listItemStyle: string) => { const isActive = editor.pteHasListStyle(listItemStyle) @@ -26,8 +25,7 @@ export function createWithPortableTextLists(portableTextFeatures: PortableTextFe const selectedBlocks = [ ...Editor.nodes(editor, { at: editor.selection, - match: (node) => - Element.isElement(node) && node._type === portableTextFeatures.types.block.name, + match: (node) => Element.isElement(node) && node._type === types.block.name, }), ] selectedBlocks.forEach(([node, path]) => { @@ -38,7 +36,7 @@ export function createWithPortableTextLists(portableTextFeatures: PortableTextFe ...rest, listItem: undefined, level: undefined, - } as PortableTextBlock + } as Partial debug(`Unsetting list '${listItemStyle}'`) Transforms.setNodes(editor, newNode, {at: path}) } @@ -62,10 +60,8 @@ export function createWithPortableTextLists(portableTextFeatures: PortableTextFe { ...node, level: 1, - listItem: - listItemStyle || - (portableTextFeatures.lists[0] && portableTextFeatures.lists[0].value), - } as PortableTextBlock, + listItem: listItemStyle || (types.lists[0] && types.lists[0].value), + } as Partial, {at: path} ) }) @@ -94,12 +90,11 @@ export function createWithPortableTextLists(portableTextFeatures: PortableTextFe debug('Unset list') Transforms.setNodes( editor, - // @todo: fix typing { ...node, level: undefined, listItem: undefined, - } as any, + }, {at: path} ) } diff --git a/packages/@sanity/portable-text-editor/src/editor/plugins/createWithPortableTextMarkModel.ts b/packages/@sanity/portable-text-editor/src/editor/plugins/createWithPortableTextMarkModel.ts index 130f703470d..f4fa1db518a 100644 --- a/packages/@sanity/portable-text-editor/src/editor/plugins/createWithPortableTextMarkModel.ts +++ b/packages/@sanity/portable-text-editor/src/editor/plugins/createWithPortableTextMarkModel.ts @@ -9,18 +9,16 @@ import {isEqual, flatten, uniq} from 'lodash' import {Editor, Range, Transforms, Text, Path, NodeEntry, Element} from 'slate' import {debugWithName} from '../../utils/debug' -import {PortableTextSlateEditor} from '../../types/editor' -import {PortableTextFeatures} from '../../types/portableText' +import {PortableTextMemberTypes, PortableTextSlateEditor} from '../../types/editor' const debug = debugWithName('plugin:withPortableTextMarkModel') export function createWithPortableTextMarkModel( - portableTextFeatures: PortableTextFeatures, - keyGenerator: () => string + types: PortableTextMemberTypes ): (editor: PortableTextSlateEditor) => PortableTextSlateEditor { return function withPortableTextMarkModel(editor: PortableTextSlateEditor) { const {apply, normalizeNode} = editor - const decorators = portableTextFeatures.decorators.map((t) => t.value) + const decorators = types.decorators.map((t) => t.value) // Extend Slate's default normalization. Merge spans with same set of .marks when doing merge_node operations, and clean up markDefs / marks editor.normalizeNode = (nodeEntry) => { @@ -40,7 +38,7 @@ export function createWithPortableTextMarkModel( mergeSpans(editor) } const [node, path] = nodeEntry - const isSpan = Text.isText(node) && node._type === portableTextFeatures.types.span.name + const isSpan = Text.isText(node) && node._type === types.span.name const isTextBlock = editor.isTextBlock(node) if (isSpan || isTextBlock) { if (!isTextBlock && !Array.isArray(node.marks)) { @@ -54,7 +52,7 @@ export function createWithPortableTextMarkModel( op.type === 'merge_node' && op.path.length === 1 && 'markDefs' in op.properties && - op.properties._type === portableTextFeatures.types.block.name && + op.properties._type === types.block.name && Array.isArray(op.properties.markDefs) && op.properties.markDefs.length > 0 && op.path[0] - 1 >= 0 @@ -77,7 +75,7 @@ export function createWithPortableTextMarkModel( op.type === 'split_node' && op.path.length === 1 && Element.isElementProps(op.properties) && - op.properties._type === portableTextFeatures.types.block.name && + op.properties._type === types.block.name && 'markDefs' in op.properties && Array.isArray(op.properties.markDefs) && op.properties.markDefs.length > 0 && @@ -99,7 +97,7 @@ export function createWithPortableTextMarkModel( if ( op.type === 'split_node' && op.path.length === 2 && - op.properties._type === portableTextFeatures.types.span.name && + op.properties._type === types.span.name && 'marks' in op.properties && Array.isArray(op.properties.marks) && op.properties.marks.length > 0 && @@ -120,7 +118,7 @@ export function createWithPortableTextMarkModel( if ( op.type === 'split_node' && op.path.length === 1 && - op.properties._type === portableTextFeatures.types.block.name && + op.properties._type === types.block.name && 'markDefs' in op.properties && Array.isArray(op.properties.markDefs) && op.properties.markDefs.length > 0 @@ -129,10 +127,11 @@ export function createWithPortableTextMarkModel( if ( editor.isTextBlock(block) && block.children.length === 1 && + block.markDefs && block.markDefs.length > 0 && Text.isText(block.children[0]) && block.children[0].text === '' && - block.children[0].marks.length === 0 + (!block.children[0].marks || block.children[0].marks.length === 0) ) { Transforms.setNodes(editor, {markDefs: []}, {at: blockPath}) editor.onChange() @@ -169,7 +168,7 @@ export function createWithPortableTextMarkModel( Editor.nodes(editor, { mode: 'lowest', at: selection.focus, - match: (n) => n._type === portableTextFeatures.types.span.name, + match: (n) => n._type === types.span.name, voids: false, }) )[0] || [undefined] @@ -360,11 +359,11 @@ export function createWithPortableTextMarkModel( if (selection) { const blocks = Editor.nodes(editor, { at: selection, - match: (n) => n._type === portableTextFeatures.types.block.name, + match: (n) => n._type === types.block.name, }) for (const [block, path] of blocks) { if (editor.isTextBlock(block)) { - const newMarkDefs = block.markDefs.filter((def) => { + const newMarkDefs = (block.markDefs || []).filter((def) => { return block.children.find((child) => { return ( Text.isText(child) && Array.isArray(child.marks) && child.marks.includes(def._key) diff --git a/packages/@sanity/portable-text-editor/src/editor/plugins/createWithPortableTextSelections.ts b/packages/@sanity/portable-text-editor/src/editor/plugins/createWithPortableTextSelections.ts index e7f85317a71..798cac7de40 100644 --- a/packages/@sanity/portable-text-editor/src/editor/plugins/createWithPortableTextSelections.ts +++ b/packages/@sanity/portable-text-editor/src/editor/plugins/createWithPortableTextSelections.ts @@ -1,6 +1,10 @@ import {Subject} from 'rxjs' -import {EditorChange, EditorSelection, PortableTextSlateEditor} from '../../types/editor' -import {PortableTextFeatures} from '../../types/portableText' +import { + EditorChange, + EditorSelection, + PortableTextMemberTypes, + PortableTextSlateEditor, +} from '../../types/editor' import {debugWithName} from '../../utils/debug' import {toPortableTextRange} from '../../utils/ranges' import {fromSlateValue} from '../../utils/values' @@ -11,7 +15,7 @@ const debug = debugWithName('plugin:withPortableTextSelections') // This plugin will make sure that we emit a PT selection whenever the editor has changed. export function createWithPortableTextSelections( change$: Subject, - portableTextFeatures: PortableTextFeatures + types: PortableTextMemberTypes ) { return function withPortableTextSelections( editor: PortableTextSlateEditor @@ -24,13 +28,9 @@ export function createWithPortableTextSelections( ptRange = existing } else { ptRange = toPortableTextRange( - fromSlateValue( - editor.children, - portableTextFeatures.types.block.name, - KEY_TO_VALUE_ELEMENT.get(editor) - ), + fromSlateValue(editor.children, types.block.name, KEY_TO_VALUE_ELEMENT.get(editor)), editor.selection, - portableTextFeatures + types ) SLATE_TO_PORTABLE_TEXT_RANGE.set(editor.selection, ptRange) } diff --git a/packages/@sanity/portable-text-editor/src/editor/plugins/createWithSchemaTypes.ts b/packages/@sanity/portable-text-editor/src/editor/plugins/createWithSchemaTypes.ts index d4a85949d0e..9698edccf78 100644 --- a/packages/@sanity/portable-text-editor/src/editor/plugins/createWithSchemaTypes.ts +++ b/packages/@sanity/portable-text-editor/src/editor/plugins/createWithSchemaTypes.ts @@ -1,52 +1,40 @@ import {Element, Operation, InsertNodeOperation, Text as SlateText} from 'slate' -import {PortableTextFeatures, TextBlock, ListItem, TextSpan} from '../../types/portableText' +import { + isPortableTextTextBlock, + PortableTextTextBlock, + isPortableTextSpan, + PortableTextSpan, + PortableTextListBlock, + isPortableTextListBlock, +} from '@sanity/types' import {debugWithName} from '../../utils/debug' -import {PortableTextSlateEditor} from '../../types/editor' +import {PortableTextMemberTypes, PortableTextSlateEditor} from '../../types/editor' const debug = debugWithName('plugin:withSchemaTypes') /** - * This plugin makes sure that shema types are recognized properly by Slate as blocks, voids, inlines + * This plugin makes sure that schema types are recognized properly by Slate as blocks, voids, inlines * */ -export function createWithSchemaTypes(portableTextFeatures: PortableTextFeatures) { +export function createWithSchemaTypes(types: PortableTextMemberTypes) { return function withSchemaTypes(editor: PortableTextSlateEditor): PortableTextSlateEditor { - editor.isTextBlock = (value: any): value is TextBlock => { - return ( - !editor.isVoid(value) && - 'markDefs' in value && - 'style' in value && - 'children' in value && - '_type' in value && - portableTextFeatures.types.block.name === value._type - ) + editor.isTextBlock = (value: unknown): value is PortableTextTextBlock => { + return isPortableTextTextBlock(value) && value._type === types.block.name } - editor.isTextSpan = (value: any): value is TextSpan => { - return ( - !editor.isVoid(value) && - 'text' in value && - 'marks' in value && - '_type' in value && - portableTextFeatures.types.span.name === value._type - ) + editor.isTextSpan = (value: unknown): value is PortableTextSpan => { + return isPortableTextSpan(value) && value._type == types.span.name } - editor.isListBlock = (value: any): value is ListItem => { - return Boolean( - editor.isTextBlock(value) && - 'listItem' in value && - 'level' in value && - value.listItem && - Number.isInteger(value.level) - ) + editor.isListBlock = (value: unknown): value is PortableTextListBlock => { + return isPortableTextListBlock(value) && value._type === types.block.name } editor.isVoid = (element: Element): boolean => { return ( - portableTextFeatures.types.block.name !== element._type && - (portableTextFeatures.types.blockObjects.map((obj) => obj.name).includes(element._type) || - portableTextFeatures.types.inlineObjects.map((obj) => obj.name).includes(element._type)) + types.block.name !== element._type && + (types.blockObjects.map((obj) => obj.name).includes(element._type) || + types.inlineObjects.map((obj) => obj.name).includes(element._type)) ) } editor.isInline = (element: Element): boolean => { - const inlineSchemaTypes = portableTextFeatures.types.inlineObjects.map((obj) => obj.name) + const inlineSchemaTypes = types.inlineObjects.map((obj) => obj.name) return ( inlineSchemaTypes.includes(element._type) && '__inline' in element && diff --git a/packages/@sanity/portable-text-editor/src/editor/plugins/createWithUtils.ts b/packages/@sanity/portable-text-editor/src/editor/plugins/createWithUtils.ts index 72633a35579..64e65af1b65 100644 --- a/packages/@sanity/portable-text-editor/src/editor/plugins/createWithUtils.ts +++ b/packages/@sanity/portable-text-editor/src/editor/plugins/createWithUtils.ts @@ -1,19 +1,18 @@ -import {Editor, Range, Transforms, Text, Descendant} from 'slate' -import {PortableTextSlateEditor} from '../../types/editor' -import {PortableTextFeatures} from '../../types/portableText' +import {Editor, Range, Transforms, Text} from 'slate' +import {PortableTextMemberTypes, PortableTextSlateEditor} from '../../types/editor' import {debugWithName} from '../../utils/debug' const debug = debugWithName('plugin:withUtils') interface Options { - portableTextFeatures: PortableTextFeatures + types: PortableTextMemberTypes keyGenerator: () => string } /** * This plugin makes various util commands available in the editor * */ -export function createWithUtils({portableTextFeatures, keyGenerator}: Options) { +export function createWithUtils({types, keyGenerator}: Options) { return function withUtils(editor: PortableTextSlateEditor): PortableTextSlateEditor { // Expands the the selection to wrap around the word the focus is at editor.pteExpandToWord = () => { diff --git a/packages/@sanity/portable-text-editor/src/editor/plugins/index.ts b/packages/@sanity/portable-text-editor/src/editor/plugins/index.ts index 12af686dec6..55677a6e267 100644 --- a/packages/@sanity/portable-text-editor/src/editor/plugins/index.ts +++ b/packages/@sanity/portable-text-editor/src/editor/plugins/index.ts @@ -43,8 +43,7 @@ export const withPlugins = ( ): PortableTextSlateEditor => { const e = editor as T & PortableTextSlateEditor const {portableTextEditor} = options - const {portableTextFeatures, keyGenerator, readOnly, change$, syncValue, incomingPatches$} = - portableTextEditor + const {types, keyGenerator, readOnly, change$, syncValue, incomingPatches$} = portableTextEditor e.maxBlocks = portableTextEditor.maxBlocks || -1 e.readOnly = portableTextEditor.readOnly || false if (e.destroy) { @@ -58,41 +57,34 @@ export const withPlugins = ( normalizeNode: e.normalizeNode, }) } - const operationToPatches = createOperationToPatches(portableTextFeatures) - const withObjectKeys = createWithObjectKeys(portableTextFeatures, keyGenerator) - const withSchemaTypes = createWithSchemaTypes(portableTextFeatures) - const withEditableAPI = createWithEditableAPI( - portableTextEditor, - portableTextFeatures, - keyGenerator - ) + const operationToPatches = createOperationToPatches(types) + const withObjectKeys = createWithObjectKeys(types, keyGenerator) + const withSchemaTypes = createWithSchemaTypes(types) + const withEditableAPI = createWithEditableAPI(portableTextEditor, types, keyGenerator) const [withPatches, withPatchesCleanupFunction] = readOnly ? [] : createWithPatches({ patchFunctions: operationToPatches, change$, - portableTextFeatures, + types, syncValue, incomingPatches$, }) const withMaxBlocks = createWithMaxBlocks() - const withPortableTextLists = createWithPortableTextLists(portableTextFeatures) + const withPortableTextLists = createWithPortableTextLists(types) const [withUndoRedo, withUndoRedoCleanupFunction] = readOnly ? [] : createWithUndoRedo(incomingPatches$) - const withPortableTextMarkModel = createWithPortableTextMarkModel( - portableTextFeatures, - keyGenerator - ) - const withPortableTextBlockStyle = createWithPortableTextBlockStyle(portableTextFeatures, change$) + const withPortableTextMarkModel = createWithPortableTextMarkModel(types) + const withPortableTextBlockStyle = createWithPortableTextBlockStyle(types, change$) const withPlaceholderBlock = createWithPlaceholderBlock({ keyGenerator, - portableTextFeatures, + types, }) - const withUtils = createWithUtils({keyGenerator, portableTextFeatures}) - const withPortableTextSelections = createWithPortableTextSelections(change$, portableTextFeatures) + const withUtils = createWithUtils({keyGenerator, types}) + const withPortableTextSelections = createWithPortableTextSelections(change$, types) e.destroy = () => { const originalFunctions = originalFnMap.get(e) diff --git a/packages/@sanity/portable-text-editor/src/index.ts b/packages/@sanity/portable-text-editor/src/index.ts index 84a83cf4aaf..ead4d68274b 100644 --- a/packages/@sanity/portable-text-editor/src/index.ts +++ b/packages/@sanity/portable-text-editor/src/index.ts @@ -1,12 +1,9 @@ export {PortableTextEditor, defaultKeyGenerator as keyGenerator} from './editor/PortableTextEditor' export type {PortableTextEditorProps, PortableTextEditorState} from './editor/PortableTextEditor' export * from './types/editor' -export * from './types/portableText' export * from './types/patch' -export * from './types/schema' export * from './types/options' export {compactPatches} from './utils/patches' -export {getPortableTextFeatures} from './utils/getPortableTextFeatures' export {PortableTextEditable} from './editor/Editable' export type {PortableTextEditableProps} from './editor/Editable' export {usePortableTextEditor} from './editor/hooks/usePortableTextEditor' diff --git a/packages/@sanity/portable-text-editor/src/types/editor.ts b/packages/@sanity/portable-text-editor/src/types/editor.ts index d3e378b1b4b..8b4ffcbcf65 100644 --- a/packages/@sanity/portable-text-editor/src/types/editor.ts +++ b/packages/@sanity/portable-text-editor/src/types/editor.ts @@ -1,28 +1,34 @@ -import {ArraySchemaType, ObjectSchemaType, Path} from '@sanity/types' +import { + ArraySchemaType, + BlockDecoratorDefinition, + BlockListDefinition, + BlockSchemaType, + BlockStyleDefinition, + ObjectSchemaType, + Path, + PortableTextBlock, + PortableTextChild, + PortableTextListBlock, + PortableTextObject, + PortableTextSpan, + PortableTextTextBlock, + SpanSchemaType, +} from '@sanity/types' import {Subject, Observable} from 'rxjs' import {Descendant, Node as SlateNode, Operation as SlateOperation} from 'slate' import {ReactEditor} from '@sanity/slate-react' import type {Patch} from '../types/patch' -import {Type} from '../types/schema' -import { - ListItem, - PortableTextBlock, - PortableTextChild, - TextBlock, - TextSpan, -} from '../types/portableText' import {PortableTextEditor} from '../editor/PortableTextEditor' -import {PortableTextFeatures} from '..' export interface EditableAPIDeleteOptions { mode?: 'blocks' | 'children' | 'selected' } export interface EditableAPI { - activeAnnotations: () => PortableTextBlock[] + activeAnnotations: () => PortableTextObject[] addAnnotation: ( - type: Type, - value?: {[prop: string]: any} + type: ObjectSchemaType, + value?: {[prop: string]: unknown} ) => {spanPath: Path; markDefPath: Path} | undefined blur: () => void delete: (selection: EditorSelection, options?: EditableAPIDeleteOptions) => void @@ -35,15 +41,15 @@ export interface EditableAPI { getValue: () => PortableTextBlock[] | undefined hasBlockStyle: (style: string) => boolean hasListStyle: (listStyle: string) => boolean - insertBlock: (type: Type, value?: {[prop: string]: any}) => Path - insertChild: (type: Type, value?: {[prop: string]: any}) => Path + insertBlock: (type: BlockSchemaType | ObjectSchemaType, value?: {[prop: string]: unknown}) => Path + insertChild: (type: SpanSchemaType | ObjectSchemaType, value?: {[prop: string]: unknown}) => Path isCollapsedSelection: () => boolean isExpandedSelection: () => boolean isMarkActive: (mark: string) => boolean isVoid: (element: PortableTextBlock | PortableTextChild) => boolean marks: () => string[] redo: () => void - removeAnnotation: (type: Type) => void + removeAnnotation: (type: ObjectSchemaType) => void select: (selection: EditorSelection) => void toggleBlockStyle: (blockStyle: string) => void toggleList: (listStyle: string) => void @@ -77,9 +83,9 @@ export interface PortableTextSlateEditor extends ReactEditor { history: History insertPortableTextData: (data: DataTransfer) => boolean insertTextOrHTMLData: (data: DataTransfer) => boolean - isTextBlock: (value: unknown) => value is TextBlock - isTextSpan: (value: unknown) => value is TextSpan - isListBlock: (value: unknown) => value is ListItem + isTextBlock: (value: unknown) => value is PortableTextTextBlock + isTextSpan: (value: unknown) => value is PortableTextSpan + isListBlock: (value: unknown) => value is PortableTextListBlock readOnly: boolean maxBlocks: number | undefined @@ -226,7 +232,7 @@ export type ErrorChange = { name: string // short computer readable name level: 'warning' | 'error' description: string - data?: any + data?: unknown } export type InvalidValueResolution = { @@ -282,8 +288,7 @@ export type OnPasteResultOrPromise = OnPasteResult | Promise export type OnPasteFn = (arg0: { event: React.ClipboardEvent path: Path - portableTextFeatures: PortableTextFeatures - type: ArraySchemaType + types: PortableTextMemberTypes value: PortableTextBlock[] | undefined }) => OnPasteResultOrPromise @@ -291,56 +296,114 @@ export type OnBeforeInputFn = (event: Event) => void export type OnCopyFn = ( event: React.ClipboardEvent -) => undefined | any +) => undefined | unknown export type PatchObservable = Observable<{ patches: Patch[] snapshot: PortableTextBlock[] | undefined }> -export type RenderAttributes = { - annotations?: PortableTextBlock[] +/** @public */ +export interface BlockRenderProps { + children: React.ReactElement + editorElementRef: React.RefObject focused: boolean level?: number listItem?: string path: Path selected: boolean style?: string + type: ObjectSchemaType + value: PortableTextBlock +} + +/** @public */ +export interface BlockChildRenderProps { + annotations: PortableTextObject[] + children: React.ReactElement + editorElementRef: React.RefObject + focused: boolean + path: Path + selected: boolean + type: ObjectSchemaType + value: PortableTextChild +} + +/** @public */ +export interface BlockAnnotationRenderProps { + block: PortableTextBlock + children: React.ReactElement + editorElementRef: React.RefObject + focused: boolean + path: Path + selected: boolean + type: ObjectSchemaType + value: PortableTextObject +} +/** @public */ +export interface BlockDecoratorRenderProps { + children: React.ReactElement + editorElementRef: React.RefObject + focused: boolean + path: Path + selected: boolean + type: BlockDecoratorDefinition + value: string +} +/** @public */ + +export interface BlockListItemRenderProps { + block: PortableTextTextBlock + children: React.ReactElement + editorElementRef: React.RefObject + focused: boolean + level: number + nextItem?: string + path: Path + previousItem?: string + selected: boolean + type: BlockListDefinition + value: string } -export type RenderBlockFunction = ( - value: PortableTextBlock, - type: ObjectSchemaType, - attributes: RenderAttributes, - defaultRender: (val: PortableTextBlock) => JSX.Element, - ref: React.RefObject -) => JSX.Element - -export type RenderChildFunction = ( - value: PortableTextChild, - type: ObjectSchemaType, - attributes: RenderAttributes, - defaultRender: (val: PortableTextChild) => JSX.Element, - ref: React.RefObject -) => JSX.Element - -export type RenderAnnotationFunction = ( - value: PortableTextBlock, - type: ObjectSchemaType, - attributes: RenderAttributes, - defaultRender: () => JSX.Element, - ref: React.RefObject -) => JSX.Element - -export type RenderDecoratorFunction = ( - value: string, - type: {title: string}, - attributes: RenderAttributes, - defaultRender: () => JSX.Element, - ref: React.RefObject -) => JSX.Element +export type RenderBlockFunction = (props: BlockRenderProps) => JSX.Element + +export type RenderChildFunction = (props: BlockChildRenderProps) => JSX.Element + +export type RenderAnnotationFunction = (props: BlockAnnotationRenderProps) => JSX.Element + +export type RenderStyleFunction = (props: BlockStyleRenderProps) => JSX.Element + +/** @public */ + +export interface BlockStyleRenderProps { + block: PortableTextTextBlock + children: React.ReactElement + editorElementRef: React.RefObject + focused: boolean + path: Path + selected: boolean + type: BlockStyleDefinition + value: string +} + +export type RenderListItemFunction = (props: BlockListItemRenderProps) => JSX.Element + +export type RenderDecoratorFunction = (props: BlockDecoratorRenderProps) => JSX.Element export type ScrollSelectionIntoViewFunction = ( editor: PortableTextEditor, domRange: globalThis.Range ) => void + +export type PortableTextMemberTypes = { + annotations: ObjectSchemaType[] + block: ObjectSchemaType + blockObjects: ObjectSchemaType[] + decorators: BlockDecoratorDefinition[] + inlineObjects: ObjectSchemaType[] + portableText: ArraySchemaType + span: ObjectSchemaType + styles: BlockStyleDefinition[] + lists: BlockListDefinition[] +} diff --git a/packages/@sanity/portable-text-editor/src/types/portableText.ts b/packages/@sanity/portable-text-editor/src/types/portableText.ts deleted file mode 100644 index 6df3a2c7615..00000000000 --- a/packages/@sanity/portable-text-editor/src/types/portableText.ts +++ /dev/null @@ -1,71 +0,0 @@ -import {ArraySchemaType, ObjectSchemaType} from '@sanity/types' -import type {ComponentType} from 'react' -import type {Type as SchemaType} from './schema' - -export type PortableTextBlock = { - _type: string - _key: string - [other: string]: any -} - -export interface TextBlock { - _type: string - _key: string - children: PortableTextChild[] - markDefs: MarkDef[] - listItem?: string - style?: string - level?: number -} - -export interface ListItem extends TextBlock { - listItem: string - level: number -} - -export type TextSpan = { - _key: string - _type: 'span' - text: string - marks: string[] -} - -export type PortableTextChild = - | { - _key: string - _type: string - [other: string]: any - } - | TextSpan - -export type MarkDef = {_key: string; _type: string} - -export type PortableTextFeature = { - title: string - value: string - // Backward compatibility (blockEditor) - blockEditor?: { - icon?: string | ComponentType - render?: ComponentType - } - portableText?: { - icon?: string | ComponentType - render?: ComponentType - } - type: SchemaType -} - -export type PortableTextFeatures = { - decorators: PortableTextFeature[] - styles: PortableTextFeature[] - annotations: PortableTextFeature[] - lists: PortableTextFeature[] - types: { - block: ObjectSchemaType - blockObjects: ObjectSchemaType[] - inlineObjects: ObjectSchemaType[] - portableText: ArraySchemaType - span: ObjectSchemaType - annotations: ObjectSchemaType[] - } -} diff --git a/packages/@sanity/portable-text-editor/src/types/schema.ts b/packages/@sanity/portable-text-editor/src/types/schema.ts deleted file mode 100644 index c10715456e6..00000000000 --- a/packages/@sanity/portable-text-editor/src/types/schema.ts +++ /dev/null @@ -1,35 +0,0 @@ -import {ArraySchemaType} from '@sanity/types' - -export type Type = { - type: Type - name: string - title: string - description?: string - readOnly?: boolean - of: ArraySchemaType['of'] - options: Record | null - fields?: Type[] - [prop: string]: any - jsonType: 'array' -} - -export type PortableTextType = Type & { - options?: { - modal?: {type?: 'dialog' | 'popover'; width?: number | number[] | 'auto'} - sortable?: boolean - layout?: 'grid' - } - styles?: {title: string; value: string}[] -} - -export type RawType = { - type: string - name: string - title?: string - description?: string - readOnly?: boolean - of?: Type[] | RawType[] - options?: Record | null - fields?: Type[] | RawType[] - [prop: string]: any -} diff --git a/packages/@sanity/portable-text-editor/src/types/slate.ts b/packages/@sanity/portable-text-editor/src/types/slate.ts index 044034002c2..e346a6659b5 100644 --- a/packages/@sanity/portable-text-editor/src/types/slate.ts +++ b/packages/@sanity/portable-text-editor/src/types/slate.ts @@ -1,18 +1,17 @@ import {BaseEditor, Descendant} from 'slate' import {ReactEditor} from '@sanity/slate-react' -import {PortableTextSlateEditor, TextBlock, TextSpan} from '..' +import {PortableTextSpan, PortableTextTextBlock} from '@sanity/types' +import {PortableTextSlateEditor} from '..' -interface VoidElement { +export interface VoidElement { _type: string _key: string children: Descendant[] __inline: boolean - value: { - [other: string]: any - } + value: Record } -interface SlateTextBlock extends TextBlock { +export interface SlateTextBlock extends Omit { children: Descendant[] } @@ -20,6 +19,6 @@ declare module 'slate' { interface CustomTypes { Editor: BaseEditor & ReactEditor & PortableTextSlateEditor Element: SlateTextBlock | VoidElement - Text: TextSpan + Text: PortableTextSpan } } diff --git a/packages/@sanity/portable-text-editor/src/utils/__tests__/operationToPatches.test.ts b/packages/@sanity/portable-text-editor/src/utils/__tests__/operationToPatches.test.ts index aaec962d323..4d8de89823f 100644 --- a/packages/@sanity/portable-text-editor/src/utils/__tests__/operationToPatches.test.ts +++ b/packages/@sanity/portable-text-editor/src/utils/__tests__/operationToPatches.test.ts @@ -1,12 +1,12 @@ import {createEditor, Descendant} from 'slate' -import {getPortableTextFeatures} from '../getPortableTextFeatures' +import {PortableTextTextBlock} from '@sanity/types' +import {getPortableTextMemberTypes} from '../getPortableTextMemberTypes' import {type} from '../../editor/__tests__/PortableTextEditorTester' import {createOperationToPatches} from '../operationToPatches' import {withPlugins} from '../../editor/plugins' import {PortableTextEditor, PortableTextEditorProps} from '../..' -import {TextBlock} from '../../types/portableText' -const portableTextFeatures = getPortableTextFeatures(type) +const portableTextFeatures = getPortableTextMemberTypes(type) const operationToPatches = createOperationToPatches(portableTextFeatures) const editor = withPlugins(createEditor(), { @@ -103,7 +103,7 @@ describe('operationToPatches', () => { node: { _type: 'someObject', _key: 'c130395c640c', - value: {}, + value: {title: 'The Object'}, __inline: false, children: [{_key: '1', _type: 'span', text: '', marks: []}], }, @@ -117,6 +117,7 @@ describe('operationToPatches', () => { Object { "_key": "c130395c640c", "_type": "someObject", + "title": "The Object", }, ], "path": Array [ @@ -185,7 +186,7 @@ describe('operationToPatches', () => { node: { _type: 'someObject', _key: 'c130395c640c', - value: {}, + value: {title: 'The Object'}, __inline: true, children: [{_key: '1', _type: 'span', text: '', marks: []}], }, @@ -200,6 +201,7 @@ describe('operationToPatches', () => { Object { "_key": "c130395c640c", "_type": "someObject", + "title": "The Object", }, ], "path": Array [ @@ -219,7 +221,7 @@ describe('operationToPatches', () => { }) it('produce correct insert text patch', () => { - ;(editor.children[0] as TextBlock).children[2].text = '1' + ;(editor.children[0] as PortableTextTextBlock).children[2].text = '1' editor.onChange() expect( operationToPatches.insertTextPatch( @@ -257,7 +259,7 @@ describe('operationToPatches', () => { it('produces correct remove text patch', () => { const before = createDefaultValue() - ;(before[0] as TextBlock).children[2].text = '1' + ;(before[0] as PortableTextTextBlock).children[2].text = '1' expect( operationToPatches.removeTextPatch( editor, @@ -302,7 +304,7 @@ describe('operationToPatches', () => { node: { _key: '773866318fa8', _type: 'someObject', - value: {title: 'The Object'}, + value: {title: 'The object'}, __inline: true, children: [{_type: 'span', _key: 'bogus', text: '', marks: []}], }, @@ -357,13 +359,13 @@ describe('operationToPatches', () => { it('produce correct merge node patch', () => { const val = createDefaultValue() - ;(val[0] as TextBlock).children.push({ + ;(val[0] as PortableTextTextBlock).children.push({ _type: 'span', _key: 'r4wr323432', text: '1234', marks: [], }) - const block = editor.children[0] as TextBlock + const block = editor.children[0] as PortableTextTextBlock block.children = block.children.splice(0, 3) block.children[2].text = '1234' editor.onChange() diff --git a/packages/@sanity/portable-text-editor/src/utils/__tests__/patchToOperations.test.ts b/packages/@sanity/portable-text-editor/src/utils/__tests__/patchToOperations.test.ts index b06ad14a845..5506018edb4 100644 --- a/packages/@sanity/portable-text-editor/src/utils/__tests__/patchToOperations.test.ts +++ b/packages/@sanity/portable-text-editor/src/utils/__tests__/patchToOperations.test.ts @@ -1,40 +1,39 @@ import {createEditor, Descendant} from 'slate' -import {getPortableTextFeatures} from '../getPortableTextFeatures' import {type} from '../../editor/__tests__/PortableTextEditorTester' import {createPatchToOperations} from '../patchToOperations' import {withPlugins} from '../../editor/plugins' import {keyGenerator, Patch, PortableTextEditor, PortableTextEditorProps} from '../..' import {fromSlateValue} from '../values' +import {getPortableTextMemberTypes} from '../getPortableTextMemberTypes' -const portableTextFeatures = getPortableTextFeatures(type) +const types = getPortableTextMemberTypes(type) -const patchToOperations = createPatchToOperations(portableTextFeatures, keyGenerator) +const patchToOperations = createPatchToOperations(types, keyGenerator) const editor = withPlugins(createEditor(), { portableTextEditor: new PortableTextEditor({type} as PortableTextEditorProps), }) -const createDefaultValue = () => - [ - { - _type: 'image', - _key: 'c01739b0d03b', - children: [ - { - _key: 'c01739b0d03b-void-child', - _type: 'span', - text: '', - marks: [], - }, - ], - __inline: false, - value: { - asset: { - _ref: 'image-f52f71bc1df46e080dabe43a8effe8ccfb5f21de-4032x3024-png', - _type: 'reference', - }, +const createDefaultValue = (): Descendant[] => [ + { + _type: 'image', + _key: 'c01739b0d03b', + children: [ + { + _key: 'c01739b0d03b-void-child', + _type: 'span', + text: '', + marks: [], + }, + ], + __inline: false, + value: { + asset: { + _ref: 'image-f52f71bc1df46e080dabe43a8effe8ccfb5f21de-4032x3024-png', + _type: 'reference', }, }, - ] as Descendant[] + }, +] describe('operationToPatches', () => { beforeEach(() => { @@ -56,7 +55,7 @@ describe('operationToPatches', () => { origin: 'remote', }, ] as Patch[] - const snapShot = fromSlateValue(editor.children, portableTextFeatures.types.block.name) + const snapShot = fromSlateValue(editor.children, types.block.name) patches.forEach((p) => { patchToOperations(editor, p, patches, snapShot) }) diff --git a/packages/@sanity/portable-text-editor/src/utils/__tests__/values.test.ts b/packages/@sanity/portable-text-editor/src/utils/__tests__/values.test.ts index 3b59c7f56db..be98668bf25 100644 --- a/packages/@sanity/portable-text-editor/src/utils/__tests__/values.test.ts +++ b/packages/@sanity/portable-text-editor/src/utils/__tests__/values.test.ts @@ -1,17 +1,17 @@ import {fromSlateValue, toSlateValue} from '../values' import {type} from '../../editor/__tests__/PortableTextEditorTester' -import {getPortableTextFeatures} from '../getPortableTextFeatures' +import {getPortableTextMemberTypes} from '../getPortableTextMemberTypes' -const portableTextFeatures = getPortableTextFeatures(type) +const types = getPortableTextMemberTypes(type) describe('toSlateValue', () => { it('checks undefined', () => { - const result = toSlateValue(undefined, {portableTextFeatures}) + const result = toSlateValue(undefined, {types}) expect(result).toHaveLength(0) }) it('runs given empty array', () => { - const result = toSlateValue([], {portableTextFeatures}) + const result = toSlateValue([], {types}) expect(result).toHaveLength(0) }) @@ -23,7 +23,7 @@ describe('toSlateValue', () => { _key: '123', }, ], - {portableTextFeatures} + {types} ) expect(result).toMatchObject([ @@ -44,7 +44,7 @@ describe('toSlateValue', () => { const result = toSlateValue( [ { - _type: portableTextFeatures.types.block.name, + _type: types.block.name, _key: '123', children: [ { @@ -55,7 +55,7 @@ describe('toSlateValue', () => { ], }, ], - {portableTextFeatures} + {types} ) expect(result).toMatchInlineSnapshot(` Array [ @@ -80,7 +80,7 @@ describe('toSlateValue', () => { const result = toSlateValue( [ { - _type: portableTextFeatures.types.block.name, + _type: types.block.name, _key: '123', children: [ { @@ -98,7 +98,7 @@ describe('toSlateValue', () => { ], }, ], - {portableTextFeatures} + {types} ) expect(result).toMatchInlineSnapshot(` Array [ @@ -163,6 +163,8 @@ describe('fromSlateValue', () => { __inline: true, children: [{_key: '1', _type: 'span', text: '', marks: []}], value: { + _type: 'image', + _key: 'e324t4s', asset: {_ref: '32423r32rewr3rwerwer'}, }, }, @@ -175,6 +177,8 @@ describe('fromSlateValue', () => { _key: 'wer32434', children: [{_key: '1', _type: 'span', text: '', marks: []}], value: { + _type: 'image', + _key: 'wer32434', asset: {_ref: 'werwer452423423'}, }, }, @@ -237,8 +241,8 @@ describe('fromSlateValue', () => { style: 'normal', }, ] - const toSlate1 = toSlateValue(value, {portableTextFeatures}, keyMap) - const toSlate2 = toSlateValue(value, {portableTextFeatures}, keyMap) + const toSlate1 = toSlateValue(value, {types}, keyMap) + const toSlate2 = toSlateValue(value, {types}, keyMap) expect(toSlate1[0]).toBe(toSlate2[0]) expect(toSlate1[1]).toBe(toSlate2[1]) const fromSlate1 = fromSlateValue(toSlate1, 'block', keyMap) diff --git a/packages/@sanity/portable-text-editor/src/utils/getPortableTextFeatures.ts b/packages/@sanity/portable-text-editor/src/utils/getPortableTextMemberTypes.ts similarity index 65% rename from packages/@sanity/portable-text-editor/src/utils/getPortableTextFeatures.ts rename to packages/@sanity/portable-text-editor/src/utils/getPortableTextMemberTypes.ts index c0288865051..ba8f403cdc7 100644 --- a/packages/@sanity/portable-text-editor/src/utils/getPortableTextFeatures.ts +++ b/packages/@sanity/portable-text-editor/src/utils/getPortableTextMemberTypes.ts @@ -1,14 +1,20 @@ -import {ArraySchemaType, ObjectSchemaType, SchemaType} from '@sanity/types' -import {PortableTextBlock, PortableTextFeatures} from '../types/portableText' -import {Type} from '../types/schema' +import { + ArraySchemaType, + BlockSchemaType, + ObjectSchemaType, + PortableTextBlock, + SchemaType, + SpanSchemaType, +} from '@sanity/types' +import {PortableTextMemberTypes} from '../types/editor' -export function getPortableTextFeatures( - portabletextType: ArraySchemaType -): PortableTextFeatures { - if (!portabletextType) { +export function getPortableTextMemberTypes( + portableTextType: ArraySchemaType +): PortableTextMemberTypes { + if (!portableTextType) { throw new Error("Parameter 'portabletextType' missing (required)") } - const blockType = portabletextType.of?.find(findBlockType) as ObjectSchemaType | undefined + const blockType = portableTextType.of?.find(findBlockType) as BlockSchemaType | undefined if (!blockType) { throw new Error('Block type is not defined in this schema (required)') } @@ -30,22 +36,18 @@ export function getPortableTextFeatures( } const inlineObjectTypes = (ofType.filter((memberType) => memberType.name !== 'span') || []) as ObjectSchemaType[] - const blockObjectTypes = (portabletextType.of?.filter((field) => field.name !== blockType.name) || + const blockObjectTypes = (portableTextType.of?.filter((field) => field.name !== blockType.name) || []) as ObjectSchemaType[] - const annotations = resolveEnabledAnnotationTypes(spanType) return { styles: resolveEnabledStyles(blockType), decorators: resolveEnabledDecorators(spanType), lists: resolveEnabledListItems(blockType), - annotations: annotations, - types: { - block: blockType, - span: spanType, - portableText: portabletextType, - inlineObjects: inlineObjectTypes, - blockObjects: blockObjectTypes, - annotations: annotations.map((an: Type) => an.type), - }, + block: blockType, + span: spanType, + portableText: portableTextType, + inlineObjects: inlineObjectTypes, + blockObjects: blockObjectTypes, + annotations: (spanType as SpanSchemaType).annotations, } } @@ -66,19 +68,6 @@ function resolveEnabledStyles(blockType: ObjectSchemaType) { return textStyles } -function resolveEnabledAnnotationTypes(spanType: ObjectSchemaType) { - return (spanType as any).annotations.map((annotation: Type) => { - return { - blockEditor: annotation.blockEditor, - portableText: annotation.portableText, - title: annotation.title, - type: annotation, - value: annotation.name, - icon: annotation.icon, - } - }) -} - function resolveEnabledDecorators(spanType: ObjectSchemaType) { return (spanType as any).decorators } @@ -97,13 +86,13 @@ function resolveEnabledListItems(blockType: ObjectSchemaType) { return listItems } -function findBlockType(type: SchemaType): ObjectSchemaType | null { +function findBlockType(type: SchemaType): BlockSchemaType | null { if (type.type) { return findBlockType(type.type) } if (type.name === 'block') { - return type as ObjectSchemaType + return type as BlockSchemaType } return null diff --git a/packages/@sanity/portable-text-editor/src/utils/operationToPatches.ts b/packages/@sanity/portable-text-editor/src/utils/operationToPatches.ts index 77dc1341b54..e4e5fec6da6 100644 --- a/packages/@sanity/portable-text-editor/src/utils/operationToPatches.ts +++ b/packages/@sanity/portable-text-editor/src/utils/operationToPatches.ts @@ -1,4 +1,4 @@ -import {Path} from '@sanity/types' +import {Path, PortableTextSpan, PortableTextTextBlock} from '@sanity/types' import {omitBy, isUndefined, get} from 'lodash' import { Editor, @@ -10,22 +10,19 @@ import { SplitNodeOperation, RemoveNodeOperation, MergeNodeOperation, - Text, Descendant, } from 'slate' import {set, insert, unset, diffMatchPatch, setIfMissing} from '../patch/PatchEvent' -import {PortableTextFeatures, PortableTextBlock, TextSpan} from '../types/portableText' import type {Patch, InsertPosition} from '../types/patch' import {PatchFunctions} from '../editor/plugins/createWithPatches' +import {PortableTextMemberTypes} from '../types/editor' import {fromSlateValue} from './values' import {debugWithName} from './debug' const debug = debugWithName('operationToPatches') -export function createOperationToPatches( - portableTextFeatures: PortableTextFeatures -): PatchFunctions { - const textBlockName = portableTextFeatures.types.block.name +export function createOperationToPatches(types: PortableTextMemberTypes): PatchFunctions { + const textBlockName = types.block.name function insertTextPatch( editor: Editor, operation: InsertTextOperation, @@ -39,15 +36,15 @@ export function createOperationToPatches( } const textChild = editor.isTextBlock(block) && - Text.isText(block.children[operation.path[1]]) && - (block.children[operation.path[1]] as TextSpan) + editor.isTextSpan(block.children[operation.path[1]]) && + (block.children[operation.path[1]] as PortableTextSpan) if (!textChild) { throw new Error('Could not find child') } const path: Path = [{_key: block._key}, 'children', {_key: textChild._key}, 'text'] const prevBlock = beforeValue[operation.path[0]] const prevChild = editor.isTextBlock(prevBlock) && prevBlock.children[operation.path[1]] - const prevText = Text.isText(prevChild) ? prevChild.text : '' + const prevText = editor.isTextSpan(prevChild) ? prevChild.text : '' const patch = diffMatchPatch(prevText, textChild.text, path) return patch.value.length ? [patch] : [] } @@ -61,17 +58,18 @@ export function createOperationToPatches( if (!block) { throw new Error('Could not find block') } - const textChild = - editor.isTextBlock(block) && - Text.isText(block.children[operation.path[1]]) && - (block.children[operation.path[1]] as TextSpan) + const child = (editor.isTextBlock(block) && block.children[operation.path[1]]) || undefined + const textChild: PortableTextSpan | undefined = editor.isTextSpan(child) ? child : undefined + if (child && !textChild) { + throw new Error('Expected span') + } if (!textChild) { throw new Error('Could not find child') } const path: Path = [{_key: block._key}, 'children', {_key: textChild._key}, 'text'] const beforeBlock = beforeValue[operation.path[0]] const prevTextChild = editor.isTextBlock(beforeBlock) && beforeBlock.children[operation.path[1]] - const prevText = Text.isText(prevTextChild) && prevTextChild.text + const prevText = editor.isTextSpan(prevTextChild) && prevTextChild.text const patch = diffMatchPatch(prevText || '', textChild.text, path) return patch.value ? [patch] : [] } @@ -136,7 +134,7 @@ export function createOperationToPatches( } const position = block.children.length === 0 || !block.children[operation.path[1] - 1] ? 'before' : 'after' - const child = fromSlateValue( + const blk = fromSlateValue( [ { _key: 'bogus', @@ -145,7 +143,8 @@ export function createOperationToPatches( }, ], textBlockName - )[0].children[0] + )[0] as PortableTextTextBlock + const child = blk.children[0] return [ insert([child], position, [ {_key: block._key}, @@ -197,16 +196,18 @@ export function createOperationToPatches( } if (operation.path.length === 2) { const splitSpan = splitBlock.children[operation.path[1]] - if (Text.isText(splitSpan)) { - const targetSpans = fromSlateValue( - [ - { - ...splitBlock, - children: splitBlock.children.slice(operation.path[1] + 1, operation.path[1] + 2), - }, - ], - textBlockName - )[0].children + if (editor.isTextSpan(splitSpan)) { + const targetSpans = ( + fromSlateValue( + [ + { + ...splitBlock, + children: splitBlock.children.slice(operation.path[1] + 1, operation.path[1] + 2), + } as Descendant, + ], + textBlockName + )[0] as PortableTextTextBlock + ).children patches.push( insert(targetSpans, 'after', [ @@ -225,9 +226,9 @@ export function createOperationToPatches( } function removeNodePatch( - _: Editor, + editor: Editor, operation: RemoveNodeOperation, - beforeValue: PortableTextBlock[] + beforeValue: Descendant[] ) { const block = beforeValue[operation.path[0]] if (operation.path.length === 1) { @@ -237,7 +238,8 @@ export function createOperationToPatches( } throw new Error('Block not found') } else if (operation.path.length === 2) { - const spanToRemove = block && block.children && block.children[operation.path[1]] + const spanToRemove = + editor.isTextBlock(block) && block.children && block.children[operation.path[1]] if (spanToRemove) { return [unset([{_key: block._key}, 'children', {_key: spanToRemove._key}])] } @@ -252,7 +254,7 @@ export function createOperationToPatches( function mergeNodePatch( editor: Editor, operation: MergeNodeOperation, - beforeValue: PortableTextBlock[] + beforeValue: Descendant[] ) { const patches: Patch[] = [] if (operation.path.length === 1) { @@ -267,18 +269,21 @@ export function createOperationToPatches( } } else if (operation.path.length === 2) { const block = beforeValue[operation.path[0]] - const mergedSpan = block.children[operation.path[1]] + const mergedSpan = + (editor.isTextBlock(block) && block.children[operation.path[1]]) || undefined const targetBlock = editor.children[operation.path[0]] if (!editor.isTextBlock(targetBlock)) { throw new Error('Invalid block') } const targetSpan = targetBlock.children[operation.path[1] - 1] - if (Text.isText(targetSpan)) { + if (editor.isTextSpan(targetSpan)) { // Set the merged span with it's new value patches.push( set(targetSpan.text, [{_key: block._key}, 'children', {_key: targetSpan._key}, 'text']) ) - patches.push(unset([{_key: block._key}, 'children', {_key: mergedSpan._key}])) + if (mergedSpan) { + patches.push(unset([{_key: block._key}, 'children', {_key: mergedSpan._key}])) + } } } else { throw new Error(`Unexpected path encountered: ${JSON.stringify(operation.path)}`) @@ -286,11 +291,7 @@ export function createOperationToPatches( return patches } - function moveNodePatch( - _: Editor, - operation: MoveNodeOperation, - beforeValue: PortableTextBlock[] - ) { + function moveNodePatch(editor: Editor, operation: MoveNodeOperation, beforeValue: Descendant[]) { const patches: Patch[] = [] const block = beforeValue[operation.path[0]] const targetBlock = beforeValue[operation.newPath[0]] @@ -300,11 +301,16 @@ export function createOperationToPatches( patches.push( insert([fromSlateValue([block], textBlockName)[0]], position, [{_key: targetBlock._key}]) ) - } else if (operation.path.length === 2) { + } else if ( + operation.path.length === 2 && + editor.isTextBlock(block) && + editor.isTextBlock(targetBlock) + ) { const child = block.children[operation.path[1]] const targetChild = targetBlock.children[operation.newPath[1]] const position = operation.newPath[1] === targetBlock.children.length ? 'after' : 'before' - const childToInsert = fromSlateValue([block], textBlockName)[0].children[operation.path[1]] + const childToInsert = (fromSlateValue([block], textBlockName)[0] as PortableTextTextBlock) + .children[operation.path[1]] patches.push(unset([{_key: block._key}, 'children', {_key: child._key}])) patches.push( insert([childToInsert], position, [ diff --git a/packages/@sanity/portable-text-editor/src/utils/patchToOperations.ts b/packages/@sanity/portable-text-editor/src/utils/patchToOperations.ts index db6d23c1a19..e401fd1c856 100644 --- a/packages/@sanity/portable-text-editor/src/utils/patchToOperations.ts +++ b/packages/@sanity/portable-text-editor/src/utils/patchToOperations.ts @@ -1,11 +1,11 @@ /* eslint-disable max-statements */ import {Editor, Transforms, Element, Path as SlatePath, Descendant, Text, Node} from 'slate' import * as DMP from 'diff-match-patch' -import {Path, KeyedSegment, PathSegment} from '@sanity/types' +import {Path, KeyedSegment, PathSegment, PortableTextBlock, PortableTextChild} from '@sanity/types' import {isEqual} from 'lodash' import type {Patch, InsertPatch, UnsetPatch, SetPatch, DiffMatchPatch} from '../types/patch' -import {PortableTextFeatures, PortableTextBlock, PortableTextChild} from '../types/portableText' import {applyAll} from '../patch/applyPatch' +import {PortableTextMemberTypes} from '../types/editor' import {toSlateValue} from './values' import {debugWithName} from './debug' import {KEY_TO_SLATE_ELEMENT} from './weakMaps' @@ -16,7 +16,7 @@ const debug = debugWithName('operationToPatches') const dmp = new DMP.diff_match_patch() export function createPatchToOperations( - portableTextFeatures: PortableTextFeatures, + types: PortableTextMemberTypes, keyGenerator: () => string ): ( editor: Editor, @@ -86,7 +86,7 @@ export function createPatchToOperations( const {items, position} = patch const blocksToInsert = toSlateValue( items as PortableTextBlock[], - {portableTextFeatures}, + {types}, KEY_TO_SLATE_ELEMENT.get(editor) ) as Descendant[] const posKey = findLastKey(patch.path) @@ -107,20 +107,20 @@ export function createPatchToOperations( }) // Insert children - const block: PortableTextBlock | undefined = + const block: Descendant | undefined = editor.children && blockIndex > -1 ? editor.children[blockIndex] : undefined - const childIndex = - block && - block.children.findIndex((node: PortableTextChild, indx: number) => { - return isKeyedSegment(patch.path[2]) - ? node._key === patch.path[2]._key - : indx === patch.path[2] - }) + const childIndex = editor.isTextBlock(block) + ? block.children.findIndex((node: PortableTextChild, indx: number) => { + return isKeyedSegment(patch.path[2]) + ? node._key === patch.path[2]._key + : indx === patch.path[2] + }) + : 0 const childrenToInsert = block && toSlateValue( [{...block, children: items as PortableTextChild[]}], - {portableTextFeatures}, + {types}, KEY_TO_SLATE_ELEMENT.get(editor) ) @@ -142,22 +142,21 @@ export function createPatchToOperations( : indx === patch.path[0] }) debug('blockIndex', blockIndex) - const block: PortableTextBlock | undefined = - blockIndex > -1 ? editor.children[blockIndex] : undefined - const childIndex = - block && - block.children.findIndex((node: PortableTextChild, indx: number) => { - return isKeyedSegment(patch.path[2]) - ? node._key === patch.path[2]._key - : indx === patch.path[2] - }) + const block = blockIndex > -1 ? editor.children[blockIndex] : undefined + const childIndex = editor.isTextBlock(block) + ? block.children.findIndex((node: PortableTextChild, indx: number) => { + return isKeyedSegment(patch.path[2]) + ? node._key === patch.path[2]._key + : indx === patch.path[2] + }) + : 0 let value = patch.value const targetPath: SlatePath = childIndex > -1 ? [blockIndex, childIndex] : [blockIndex] if (typeof patch.path[3] === 'string') { value = {} value[patch.path[3]] = patch.value } - const isTextBlock = portableTextFeatures.types.block.name === block?._type + const isTextBlock = editor.isTextBlock(block) if (isTextBlock) { debug(`Setting nodes at ${JSON.stringify(patch.path)} - ${JSON.stringify(targetPath)}`) debug('Value to set', JSON.stringify(value, null, 2)) @@ -198,7 +197,7 @@ export function createPatchToOperations( type: 'remove_text', path: targetPath, offset: 0, - text: block?.children[childIndex].text, + text: block.children[childIndex].text as string, }) editor.apply({ type: 'insert_text', @@ -234,7 +233,7 @@ export function createPatchToOperations( return true } // If this is a object block, just set the whole block - else if (!isTextBlock && block) { + else if (block && 'value' in block) { const newVal = applyAll([block.value], [patch])[0] Transforms.setNodes(editor, {...block, value: newVal}, {at: [blockIndex]}) return true @@ -282,20 +281,15 @@ export function createPatchToOperations( : indx === patch.path[0] }) - const block: PortableTextBlock | undefined = - blockIndex > -1 ? editor.children[blockIndex] : undefined - - const isTextBlock = portableTextFeatures.types.block.name === block?._type + const block = blockIndex > -1 ? editor.children[blockIndex] : undefined // Unset on text block children - if (isTextBlock && patch.path[1] === 'children' && patch.path.length === 3) { - const childIndex = - block && - block.children.findIndex((node: PortableTextChild, indx: number) => { - return isKeyedSegment(patch.path[2]) - ? node._key === patch.path[2]._key - : indx === patch.path[2] - }) + if (editor.isTextBlock(block) && patch.path[1] === 'children' && patch.path.length === 3) { + const childIndex = block.children.findIndex((node: PortableTextChild, indx: number) => { + return isKeyedSegment(patch.path[2]) + ? node._key === patch.path[2]._key + : indx === patch.path[2] + }) const targetPath = [blockIndex, childIndex] const prevSel = editor.selection && {...editor.selection} const onSamePath = isEqual(editor.selection?.focus.path, targetPath) @@ -305,22 +299,24 @@ export function createPatchToOperations( if (prevSel && onSamePath && editor.isTextBlock(block)) { const needToAdjust = childIndex >= prevSel.focus.path[1] if (needToAdjust) { + const textChild = block.children[childIndex] const isMergeUnset = previousPatch?.type === 'set' && previousPatch.path[3] === 'text' && typeof previousPatch.value === 'string' && + editor.isTextSpan(textChild) && isEqual( - previousPatch.value.slice(-block.children[childIndex].text.length), + previousPatch.value.slice(-textChild.text.length), block.children[childIndex].text ) if (isMergeUnset) { + const mergedChild = block.children[Math.max(childIndex - 1, 0)] debug('Adjusting selection for merging of nodes') prevSel.focus = {...prevSel.focus} prevSel.focus.path = [targetPath[0], Math.max(targetPath[1] - 1, 0)] - prevSel.focus.offset = - block.children[Math.max(childIndex - 1, 0)].text.length - - block.children[childIndex].text.length + - prevSel.focus.offset + prevSel.focus.offset = editor.isTextSpan(mergedChild) + ? mergedChild.text.length - textChild.text.length + prevSel.focus.offset + : 0 prevSel.anchor = prevSel.focus Transforms.select(editor, prevSel) Transforms.removeNodes(editor, {at: [blockIndex, childIndex]}) @@ -334,7 +330,7 @@ export function createPatchToOperations( return true } // Inside block objects - patch block and set it again - if (!isTextBlock && block) { + if (!editor.isTextBlock(block)) { const newBlock = applyAll([block], [patch])[0] Transforms.setNodes(editor, newBlock, {at: [blockIndex]}) return true diff --git a/packages/@sanity/portable-text-editor/src/utils/paths.ts b/packages/@sanity/portable-text-editor/src/utils/paths.ts index 459eb4f09a8..a05d8e93acc 100644 --- a/packages/@sanity/portable-text-editor/src/utils/paths.ts +++ b/packages/@sanity/portable-text-editor/src/utils/paths.ts @@ -1,13 +1,12 @@ import {isEqual} from 'lodash' -import {Editor, Point, Path as SlatePath, Element, Node} from 'slate' -import {isKeySegment, Path} from '@sanity/types' -import {EditorSelectionPoint} from '../types/editor' -import {PortableTextBlock, PortableTextFeatures} from '../types/portableText' +import {Editor, Point, Path as SlatePath, Element} from 'slate' +import {isKeySegment, Path, PortableTextBlock} from '@sanity/types' +import {EditorSelectionPoint, PortableTextMemberTypes} from '../types/editor' export function createKeyedPath( point: Point, value: PortableTextBlock[] | undefined, - portableTextFeatures: PortableTextFeatures + types: PortableTextMemberTypes ): Path | null { const blockPath = [point.path[0]] if (!value) { @@ -18,12 +17,12 @@ export function createKeyedPath( return null } const keyedBlockPath = [{_key: block._key}] - if (block._type !== portableTextFeatures.types.block.name) { + if (block._type !== types.block.name) { return keyedBlockPath as Path } let keyedChildPath const childPath = point.path.slice(0, 2) - const child = block.children[childPath[1]] + const child = Array.isArray(block.children) && block.children[childPath[1]] if (child) { keyedChildPath = ['children', {_key: child._key}] } diff --git a/packages/@sanity/portable-text-editor/src/utils/ranges.ts b/packages/@sanity/portable-text-editor/src/utils/ranges.ts index 5a4a57b703c..b2d65b35d22 100644 --- a/packages/@sanity/portable-text-editor/src/utils/ranges.ts +++ b/packages/@sanity/portable-text-editor/src/utils/ranges.ts @@ -1,26 +1,26 @@ +import {PortableTextBlock} from '@sanity/types' import {BaseRange, Editor, Range} from 'slate' -import {EditorSelection, EditorSelectionPoint} from '../types/editor' -import {PortableTextBlock, PortableTextFeatures} from '../types/portableText' +import {EditorSelection, EditorSelectionPoint, PortableTextMemberTypes} from '../types/editor' import {createArrayedPath, createKeyedPath} from './paths' export function toPortableTextRange( value: PortableTextBlock[] | undefined, range: BaseRange | Partial | null, - portableTextFeatures: PortableTextFeatures + types: PortableTextMemberTypes ): EditorSelection { if (!range) { return null } let anchor: EditorSelectionPoint | null = null let focus: EditorSelectionPoint | null = null - const anchorPath = range.anchor && createKeyedPath(range.anchor, value, portableTextFeatures) + const anchorPath = range.anchor && createKeyedPath(range.anchor, value, types) if (anchorPath && range.anchor) { anchor = { path: anchorPath, offset: range.anchor.offset, } } - const focusPath = range.focus && createKeyedPath(range.focus, value, portableTextFeatures) + const focusPath = range.focus && createKeyedPath(range.focus, value, types) if (focusPath && range.focus) { focus = { path: focusPath, diff --git a/packages/@sanity/portable-text-editor/src/utils/selection.ts b/packages/@sanity/portable-text-editor/src/utils/selection.ts index fc45722e716..4afadbe78d7 100644 --- a/packages/@sanity/portable-text-editor/src/utils/selection.ts +++ b/packages/@sanity/portable-text-editor/src/utils/selection.ts @@ -1,5 +1,5 @@ import {isEqual} from 'lodash' -import {PortableTextBlock} from '../types/portableText' +import {Path, PortableTextBlock} from '@sanity/types' import {EditorSelection, EditorSelectionPoint} from '../types/editor' export function normalizePoint( @@ -9,7 +9,7 @@ export function normalizePoint( if (!point || !value) { return null } - const newPath: any = [] + const newPath: Path = [] let newOffset: number = point.offset || 0 const blockKey = typeof point.path[0] === 'object' && '_key' in point.path[0] && point.path[0]._key @@ -22,10 +22,11 @@ export function normalizePoint( return null } if (block && point.path[1] === 'children') { - if (!block.children || block.children.length === 0) { + if (!block.children || (Array.isArray(block.children) && block.children.length === 0)) { return null } - const child = block.children.find((cld: any) => cld._key === childKey) + const child = + Array.isArray(block.children) && block.children.find((cld) => cld._key === childKey) if (child) { newPath.push('children') newPath.push({_key: child._key}) diff --git a/packages/@sanity/portable-text-editor/src/utils/validateValue.ts b/packages/@sanity/portable-text-editor/src/utils/validateValue.ts index 72728e0d30a..a93a320070b 100644 --- a/packages/@sanity/portable-text-editor/src/utils/validateValue.ts +++ b/packages/@sanity/portable-text-editor/src/utils/validateValue.ts @@ -1,23 +1,17 @@ +import {PortableTextBlock, PortableTextSpan, PortableTextTextBlock} from '@sanity/types' import {flatten, isObject, uniq} from 'lodash' import {set, unset, insert} from '../patch/PatchEvent' -import {PortableTextBlock, PortableTextChild, PortableTextFeatures} from '../types/portableText' -import {InvalidValueResolution} from '../types/editor' +import {InvalidValueResolution, PortableTextMemberTypes} from '../types/editor' export function validateValue( value: PortableTextBlock[] | undefined, - portableTextFeatures: PortableTextFeatures, + types: PortableTextMemberTypes, keyGenerator: () => string ): {valid: boolean; resolution: InvalidValueResolution | null} { let resolution: InvalidValueResolution | null = null let valid = true - const validChildTypes = [ - ...[portableTextFeatures.types.span.name], - ...portableTextFeatures.types.inlineObjects.map((t) => t.name), - ] - const validBlockTypes = [ - ...[portableTextFeatures.types.block.name], - ...portableTextFeatures.types.blockObjects.map((t) => t.name), - ] + const validChildTypes = [...[types.span.name], ...types.inlineObjects.map((t) => t.name)] + const validBlockTypes = [...[types.block.name], ...types.blockObjects.map((t) => t.name)] // Undefined is allowed if (value === undefined) { @@ -60,7 +54,7 @@ export function validateValue( if (!blk._type || !validBlockTypes.includes(blk._type)) { // Special case where block type is set to default 'block', but the block type is named something else according to the schema. if (blk._type === 'block') { - const currentBlockTypeName = portableTextFeatures.types.block.name + const currentBlockTypeName = types.block.name resolution = { patches: [set({...blk, _type: currentBlockTypeName}, [{_key: blk._key}])], description: `Block with _key '${blk._key}' has invalid type name '${blk._type}'. According to the schema, the block type name is '${currentBlockTypeName}'`, @@ -78,31 +72,32 @@ export function validateValue( return true } // Test that every child in text block is valid - if (blk._type === portableTextFeatures.types.block.name) { + if (blk._type === types.block.name) { + const textBlock = blk as PortableTextTextBlock // Test that it has children - if (!blk.children) { + if (!textBlock.children) { resolution = { - patches: [unset([{_key: blk._key}])], - description: `Text block with _key '${blk._key}' is missing required key 'children'.`, + patches: [unset([{_key: textBlock._key}])], + description: `Text block with _key '${textBlock._key}' is missing required key 'children'.`, action: 'Remove the block', - item: blk, + item: textBlock, } return true } // Test that markDefs exists if (!blk.markDefs) { resolution = { - patches: [set({...blk, markDefs: []}, [{_key: blk._key}])], + patches: [set({...textBlock, markDefs: []}, [{_key: textBlock._key}])], description: `Block is missing required key 'markDefs'.`, action: 'Add empty markDefs array', - item: blk, + item: textBlock, } return true } // // Test that every span has .marks // const spansWithUndefinedMarks = blk.children - // .filter(cld => cld._type === portableTextFeatures.types.span.name) + // .filter(cld => cld._type === types.span.name) // .filter(cld => typeof cld.marks === 'undefined') // if (spansWithUndefinedMarks.length > 0) { @@ -117,12 +112,12 @@ export function validateValue( // } // return true // } - const allUsedMarks: string[] = uniq( + const allUsedMarks = uniq( flatten( - blk.children - .filter((cld: any) => cld._type === portableTextFeatures.types.span.name) - .map((cld: any) => cld.marks || []) - ) + textBlock.children + .filter((cld) => cld._type === types.span.name) + .map((cld) => cld.marks || []) + ) as string[] ) // // Test that all markDefs are in use // if (blk.markDefs && blk.markDefs.length > 0) { @@ -144,22 +139,23 @@ export function validateValue( // Test that every annotation mark used has a definition const annotationMarks = allUsedMarks.filter( - (mark) => !portableTextFeatures.decorators.map((dec) => dec.value).includes(mark) + (mark) => !types.decorators.map((dec) => dec.value).includes(mark) ) - const orphanedMarks = annotationMarks.filter( - (mark) => !blk.markDefs.find((def: any) => def._key === mark) + const orphanedMarks = annotationMarks.filter((mark) => + textBlock.markDefs ? !textBlock.markDefs.find((def) => def._key === mark) : false ) if (orphanedMarks.length > 0) { - const children = blk.children.filter( - (cld: any) => + const spanChildren = textBlock.children.filter( + (cld) => + cld._type === types.span.name && Array.isArray(cld.marks) && - cld.marks.some((mark: any) => orphanedMarks.includes(mark)) - ) as PortableTextChild[] - if (children) { + cld.marks.some((mark) => orphanedMarks.includes(mark)) + ) as PortableTextSpan[] + if (spanChildren) { resolution = { - patches: children.map((child) => { + patches: spanChildren.map((child) => { return set( - child.marks.filter((cmrk: any) => !orphanedMarks.includes(cmrk)), + (child.marks || []).filter((cMrk) => !orphanedMarks.includes(cMrk)), [{_key: blk._key}, 'children', {_key: child._key}, 'marks'] ) }), @@ -174,9 +170,9 @@ export function validateValue( } // Test that children is lengthy - if (blk.children && blk.children.length === 0) { + if (textBlock.children && textBlock.children.length === 0) { const newSpan = { - _type: portableTextFeatures.types.span.name, + _type: types.span.name, _key: keyGenerator(), text: '', } @@ -190,18 +186,18 @@ export function validateValue( } // Test every child if ( - blk.children.some((child: any, cIndex: number) => { + textBlock.children.some((child, cIndex: number) => { if (!child._key) { - const newchild = {...child, _key: keyGenerator()} + const newChild = {...child, _key: keyGenerator()} resolution = { - patches: [set(newchild, [{_key: blk._key}, 'children', cIndex])], + patches: [set(newChild, [{_key: blk._key}, 'children', cIndex])], description: `Child at index ${cIndex} is missing required _key in block with _key ${blk._key}.`, action: 'Set a new random _key on the object', item: blk, } return true } - // Verify that childs have valid types + // Verify that children have valid types if (!child._type || validChildTypes.includes(child._type) === false) { resolution = { patches: [unset([{_key: blk._key}, 'children', {_key: child._key}])], @@ -212,7 +208,7 @@ export function validateValue( return true } // Verify that spans have .text - if (child._type === portableTextFeatures.types.span.name && child.text === undefined) { + if (child._type === types.span.name && child.text === undefined) { resolution = { patches: [ set({...child, text: ''}, [{_key: blk._key}, 'children', {_key: child._key}]), diff --git a/packages/@sanity/portable-text-editor/src/utils/values.ts b/packages/@sanity/portable-text-editor/src/utils/values.ts index 0acc25c7914..42796daebec 100644 --- a/packages/@sanity/portable-text-editor/src/utils/values.ts +++ b/packages/@sanity/portable-text-editor/src/utils/values.ts @@ -1,15 +1,15 @@ import {isEqual} from 'lodash' import {Node, Element, Text, Descendant} from 'slate' -import {PathSegment} from '@sanity/types' import { - MarkDef, + PathSegment, PortableTextBlock, PortableTextChild, - PortableTextFeatures, - TextBlock, -} from '../types/portableText' + PortableTextObject, + PortableTextTextBlock, +} from '@sanity/types' +import {PortableTextMemberTypes} from '../types/editor' -const EMPTY_MARKDEFS: MarkDef[] = [] +const EMPTY_MARKDEFS: PortableTextObject[] = [] type Partial = { [P in keyof T]?: T[P] @@ -29,16 +29,16 @@ function keepObjectEquality( export function toSlateValue( value: PortableTextBlock[] | undefined, - {portableTextFeatures}: {portableTextFeatures: PortableTextFeatures}, + {types}: {types: PortableTextMemberTypes}, keyMap: Record = {} ): Descendant[] { if (value && Array.isArray(value)) { return value.map((block) => { const {_type, _key, ...rest} = block const voidChildren = [{_key: `${_key}-void-child`, _type: 'span', text: '', marks: []}] - const isPortableText = block && block._type === portableTextFeatures.types.block.name + const isPortableText = block && block._type === types.block.name if (isPortableText) { - const textBlock = block as TextBlock + const textBlock = block as PortableTextTextBlock let hasInlines = false const hasMissingStyle = typeof textBlock.style === 'undefined' const hasMissingMarkDefs = typeof textBlock.markDefs === 'undefined' @@ -65,7 +65,7 @@ export function toSlateValue( return block } if (hasMissingStyle) { - rest.style = portableTextFeatures.styles[0].value + rest.style = types.styles[0].value } if (hasMissingMarkDefs) { rest.markDefs = EMPTY_MARKDEFS @@ -123,7 +123,7 @@ export function fromSlateValue( export function isEqualToEmptyEditor( children: Descendant[] | PortableTextBlock[], - portableTextFeatures: PortableTextFeatures + types: PortableTextMemberTypes ): boolean { return ( children === undefined || @@ -132,9 +132,9 @@ export function isEqualToEmptyEditor( Array.isArray(children) && children.length === 1 && Element.isElement(children[0]) && - children[0]._type === portableTextFeatures.types.block.name && + children[0]._type === types.block.name && 'style' in children[0] && - children[0].style === portableTextFeatures.styles[0].value && + children[0].style === types.styles[0].value && Array.isArray(children[0].children) && children[0].children.length === 1 && Text.isText(children[0].children[0]) && diff --git a/packages/@sanity/portable-text-editor/src/utils/weakMaps.ts b/packages/@sanity/portable-text-editor/src/utils/weakMaps.ts index a1d7814b27d..7243ee13f3e 100644 --- a/packages/@sanity/portable-text-editor/src/utils/weakMaps.ts +++ b/packages/@sanity/portable-text-editor/src/utils/weakMaps.ts @@ -1,4 +1,4 @@ -import {Editor, Element, Range} from 'slate' +import {Editor, Element, Range, Text} from 'slate' import {EditorSelection} from '..' /** @@ -9,7 +9,7 @@ import {EditorSelection} from '..' export const IS_DRAGGING: WeakMap = new WeakMap() // Is the editor dragging a element? export const IS_DRAGGING_BLOCK_ELEMENT: WeakMap = new WeakMap() -export const IS_DRAGGING_CHILD_ELEMENT: WeakMap = new WeakMap() +export const IS_DRAGGING_CHILD_ELEMENT: WeakMap = new WeakMap() // When dragging elements, this will be the target element export const IS_DRAGGING_ELEMENT_TARGET: WeakMap = new WeakMap() export const IS_DRAGGING_ELEMENT_RANGE: WeakMap = new WeakMap()