UNPKG

@portabletext/editor

Version:

Portable Text Editor made in React

1 lines 140 kB
{"version":3,"file":"index.cjs","sources":["../src/editor-event-listener.tsx","../src/internal-utils/compound-client-rect.ts","../src/internal-utils/drag-selection.ts","../src/internal-utils/event-position.ts","../src/internal-utils/selection.ts","../src/internal-utils/selection-elements.ts","../src/editor/components/DefaultObject.tsx","../src/editor/components/drop-indicator.tsx","../src/editor/components/Element.tsx","../src/editor/components/Leaf.tsx","../src/editor/plugins/createWithHotKeys.ts","../src/editor/range-decorations-machine.ts","../src/editor/Editable.tsx"],"sourcesContent":["import {useEffect} from 'react'\nimport {useEffectEvent} from 'use-effect-event'\nimport type {EditorEmittedEvent} from './editor/editor-machine'\nimport {useEditor} from './editor/editor-provider'\n\n/**\n * @public\n * @deprecated\n * This component has been renamed. Use `EventListenerPlugin` instead.\n *\n * ```\n * import {EventListenerPlugin} from '@portabletext/editor/plugins'\n * ```\n */\nexport function EditorEventListener(props: {\n on: (event: EditorEmittedEvent) => void\n}) {\n const editor = useEditor()\n const on = useEffectEvent(props.on)\n\n useEffect(() => {\n const subscription = editor.on('*', on)\n\n return () => {\n subscription.unsubscribe()\n }\n }, [editor])\n\n return null\n}\n","export function getCompoundClientRect(nodes: Array<Node>): DOMRect {\n if (nodes.length === 0) {\n return new DOMRect(0, 0, 0, 0)\n }\n\n const elements = nodes.filter((node) => node instanceof Element)\n\n const firstRect = elements.at(0)?.getBoundingClientRect()\n\n if (!firstRect) {\n return new DOMRect(0, 0, 0, 0)\n }\n\n let left = firstRect.left\n let top = firstRect.top\n let right = firstRect.right\n let bottom = firstRect.bottom\n\n for (let i = 1; i < elements.length; i++) {\n const rect = elements[i].getBoundingClientRect()\n left = Math.min(left, rect.left)\n top = Math.min(top, rect.top)\n right = Math.max(right, rect.right)\n bottom = Math.max(bottom, rect.bottom)\n }\n\n return new DOMRect(left, top, right - left, bottom - top)\n}\n","import type {EditorSnapshot} from '..'\nimport * as selectors from '../selectors'\nimport * as utils from '../utils'\nimport type {EventPosition} from './event-position'\n\n/**\n * Given the current editor `snapshot` and an `eventSelection` representing\n * where the drag event origins from, this function calculates the selection\n * in the editor that should be dragged.\n */\nexport function getDragSelection({\n eventSelection,\n snapshot,\n}: {\n eventSelection: EventPosition['selection']\n snapshot: EditorSnapshot\n}) {\n let dragSelection = eventSelection\n\n const draggedInlineObject = selectors.getFocusInlineObject({\n ...snapshot,\n context: {\n ...snapshot.context,\n selection: eventSelection,\n },\n })\n\n if (draggedInlineObject) {\n return dragSelection\n }\n\n const draggingCollapsedSelection = selectors.isSelectionCollapsed({\n ...snapshot,\n context: {\n ...snapshot.context,\n selection: eventSelection,\n },\n })\n const draggedTextBlock = selectors.getFocusTextBlock({\n ...snapshot,\n context: {\n ...snapshot.context,\n selection: eventSelection,\n },\n })\n const draggedSpan = selectors.getFocusSpan({\n ...snapshot,\n context: {\n ...snapshot.context,\n selection: eventSelection,\n },\n })\n\n if (draggingCollapsedSelection && draggedTextBlock && draggedSpan) {\n // Looks like we are dragging an empty span\n // Let's drag the entire block instead\n dragSelection = {\n anchor: utils.getBlockStartPoint(draggedTextBlock),\n focus: utils.getBlockEndPoint(draggedTextBlock),\n }\n }\n\n const selectedBlocks = selectors.getSelectedBlocks(snapshot)\n\n if (\n snapshot.context.selection &&\n selectors.isSelectionExpanded(snapshot) &&\n selectedBlocks.length > 1\n ) {\n const selectionStartBlock = selectors.getSelectionStartBlock(snapshot)\n const selectionEndBlock = selectors.getSelectionEndBlock(snapshot)\n\n if (!selectionStartBlock || !selectionEndBlock) {\n return dragSelection\n }\n\n const selectionStartPoint = utils.getBlockStartPoint(selectionStartBlock)\n const selectionEndPoint = utils.getBlockEndPoint(selectionEndBlock)\n\n const eventSelectionInsideBlocks = selectors.isOverlappingSelection(\n eventSelection,\n )({\n ...snapshot,\n context: {\n ...snapshot.context,\n selection: {anchor: selectionStartPoint, focus: selectionEndPoint},\n },\n })\n\n if (eventSelectionInsideBlocks) {\n dragSelection = {\n anchor: selectionStartPoint,\n focus: selectionEndPoint,\n }\n }\n }\n\n return dragSelection\n}\n","import {Editor, type BaseRange, type Node} from 'slate'\nimport {DOMEditor, isDOMNode} from 'slate-dom'\nimport type {EditorSchema, EditorSelection} from '..'\nimport type {EditorActor} from '../editor/editor-machine'\nimport type {PortableTextSlateEditor} from '../types/editor'\nimport * as utils from '../utils'\nimport {\n getFirstBlock,\n getLastBlock,\n getNodeBlock,\n slateRangeToSelection,\n} from './slate-utils'\n\nexport type EventPosition = {\n block: 'start' | 'end'\n /**\n * Did the event origin from the editor DOM node itself or from a child node?\n */\n isEditor: boolean\n selection: NonNullable<EditorSelection>\n}\nexport type EventPositionBlock = EventPosition['block']\n\nexport function getEventPosition({\n editorActor,\n slateEditor,\n event,\n}: {\n editorActor: EditorActor\n slateEditor: PortableTextSlateEditor\n event: DragEvent | MouseEvent\n}): EventPosition | undefined {\n if (editorActor.getSnapshot().matches({setup: 'setting up'})) {\n return undefined\n }\n\n const node = getEventNode({slateEditor, event})\n\n if (!node) {\n return undefined\n }\n\n const block = getNodeBlock({\n editor: slateEditor,\n schema: editorActor.getSnapshot().context.schema,\n node,\n })\n\n const positionBlock = getEventPositionBlock({node, slateEditor, event})\n const selection = getEventSelection({\n schema: editorActor.getSnapshot().context.schema,\n slateEditor,\n event,\n })\n\n if (block && positionBlock && !selection && !Editor.isEditor(node)) {\n return {\n block: positionBlock,\n isEditor: false,\n selection: {\n anchor: utils.getBlockStartPoint({\n node: block,\n path: [{_key: block._key}],\n }),\n focus: utils.getBlockEndPoint({\n node: block,\n path: [{_key: block._key}],\n }),\n },\n }\n }\n\n if (!positionBlock || !selection) {\n return undefined\n }\n\n const focusBlockPath = selection.focus.path.at(0)\n const focusBlockKey = utils.isKeyedSegment(focusBlockPath)\n ? focusBlockPath._key\n : undefined\n\n if (!focusBlockKey) {\n return undefined\n }\n\n if (\n utils.isSelectionCollapsed(selection) &&\n block &&\n focusBlockKey !== block._key\n ) {\n return {\n block: positionBlock,\n isEditor: false,\n selection: {\n anchor: utils.getBlockStartPoint({\n node: block,\n path: [{_key: block._key}],\n }),\n focus: utils.getBlockEndPoint({\n node: block,\n path: [{_key: block._key}],\n }),\n },\n }\n }\n\n return {\n block: positionBlock,\n isEditor: Editor.isEditor(node),\n selection,\n }\n}\n\nexport function getEventNode({\n slateEditor,\n event,\n}: {\n slateEditor: PortableTextSlateEditor\n event: DragEvent | MouseEvent\n}) {\n if (!DOMEditor.hasTarget(slateEditor, event.target)) {\n return undefined\n }\n\n const node = DOMEditor.toSlateNode(slateEditor, event.target)\n\n return node\n}\n\nfunction getEventPositionBlock({\n node,\n slateEditor,\n event,\n}: {\n node: Node\n slateEditor: PortableTextSlateEditor\n event: DragEvent | MouseEvent\n}): EventPositionBlock | undefined {\n const [firstBlock] = getFirstBlock({editor: slateEditor})\n\n if (!firstBlock) {\n return undefined\n }\n\n const firstBlockElement = DOMEditor.toDOMNode(slateEditor, firstBlock)\n const firstBlockRect = firstBlockElement.getBoundingClientRect()\n\n if (event.pageY < firstBlockRect.top) {\n return 'start'\n }\n\n const [lastBlock] = getLastBlock({editor: slateEditor})\n\n if (!lastBlock) {\n return undefined\n }\n\n const lastBlockElement = DOMEditor.toDOMNode(slateEditor, lastBlock)\n const lastBlockRef = lastBlockElement.getBoundingClientRect()\n\n if (event.pageY > lastBlockRef.bottom) {\n return 'end'\n }\n\n const element = DOMEditor.toDOMNode(slateEditor, node)\n const elementRect = element.getBoundingClientRect()\n const top = elementRect.top\n const height = elementRect.height\n const location = Math.abs(top - event.pageY)\n\n return location < height / 2 ? 'start' : 'end'\n}\n\nexport function getEventSelection({\n schema,\n slateEditor,\n event,\n}: {\n schema: EditorSchema\n slateEditor: PortableTextSlateEditor\n event: DragEvent | MouseEvent\n}): EditorSelection {\n const range = getSlateRangeFromEvent(slateEditor, event)\n\n const selection = range\n ? slateRangeToSelection({\n schema,\n editor: slateEditor,\n range,\n })\n : null\n\n return selection\n}\n\nfunction getSlateRangeFromEvent(\n editor: PortableTextSlateEditor,\n event: DragEvent | MouseEvent,\n) {\n if (!event.target) {\n return undefined\n }\n\n if (!isDOMNode(event.target)) {\n return undefined\n }\n\n const window = DOMEditor.getWindow(editor)\n\n let domRange: Range | undefined\n\n if (window.document.caretPositionFromPoint !== undefined) {\n const position = window.document.caretPositionFromPoint(\n event.clientX,\n event.clientY,\n )\n\n if (position) {\n try {\n domRange = window.document.createRange()\n domRange.setStart(position.offsetNode, position.offset)\n domRange.setEnd(position.offsetNode, position.offset)\n } catch {}\n }\n } else if (window.document.caretRangeFromPoint !== undefined) {\n // Use WebKit-proprietary fallback method\n domRange =\n window.document.caretRangeFromPoint(event.clientX, event.clientY) ??\n undefined\n } else {\n console.warn(\n 'Neither caretPositionFromPoint nor caretRangeFromPoint is supported',\n )\n return undefined\n }\n\n if (!domRange) {\n return undefined\n }\n\n let range: BaseRange | undefined\n\n try {\n range = DOMEditor.toSlateRange(editor, domRange, {\n exactMatch: false,\n // It can still throw even with this option set to true\n suppressThrow: false,\n })\n } catch {}\n\n return range\n}\n","import type {Path, PortableTextBlock} from '@sanity/types'\nimport {isEqual} from 'lodash'\nimport type {EditorSelection, EditorSelectionPoint} from '../types/editor'\n\nexport function normalizePoint(\n point: EditorSelectionPoint,\n value: PortableTextBlock[],\n): EditorSelectionPoint | null {\n if (!point || !value) {\n return null\n }\n const newPath: Path = []\n let newOffset: number = point.offset || 0\n const blockKey =\n typeof point.path[0] === 'object' &&\n '_key' in point.path[0] &&\n point.path[0]._key\n const childKey =\n typeof point.path[2] === 'object' &&\n '_key' in point.path[2] &&\n point.path[2]._key\n const block: PortableTextBlock | undefined = value.find(\n (blk) => blk._key === blockKey,\n )\n if (block) {\n newPath.push({_key: block._key})\n } else {\n return null\n }\n if (block && point.path[1] === 'children') {\n if (\n !block.children ||\n (Array.isArray(block.children) && block.children.length === 0)\n ) {\n return null\n }\n const child =\n Array.isArray(block.children) &&\n block.children.find((cld) => cld._key === childKey)\n if (child) {\n newPath.push('children')\n newPath.push({_key: child._key})\n newOffset =\n child.text && child.text.length >= point.offset\n ? point.offset\n : (child.text && child.text.length) || 0\n } else {\n return null\n }\n }\n return {path: newPath, offset: newOffset}\n}\n\nexport function normalizeSelection(\n selection: EditorSelection,\n value: PortableTextBlock[] | undefined,\n): EditorSelection | null {\n if (!selection || !value || value.length === 0) {\n return null\n }\n let newAnchor: EditorSelectionPoint | null = null\n let newFocus: EditorSelectionPoint | null = null\n const {anchor, focus} = selection\n if (\n anchor &&\n value.find((blk) => isEqual({_key: blk._key}, anchor.path[0]))\n ) {\n newAnchor = normalizePoint(anchor, value)\n }\n if (focus && value.find((blk) => isEqual({_key: blk._key}, focus.path[0]))) {\n newFocus = normalizePoint(focus, value)\n }\n if (newAnchor && newFocus) {\n return {anchor: newAnchor, focus: newFocus, backward: selection.backward}\n }\n return null\n}\n","import {Editor} from 'slate'\nimport {DOMEditor} from 'slate-dom'\nimport type {EditorSnapshot} from '..'\nimport type {PortableTextSlateEditor} from '../types/editor'\nimport {toSlateRange} from './ranges'\n\nexport type SelectionDomNodes = {\n blockNodes: Array<Node>\n childNodes: Array<Node>\n}\n\nexport function getSelectionDomNodes({\n slateEditor,\n snapshot,\n}: {\n slateEditor: PortableTextSlateEditor\n snapshot: EditorSnapshot\n}): SelectionDomNodes {\n if (!snapshot.context.selection) {\n return {\n blockNodes: [],\n childNodes: [],\n }\n }\n\n const range = toSlateRange(snapshot.context.selection, slateEditor)\n\n if (!range) {\n return {\n blockNodes: [],\n childNodes: [],\n }\n }\n\n const blockEntries = Array.from(\n Editor.nodes(slateEditor, {\n at: range,\n mode: 'highest',\n match: (n) => !Editor.isEditor(n),\n }),\n )\n\n const childEntries = Array.from(\n Editor.nodes(slateEditor, {\n at: range,\n mode: 'lowest',\n match: (n) =>\n (!Editor.isEditor(n) && slateEditor.isTextSpan(n)) ||\n !slateEditor.isBlock(n),\n }),\n )\n\n return {\n blockNodes: blockEntries.map(([blockNode]) =>\n DOMEditor.toDOMNode(slateEditor, blockNode),\n ),\n childNodes: childEntries.map(([childNode]) =>\n DOMEditor.toDOMNode(slateEditor, childNode),\n ),\n }\n}\n","import type {PortableTextBlock, PortableTextChild} from '@sanity/types'\n\nexport function DefaultBlockObject(props: {\n value: PortableTextBlock | PortableTextChild\n}) {\n return (\n <div style={{userSelect: 'none'}}>\n [{props.value._type}: {props.value._key}]\n </div>\n )\n}\n\nexport function DefaultInlineObject(props: {\n value: PortableTextBlock | PortableTextChild\n}) {\n return (\n <span style={{userSelect: 'none'}}>\n [{props.value._type}: {props.value._key}]\n </span>\n )\n}\n","export function DropIndicator() {\n return (\n <div\n contentEditable={false}\n className=\"pt-drop-indicator\"\n style={{\n position: 'absolute',\n width: '100%',\n height: 1,\n borderBottom: '1px solid currentColor',\n zIndex: 5,\n }}\n >\n <span />\n </div>\n )\n}\n","import type {\n Path,\n PortableTextChild,\n PortableTextObject,\n PortableTextTextBlock,\n} from '@sanity/types'\nimport {\n useContext,\n useEffect,\n useMemo,\n useRef,\n useState,\n type FunctionComponent,\n type JSX,\n type ReactElement,\n} from 'react'\nimport {Editor, Range, Element as SlateElement} from 'slate'\nimport {\n ReactEditor,\n useSelected,\n useSlateStatic,\n type RenderElementProps,\n} from 'slate-react'\nimport {defineBehavior} from '../../behaviors'\nimport {debugWithName} from '../../internal-utils/debug'\nimport type {EventPositionBlock} from '../../internal-utils/event-position'\nimport {fromSlateValue} from '../../internal-utils/values'\nimport {KEY_TO_VALUE_ELEMENT} from '../../internal-utils/weakMaps'\nimport * as selectors from '../../selectors'\nimport type {\n BlockRenderProps,\n PortableTextMemberSchemaTypes,\n RenderBlockFunction,\n RenderChildFunction,\n RenderListItemFunction,\n RenderStyleFunction,\n} from '../../types/editor'\nimport {EditorActorContext} from '../editor-actor-context'\nimport {DefaultBlockObject, DefaultInlineObject} from './DefaultObject'\nimport {DropIndicator} from './drop-indicator'\n\nconst debug = debugWithName('components:Element')\nconst debugRenders = false\nconst EMPTY_ANNOTATIONS: PortableTextObject[] = []\n\n/**\n * @internal\n */\nexport interface ElementProps {\n attributes: RenderElementProps['attributes']\n children: ReactElement<any>\n element: SlateElement\n schemaTypes: PortableTextMemberSchemaTypes\n readOnly: boolean\n renderBlock?: RenderBlockFunction\n renderChild?: RenderChildFunction\n renderListItem?: RenderListItemFunction\n renderStyle?: RenderStyleFunction\n spellCheck?: boolean\n}\n\nconst inlineBlockStyle = {display: 'inline-block'}\n\n/**\n * Renders Portable Text block and inline object nodes in Slate\n * @internal\n */\nexport const Element: FunctionComponent<ElementProps> = ({\n attributes,\n children,\n element,\n schemaTypes,\n readOnly,\n renderBlock,\n renderChild,\n renderListItem,\n renderStyle,\n spellCheck,\n}) => {\n const editorActor = useContext(EditorActorContext)\n const slateEditor = useSlateStatic()\n const selected = useSelected()\n const blockRef = useRef<HTMLDivElement | null>(null)\n const inlineBlockObjectRef = useRef(null)\n const focused =\n (selected &&\n slateEditor.selection &&\n Range.isCollapsed(slateEditor.selection)) ||\n false\n const [dragPositionBlock, setDragPositionBlock] =\n useState<EventPositionBlock>()\n\n useEffect(() => {\n const behavior = defineBehavior({\n on: 'drag.dragover',\n guard: ({snapshot, event}) => {\n const dropFocusBlock = selectors.getFocusBlock({\n ...snapshot,\n context: {\n ...snapshot.context,\n selection: event.position.selection,\n },\n })\n\n if (!dropFocusBlock || dropFocusBlock.node._key !== element._key) {\n return false\n }\n\n const dragOrigin = snapshot.beta.internalDrag?.origin\n\n if (!dragOrigin) {\n return false\n }\n\n const draggedBlocks = selectors.getSelectedBlocks({\n ...snapshot,\n context: {\n ...snapshot.context,\n selection: dragOrigin.selection,\n },\n })\n\n if (\n draggedBlocks.some(\n (draggedBlock) => draggedBlock.node._key === element._key,\n )\n ) {\n return false\n }\n\n const draggingEntireBlocks = selectors.isSelectingEntireBlocks({\n ...snapshot,\n context: {\n ...snapshot.context,\n selection: dragOrigin.selection,\n },\n })\n\n return draggingEntireBlocks\n },\n actions: [\n ({event}) => [\n {\n type: 'effect',\n effect: () => {\n setDragPositionBlock(event.position.block)\n },\n },\n {\n type: 'noop',\n },\n ],\n ],\n })\n\n editorActor.send({\n type: 'add behavior',\n behavior,\n })\n\n return () => {\n editorActor.send({\n type: 'remove behavior',\n behavior,\n })\n }\n }, [editorActor, element._key])\n\n useEffect(() => {\n const behavior = defineBehavior({\n on: 'drag.*',\n guard: ({event}) => {\n return event.type !== 'drag.dragover'\n },\n actions: [\n () => [\n {\n type: 'effect',\n effect: () => {\n setDragPositionBlock(undefined)\n },\n },\n ],\n ],\n })\n\n editorActor.send({\n type: 'add behavior',\n behavior,\n })\n\n return () => {\n editorActor.send({\n type: 'remove behavior',\n behavior,\n })\n }\n }, [editorActor])\n\n const value = useMemo(\n () =>\n fromSlateValue(\n [element],\n schemaTypes.block.name,\n KEY_TO_VALUE_ELEMENT.get(slateEditor),\n )[0],\n [slateEditor, element, schemaTypes.block.name],\n )\n\n let renderedBlock = children\n\n let className: string | undefined\n\n const blockPath: Path = useMemo(() => [{_key: element._key}], [element])\n\n if (typeof element._type !== 'string') {\n throw new Error(`Expected element to have a _type property`)\n }\n\n if (typeof element._key !== 'string') {\n throw new Error(`Expected element to have a _key property`)\n }\n\n // Test for inline objects first\n if (slateEditor.isInline(element)) {\n const path = ReactEditor.findPath(slateEditor, element)\n const [block] = Editor.node(slateEditor, path, {depth: 1})\n const schemaType = schemaTypes.inlineObjects.find(\n (_type) => _type.name === element._type,\n )\n if (!schemaType) {\n throw new Error('Could not find type for inline block element')\n }\n if (SlateElement.isElement(block)) {\n const elmPath: Path = [\n {_key: block._key},\n 'children',\n {_key: element._key},\n ]\n if (debugRenders) {\n debug(`Render ${element._key} (inline object)`)\n }\n return (\n <span {...attributes}>\n {/* Note that children must follow immediately or cut and selections will not work properly in Chrome. */}\n {children}\n <span\n draggable={!readOnly}\n className=\"pt-inline-object\"\n data-testid=\"pt-inline-object\"\n ref={inlineBlockObjectRef}\n key={element._key}\n style={inlineBlockStyle}\n contentEditable={false}\n >\n {renderChild &&\n renderChild({\n annotations: EMPTY_ANNOTATIONS, // These inline objects currently doesn't support annotations. This is a limitation of the current PT spec/model.\n children: <DefaultInlineObject value={value} />,\n editorElementRef: inlineBlockObjectRef,\n focused,\n path: elmPath,\n schemaType,\n selected,\n type: schemaType,\n value: value as PortableTextChild,\n })}\n {!renderChild && <DefaultInlineObject value={value} />}\n </span>\n </span>\n )\n }\n throw new Error('Block not found!')\n }\n\n // If not inline, it's either a block (text) or a block object (non-text)\n // NOTE: text blocks aren't draggable with DraggableBlock (yet?)\n if (element._type === schemaTypes.block.name) {\n className = `pt-block pt-text-block`\n const isListItem = 'listItem' in element\n if (debugRenders) {\n debug(`Render ${element._key} (text block)`)\n }\n const style = ('style' in element && element.style) || 'normal'\n className = `pt-block pt-text-block pt-text-block-style-${style}`\n const blockStyleType = schemaTypes.styles.find(\n (item) => item.value === style,\n )\n if (renderStyle && blockStyleType) {\n renderedBlock = renderStyle({\n block: element as PortableTextTextBlock,\n children,\n focused,\n selected,\n value: style,\n path: blockPath,\n schemaType: blockStyleType,\n editorElementRef: blockRef,\n })\n }\n let level: number | undefined\n\n if (isListItem) {\n if (typeof element.level === 'number') {\n level = element.level\n }\n className += ` pt-list-item pt-list-item-${element.listItem} pt-list-item-level-${level || 1}`\n }\n\n if (slateEditor.isListBlock(value) && isListItem && element.listItem) {\n const listType = schemaTypes.lists.find(\n (item) => item.value === element.listItem,\n )\n if (renderListItem && listType) {\n renderedBlock = renderListItem({\n block: value,\n children: renderedBlock,\n focused,\n selected,\n value: element.listItem,\n path: blockPath,\n schemaType: listType,\n level: value.level || 1,\n editorElementRef: blockRef,\n })\n }\n }\n\n const renderProps: Omit<BlockRenderProps, 'type'> = Object.defineProperty(\n {\n children: renderedBlock,\n editorElementRef: blockRef,\n focused,\n level,\n listItem: isListItem ? element.listItem : undefined,\n path: blockPath,\n selected,\n style,\n schemaType: schemaTypes.block,\n value,\n },\n 'type',\n {\n enumerable: false,\n get() {\n console.warn(\n \"Property 'type' is deprecated, use 'schemaType' instead.\",\n )\n return schemaTypes.block\n },\n },\n )\n\n const propsOrDefaultRendered = renderBlock\n ? renderBlock(renderProps as BlockRenderProps)\n : children\n\n return (\n <div\n key={element._key}\n {...attributes}\n className={className}\n spellCheck={spellCheck}\n >\n {dragPositionBlock === 'start' ? <DropIndicator /> : null}\n <div ref={blockRef}>{propsOrDefaultRendered}</div>\n {dragPositionBlock === 'end' ? <DropIndicator /> : null}\n </div>\n )\n }\n\n const schemaType = schemaTypes.blockObjects.find(\n (_type) => _type.name === element._type,\n )\n\n if (!schemaType) {\n throw new Error(\n `Could not find schema type for block element of _type ${element._type}`,\n )\n }\n\n if (debugRenders) {\n debug(`Render ${element._key} (object block)`)\n }\n\n className = 'pt-block pt-object-block'\n\n const block = fromSlateValue(\n [element],\n schemaTypes.block.name,\n KEY_TO_VALUE_ELEMENT.get(slateEditor),\n )[0]\n\n let renderedBlockFromProps: JSX.Element | undefined\n\n if (renderBlock) {\n const _props: Omit<BlockRenderProps, 'type'> = Object.defineProperty(\n {\n children: <DefaultBlockObject value={value} />,\n editorElementRef: blockRef,\n focused,\n path: blockPath,\n schemaType,\n selected,\n value: block,\n },\n 'type',\n {\n enumerable: false,\n get() {\n console.warn(\n \"Property 'type' is deprecated, use 'schemaType' instead.\",\n )\n return schemaType\n },\n },\n )\n renderedBlockFromProps = renderBlock(_props as BlockRenderProps)\n }\n\n return (\n <div key={element._key} {...attributes} className={className}>\n {dragPositionBlock === 'start' ? <DropIndicator /> : null}\n {children}\n <div ref={blockRef} contentEditable={false} draggable={!readOnly}>\n {renderedBlockFromProps ? (\n renderedBlockFromProps\n ) : (\n <DefaultBlockObject value={value} />\n )}\n </div>\n {dragPositionBlock === 'end' ? <DropIndicator /> : null}\n </div>\n )\n}\n\nElement.displayName = 'Element'\n","import type {\n Path,\n PortableTextObject,\n PortableTextTextBlock,\n} from '@sanity/types'\nimport {isEqual, uniq} from 'lodash'\nimport {\n startTransition,\n useCallback,\n useEffect,\n useMemo,\n useRef,\n useState,\n type ReactElement,\n} from 'react'\nimport {Text} from 'slate'\nimport {useSelected, type RenderLeafProps} from 'slate-react'\nimport {debugWithName} from '../../internal-utils/debug'\nimport type {\n BlockAnnotationRenderProps,\n BlockChildRenderProps,\n BlockDecoratorRenderProps,\n PortableTextMemberSchemaTypes,\n RenderAnnotationFunction,\n RenderChildFunction,\n RenderDecoratorFunction,\n} from '../../types/editor'\nimport type {EditorActor} from '../editor-machine'\nimport {usePortableTextEditor} from '../hooks/usePortableTextEditor'\nimport {PortableTextEditor} from '../PortableTextEditor'\n\nconst debug = debugWithName('components:Leaf')\n\nconst EMPTY_MARKS: string[] = []\n\n/**\n * @internal\n */\nexport interface LeafProps extends RenderLeafProps {\n editorActor: EditorActor\n children: ReactElement<any>\n schemaTypes: PortableTextMemberSchemaTypes\n renderAnnotation?: RenderAnnotationFunction\n renderChild?: RenderChildFunction\n renderDecorator?: RenderDecoratorFunction\n readOnly: boolean\n}\n\n/**\n * Renders Portable Text span nodes in Slate\n * @internal\n */\nexport const Leaf = (props: LeafProps) => {\n const {\n editorActor,\n attributes,\n children,\n leaf,\n schemaTypes,\n renderChild,\n renderDecorator,\n renderAnnotation,\n } = props\n const spanRef = useRef<HTMLElement>(null)\n const portableTextEditor = usePortableTextEditor()\n const blockSelected = useSelected()\n const [focused, setFocused] = useState(false)\n const [selected, setSelected] = useState(false)\n const block = children.props.parent as PortableTextTextBlock | undefined\n const path: Path = useMemo(\n () => (block ? [{_key: block?._key}, 'children', {_key: leaf._key}] : []),\n [block, leaf._key],\n )\n const decoratorValues = useMemo(\n () => schemaTypes.decorators.map((dec) => dec.value),\n [schemaTypes.decorators],\n )\n const marks: string[] = useMemo(\n () =>\n uniq(\n (leaf.marks || EMPTY_MARKS).filter((mark) =>\n decoratorValues.includes(mark),\n ),\n ),\n [decoratorValues, leaf.marks],\n )\n const annotationMarks = Array.isArray(leaf.marks) ? leaf.marks : EMPTY_MARKS\n const annotations = useMemo(\n () =>\n annotationMarks\n .map(\n (mark) =>\n !decoratorValues.includes(mark) &&\n block?.markDefs?.find((def) => def._key === mark),\n )\n .filter(Boolean) as PortableTextObject[],\n [annotationMarks, block, decoratorValues],\n )\n\n const shouldTrackSelectionAndFocus = annotations.length > 0 && blockSelected\n\n useEffect(() => {\n if (!shouldTrackSelectionAndFocus) {\n setFocused(false)\n return\n }\n const sel = PortableTextEditor.getSelection(portableTextEditor)\n if (\n sel &&\n isEqual(sel.focus.path, path) &&\n PortableTextEditor.isCollapsedSelection(portableTextEditor)\n ) {\n startTransition(() => {\n setFocused(true)\n })\n }\n }, [shouldTrackSelectionAndFocus, path, portableTextEditor])\n\n // Function to check if this leaf is currently inside the user's text selection\n const setSelectedFromRange = useCallback(() => {\n if (!shouldTrackSelectionAndFocus) {\n return\n }\n debug('Setting selection and focus from range')\n const winSelection = window.getSelection()\n if (!winSelection) {\n setSelected(false)\n return\n }\n if (winSelection && winSelection.rangeCount > 0) {\n const range = winSelection.getRangeAt(0)\n if (spanRef.current && range.intersectsNode(spanRef.current)) {\n setSelected(true)\n } else {\n setSelected(false)\n }\n } else {\n setSelected(false)\n }\n }, [shouldTrackSelectionAndFocus])\n\n useEffect(() => {\n if (!shouldTrackSelectionAndFocus) {\n return undefined\n }\n\n const onBlur = editorActor.on('blurred', () => {\n setFocused(false)\n setSelected(false)\n })\n\n const onFocus = editorActor.on('focused', () => {\n const sel = PortableTextEditor.getSelection(portableTextEditor)\n if (\n sel &&\n isEqual(sel.focus.path, path) &&\n PortableTextEditor.isCollapsedSelection(portableTextEditor)\n ) {\n setFocused(true)\n }\n setSelectedFromRange()\n })\n\n const onSelection = editorActor.on('selection', (event) => {\n if (\n event.selection &&\n isEqual(event.selection.focus.path, path) &&\n PortableTextEditor.isCollapsedSelection(portableTextEditor)\n ) {\n setFocused(true)\n } else {\n setFocused(false)\n }\n setSelectedFromRange()\n })\n\n return () => {\n onBlur.unsubscribe()\n onFocus.unsubscribe()\n onSelection.unsubscribe()\n }\n }, [\n editorActor,\n path,\n portableTextEditor,\n setSelectedFromRange,\n shouldTrackSelectionAndFocus,\n ])\n\n useEffect(() => setSelectedFromRange(), [setSelectedFromRange])\n\n const content = useMemo(() => {\n let returnedChildren = children\n // Render text nodes\n if (Text.isText(leaf) && leaf._type === schemaTypes.span.name) {\n marks.forEach((mark) => {\n const schemaType = schemaTypes.decorators.find(\n (dec) => dec.value === mark,\n )\n if (schemaType && renderDecorator) {\n const _props: Omit<BlockDecoratorRenderProps, 'type'> =\n Object.defineProperty(\n {\n children: returnedChildren,\n editorElementRef: spanRef,\n focused,\n path,\n selected,\n schemaType,\n value: mark,\n },\n 'type',\n {\n enumerable: false,\n get() {\n console.warn(\n \"Property 'type' is deprecated, use 'schemaType' instead.\",\n )\n return schemaType\n },\n },\n )\n returnedChildren = renderDecorator(\n _props as BlockDecoratorRenderProps,\n )\n }\n })\n\n if (block && annotations.length > 0) {\n annotations.forEach((annotation) => {\n const schemaType = schemaTypes.annotations.find(\n (t) => t.name === annotation._type,\n )\n if (schemaType) {\n if (renderAnnotation) {\n const _props: Omit<BlockAnnotationRenderProps, 'type'> =\n Object.defineProperty(\n {\n block,\n children: returnedChildren,\n editorElementRef: spanRef,\n focused,\n path,\n selected,\n schemaType,\n value: annotation,\n },\n 'type',\n {\n enumerable: false,\n get() {\n console.warn(\n \"Property 'type' is deprecated, use 'schemaType' instead.\",\n )\n return schemaType\n },\n },\n )\n\n returnedChildren = (\n <span ref={spanRef}>\n {renderAnnotation(_props as BlockAnnotationRenderProps)}\n </span>\n )\n } else {\n returnedChildren = <span ref={spanRef}>{returnedChildren}</span>\n }\n }\n })\n }\n if (block && renderChild) {\n const child = block.children.find((_child) => _child._key === leaf._key) // Ensure object equality\n if (child) {\n const defaultRendered = <>{returnedChildren}</>\n const _props: Omit<BlockChildRenderProps, 'type'> =\n Object.defineProperty(\n {\n annotations,\n children: defaultRendered,\n editorElementRef: spanRef,\n focused,\n path,\n schemaType: schemaTypes.span,\n selected,\n value: child,\n },\n 'type',\n {\n enumerable: false,\n get() {\n console.warn(\n \"Property 'type' is deprecated, use 'schemaType' instead.\",\n )\n return schemaTypes.span\n },\n },\n )\n returnedChildren = renderChild(_props as BlockChildRenderProps)\n }\n }\n }\n return returnedChildren\n }, [\n annotations,\n block,\n children,\n focused,\n leaf,\n marks,\n path,\n renderAnnotation,\n renderChild,\n renderDecorator,\n schemaTypes.annotations,\n schemaTypes.decorators,\n schemaTypes.span,\n selected,\n ])\n return useMemo(\n () => (\n <span key={leaf._key} {...attributes} ref={spanRef}>\n {content}\n </span>\n ),\n [leaf, attributes, content],\n )\n}\n\nLeaf.displayName = 'Leaf'\n","import type {KeyboardEvent} from 'react'\nimport type {ReactEditor} from 'slate-react'\nimport {debugWithName} from '../../internal-utils/debug'\nimport {isHotkey} from '../../internal-utils/is-hotkey'\nimport type {PortableTextSlateEditor} from '../../types/editor'\nimport type {HotkeyOptions} from '../../types/options'\nimport type {EditorActor} from '../editor-machine'\nimport type {PortableTextEditor} from '../PortableTextEditor'\n\nconst debug = debugWithName('plugin:withHotKeys')\n\n/**\n * This plugin takes care of all hotkeys in the editor\n *\n */\nexport function createWithHotkeys(\n editorActor: EditorActor,\n portableTextEditor: PortableTextEditor,\n hotkeysFromOptions?: HotkeyOptions,\n): (editor: PortableTextSlateEditor & ReactEditor) => any {\n const reservedHotkeys = ['enter', 'tab', 'shift', 'delete', 'end']\n const activeHotkeys = hotkeysFromOptions ?? {}\n return function withHotKeys(editor: PortableTextSlateEditor & ReactEditor) {\n editor.pteWithHotKeys = (event: KeyboardEvent<HTMLDivElement>): void => {\n // Wire up custom marks hotkeys\n Object.keys(activeHotkeys).forEach((cat) => {\n if (cat === 'marks') {\n for (const hotkey in activeHotkeys[cat]) {\n if (reservedHotkeys.includes(hotkey)) {\n throw new Error(`The hotkey ${hotkey} is reserved!`)\n }\n if (isHotkey(hotkey, event.nativeEvent)) {\n event.preventDefault()\n const possibleMark = activeHotkeys[cat]\n if (possibleMark) {\n const mark = possibleMark[hotkey]\n debug(`HotKey ${hotkey} to toggle ${mark}`)\n editorActor.send({\n type: 'behavior event',\n behaviorEvent: {\n type: 'decorator.toggle',\n decorator: mark,\n },\n editor,\n })\n }\n }\n }\n }\n if (cat === 'custom') {\n for (const hotkey in activeHotkeys[cat]) {\n if (reservedHotkeys.includes(hotkey)) {\n throw new Error(`The hotkey ${hotkey} is reserved!`)\n }\n if (isHotkey(hotkey, event.nativeEvent)) {\n const possibleCommand = activeHotkeys[cat]\n if (possibleCommand) {\n const command = possibleCommand[hotkey]\n command(event, portableTextEditor)\n }\n }\n }\n }\n })\n }\n return editor\n }\n}\n","import {isEqual} from 'lodash'\nimport {\n Element,\n Path,\n Range,\n type BaseRange,\n type NodeEntry,\n type Operation,\n} from 'slate'\nimport {\n and,\n assertEvent,\n assign,\n fromCallback,\n setup,\n type ActorRefFrom,\n type AnyEventObject,\n type CallbackLogicFunction,\n} from 'xstate'\nimport {moveRangeByOperation, toSlateRange} from '../internal-utils/ranges'\nimport {slateRangeToSelection} from '../internal-utils/slate-utils'\nimport {isEqualToEmptyEditor} from '../internal-utils/values'\nimport type {PortableTextSlateEditor, RangeDecoration} from '../types/editor'\nimport type {EditorSchema} from './editor-schema'\n\nconst slateOperationCallback: CallbackLogicFunction<\n AnyEventObject,\n {type: 'slate operation'; operation: Operation},\n {slateEditor: PortableTextSlateEditor}\n> = ({input, sendBack}) => {\n const originalApply = input.slateEditor.apply\n\n input.slateEditor.apply = (op) => {\n if (op.type !== 'set_selection') {\n sendBack({type: 'slate operation', operation: op})\n }\n\n originalApply(op)\n }\n\n return () => {\n input.slateEditor.apply = originalApply\n }\n}\n\ntype DecoratedRange = BaseRange & {rangeDecoration: RangeDecoration}\n\nexport const rangeDecorationsMachine = setup({\n types: {\n context: {} as {\n decoratedRanges: Array<DecoratedRange>\n pendingRangeDecorations: Array<RangeDecoration>\n skipSetup: boolean\n readOnly: boolean\n schema: EditorSchema\n slateEditor: PortableTextSlateEditor\n updateCount: number\n },\n input: {} as {\n rangeDecorations: Array<RangeDecoration>\n readOnly: boolean\n schema: EditorSchema\n skipSetup: boolean\n slateEditor: PortableTextSlateEditor\n },\n events: {} as\n | {\n type: 'ready'\n }\n | {\n type: 'range decorations updated'\n rangeDecorations: Array<RangeDecoration>\n }\n | {\n type: 'slate operation'\n operation: Operation\n }\n | {\n type: 'update read only'\n readOnly: boolean\n },\n },\n actions: {\n 'update pending range decorations': assign({\n pendingRangeDecorations: ({event}) => {\n assertEvent(event, 'range decorations updated')\n\n return event.rangeDecorations\n },\n }),\n 'set up initial range decorations': assign({\n decoratedRanges: ({context, event}) => {\n assertEvent(event, 'ready')\n\n const rangeDecorationState: Array<DecoratedRange> = []\n\n for (const rangeDecoration of context.pendingRangeDecorations) {\n const slateRange = toSlateRange(\n rangeDecoration.selection,\n context.slateEditor,\n )\n\n if (!Range.isRange(slateRange)) {\n rangeDecoration.onMoved?.({\n newSelection: null,\n rangeDecoration,\n origin: 'local',\n })\n continue\n }\n\n rangeDecorationState.push({\n rangeDecoration,\n ...slateRange,\n })\n }\n\n return rangeDecorationState\n },\n }),\n 'update range decorations': assign({\n decoratedRanges: ({context, event}) => {\n assertEvent(event, 'range decorations updated')\n\n const rangeDecorationState: Array<DecoratedRange> = []\n\n for (const rangeDecoration of event.rangeDecorations) {\n const slateRange = toSlateRange(\n rangeDecoration.selection,\n context.slateEditor,\n )\n\n if (!Range.isRange(slateRange)) {\n rangeDecoration.onMoved?.({\n newSelection: null,\n rangeDecoration,\n origin: 'local',\n })\n continue\n }\n\n rangeDecorationState.push({\n rangeDecoration,\n ...slateRange,\n })\n }\n\n return rangeDecorationState\n },\n }),\n 'move range decorations': assign({\n decoratedRanges: ({context, event}) => {\n assertEvent(event, 'slate operation')\n\n const rangeDecorationState: Array<DecoratedRange> = []\n\n for (const decoratedRange of context.decoratedRanges) {\n const slateRange = toSlateRange(\n decoratedRange.rangeDecoration.selection,\n context.slateEditor,\n )\n\n if (!Range.isRange(slateRange)) {\n decoratedRange.rangeDecoration.onMoved?.({\n newSelection: null,\n rangeDecoration: decoratedRange.rangeDecoration,\n origin: 'local',\n })\n continue\n }\n\n let newRange: BaseRange | null | undefined\n\n newRange = moveRangeByOperation(slateRange, event.operation)\n if (\n (newRange && newRange !== slateRange) ||\n (newRange === null && slateRange)\n ) {\n const newRangeSelection = newRange\n ? slateRangeToSelection({\n schema: context.schema,\n editor: context.slateEditor,\n range: newRange,\n })\n : null\n\n decoratedRange.rangeDecoration.onMoved?.({\n newSelection: newRangeSelection,\n rangeDecoration: decoratedRange.rangeDecoration,\n origin: 'local',\n })\n }\n\n // If the newRange is null, it means that the range is not valid anymore and should be removed\n // If it's undefined, it means that the slateRange is still valid and should be kept\n if (newRange !== null) {\n rangeDecorationState.push({\n ...(newRange || slateRange),\n rangeDecoration: {\n ...decoratedRange.rangeDecoration,\n selection: slateRangeToSelection({\n schema: context.schema,\n editor: context.slateEditor,\n range: newRange,\n }),\n },\n })\n }\n }\n\n return rangeDecorationState\n },\n }),\n 'assign readOnly': assign({\n readOnly: ({event}) => {\n assertEvent(event, 'update read only')\n return event.readOnly\n },\n }),\n 'increment update count': assign({\n updateCount: ({context}) => {\n return context.updateCount + 1\n },\n }),\n },\n actors: {\n 'slate operation listener': fromCallback(slateOperationCallback),\n },\n guards: {\n 'has pending range decorations': ({context}) =>\n context.pendingRangeDecorations.length > 0,\n 'has range decorations': ({context}) => context.decoratedRanges.length > 0,\n 'has different decorations': ({context, event}) => {\n assertEvent(event, 'range decorations updated')\n\n const existingRangeDecorations = context.decoratedRanges.map(\n (decoratedRange) => ({\n anchor: decoratedRange.rangeDecoration.selection?.anchor,\n focus: decoratedRange.rangeDecoration.selection?.focus,\n }),\n )\n\n const newRangeDecorations = event.rangeDecorations.map(\n (rangeDecoration) => ({\n anchor: rangeDecoration.selection?.anchor,\n focus: rangeDecoration.selection?.focus,\n }),\n )\n\n const different = !isEqual(existingRangeDecorations, newRangeDecorations)\n\n return different\n },\n 'not read only': ({context}) => !context.readOnly,\n 'should skip setup': ({context}) => context.skipSetup,\n },\n}).createMachine({\n id: 'range decorations',\n context: ({input}) => ({\n readOnly: input.readOnly,\n pendingRangeDecorations: input.rangeDecorations,\n decoratedRanges: [],\n skipSetup: input.skipSetup,\n schema: input.schema,\n slateEditor: input.slateEditor,\n updateCount: 0,\n }),\n invoke: {\n src: 'slate operation listener',\n input: ({context}) => ({slateEditor: context.slateEditor}),\n },\n on: {\n 'update read only': {\n actions: ['assign readOnly'],\n },\n },\n initial: 'setting up',\n states: {\n 'setting up': {\n always: [\n {\n guard: and(['should skip setup', 'has pending range decorations']),\n target: 'ready',\n actions: [\n 'set up initial range decorations',\n 'increment update count',\n ],\n },\n {\n guard: 'should skip setup',\n target: 'ready',\n },\n ],\n on: {\n 'range decorations updated': {\n actions: ['update pending range decorations'],\n },\n 'ready': [\n {\n target: 'ready',\n guard: 'has pending range decorations',\n actions: [\n 'set up initial range decorations',\n 'increment update count',\n ],\n },\n {\n target: 'ready',\n },\n ],\n },\n },\n 'ready': {\n initial: 'idle',\n on: {\n 'range decorations updated': {\n target: '.idle',\n guard: 'has different decorations',\n actions: ['update range decorations', 'increment update count'],\n },\n },\n states: {\n 'idle': {\n on: {\n 'slate operation': {\n target: 'moving range decorations',\n guard: and(['has range decorations', 'not read only']),\n },\n },\n },\n 'moving range decorations': {\n entry: ['move range decorations'],\n always: {\n target: 'idle',\n },\n },\n },\n },\n },\n})\n\nexport function createDecorate(\n rangeDecorationActor: ActorRefFrom<typeof rangeDecorationsMachine>,\n) {\n return function decorate([node, path]: NodeEntry): Array<BaseRange> {\n if (\n isEqualToEmptyEditor(\n rangeDecorationActor.getSnapshot().context.slateEditor.children,\n rangeDecorationActor.getSnapshot().context.schema,\n )\n ) {\n return [\n {\n anchor: {\n path: [0, 0],\n offset: 0,\n },\n focus: {\n path: [0, 0],\n offset: 0,\n },\n placeholder: true,\n } as BaseRange,\n ]\n }\n\n // Editor node has a path length of 0 (should never be decorated)\n if (path.length === 0) {\n return []\n }\n\n if (!Element.isElement(node) || node