diff --git a/packages/editor/src/editor/PortableTextEditor.tsx b/packages/editor/src/editor/PortableTextEditor.tsx index 29fd2fa6..6a191d6e 100644 --- a/packages/editor/src/editor/PortableTextEditor.tsx +++ b/packages/editor/src/editor/PortableTextEditor.tsx @@ -11,7 +11,8 @@ import type { } from '@sanity/types' import {Component, type MutableRefObject, type PropsWithChildren} from 'react' import {Subject} from 'rxjs' -import {createActor} from 'xstate' +import {Editor} from 'slate' +import {assertEvent, createActor, raise} from 'xstate' import type { EditableAPI, EditableAPIDeleteOptions, @@ -26,7 +27,14 @@ import {getPortableTextMemberSchemaTypes} from '../utils/getPortableTextMemberSc import {compileType} from '../utils/schema' import {SlateContainer} from './components/SlateContainer' import {Synchronizer} from './components/Synchronizer' -import {editorMachine, type EditorActor} from './editor-machine' +import { + atTheEndOfSpan, + editorHasAnnotationMarks, + editorMachine, + selectingSpan, + selectionCollapsed, + type EditorActor, +} from './editor-machine' import {PortableTextEditorContext} from './hooks/usePortableTextEditor' import {defaultKeyGenerator} from './hooks/usePortableTextEditorKeyGenerator' import {PortableTextEditorSelectionProvider} from './hooks/usePortableTextEditorSelection' @@ -128,12 +136,43 @@ export class PortableTextEditor extends Component { : compileType(props.schemaType), ) - this.editorActor = createActor(editorMachine, { - input: { - keyGenerator: this.props.keyGenerator ?? defaultKeyGenerator, - schemaTypes: this.schemaTypes, + this.editorActor = createActor( + editorMachine.provide({ + actions: { + 'on insert text': raise(({context, event}) => { + assertEvent(event, 'insert text') + + if ( + selectionCollapsed({event}) && + selectingSpan({event}) && + atTheEndOfSpan({event}) && + editorHasAnnotationMarks({context, event}) + ) { + return { + type: 'insert span' as const, + editor: event.editor, + text: event.text, + marks: (Editor.marks(event.editor)?.marks ?? []).filter( + (mark) => context.schema.decorators.includes(mark), + ), + } + } + + return { + type: 'default insert text' as const, + text: event.text, + editor: event.editor, + } + }), + }, + }), + { + input: { + keyGenerator: this.props.keyGenerator ?? defaultKeyGenerator, + schemaTypes: this.schemaTypes, + }, }, - }) + ) this.editorActor.start() } diff --git a/packages/editor/src/editor/editor-machine.ts b/packages/editor/src/editor/editor-machine.ts index f6d8e7b2..7f1f6a53 100644 --- a/packages/editor/src/editor/editor-machine.ts +++ b/packages/editor/src/editor/editor-machine.ts @@ -3,7 +3,6 @@ import type {PortableTextBlock} from '@sanity/types' import type {FocusEvent} from 'react' import {Editor, Range, Transforms} from 'slate' import { - and, assertEvent, assign, emit, @@ -49,6 +48,17 @@ const networkLogic = fromCallback(({sendBack}) => { */ export type PatchEvent = {type: 'patch'; patch: Patch} +/** + * @internal + */ +export type EditorContext = { + keyGenerator: () => string + pendingEvents: Array + schema: { + decorators: Array + } +} + /** * @internal */ @@ -58,6 +68,24 @@ export type MutationEvent = { snapshot: Array | undefined } +/** + * @internal + */ +export type InsertTextEvent = { + type: 'insert text' + text: string + editor: PortableTextSlateEditor +} + +/** + * @internal + */ +export type DefaultInsertTextEvent = { + type: 'default insert text' + text: string + editor: PortableTextSlateEditor +} + type EditorEvent = | {type: 'normalizing'} | {type: 'done normalizing'} @@ -66,11 +94,8 @@ type EditorEvent = type: 'update schema' schemaTypes: PortableTextMemberSchemaTypes } - | { - type: 'insert text' - text: string - editor: PortableTextSlateEditor - } + | InsertTextEvent + | DefaultInsertTextEvent | { type: 'insert span' editor: PortableTextSlateEditor @@ -114,13 +139,7 @@ type EditorEmittedEvent = */ export const editorMachine = setup({ types: { - context: {} as { - keyGenerator: () => string - pendingEvents: Array - schema: { - decorators: Array - } - }, + context: {} as EditorContext, events: {} as EditorEvent, emitted: {} as EditorEmittedEvent, input: {} as { @@ -161,59 +180,10 @@ export const editorMachine = setup({ 'clear pending events': assign({ pendingEvents: [], }), - }, - guards: { - 'selection collapsed': ({event}) => { - assertEvent(event, 'insert text') - - return ( - event.editor.selection != null && - Range.isCollapsed(event.editor.selection) - ) - }, - 'selecting span': ({event}) => { - assertEvent(event, 'insert text') - - if (!event.editor.selection) { - return false - } - - const [node] = Array.from( - Editor.nodes(event.editor, { - mode: 'lowest', - at: event.editor.selection.focus, - match: (n) => event.editor.isTextSpan(n), - voids: false, - }), - )[0] - - return node !== undefined - }, - 'at the end of span': ({event}) => { - assertEvent(event, 'insert text') - - if (!event.editor.selection) { - return false - } - - const [node] = Array.from( - Editor.nodes(event.editor, { - mode: 'lowest', - at: event.editor.selection.focus, - match: (n) => event.editor.isTextSpan(n), - voids: false, - }), - )[0] - - return node.text.length === event.editor.selection.focus.offset - }, - 'editor has annotation marks': ({context, event}) => { + 'on insert text': raise(({event}) => { assertEvent(event, 'insert text') - - return (Editor.marks(event.editor)?.marks ?? []).some( - (mark) => !context.schema.decorators.includes(mark), - ) - }, + return {...event, type: 'default insert text' as const} + }), }, actors: { networkLogic, @@ -247,29 +217,15 @@ export const editorMachine = setup({ 'loading': {actions: emit({type: 'loading'})}, 'done loading': {actions: emit({type: 'done loading'})}, 'update schema': {actions: 'assign schema'}, - 'insert text': [ - { - guard: and([ - 'selecting span', - 'selection collapsed', - 'at the end of span', - 'editor has annotation marks', - ]), - actions: raise(({context, event}) => ({ - type: 'insert span', - text: event.text, - editor: event.editor, - marks: (Editor.marks(event.editor)?.marks ?? []).filter((mark) => - context.schema.decorators.includes(mark), - ), - })), - }, - { - actions: ({event}) => { - Editor.insertText(event.editor, event.text) - }, + 'insert text': { + actions: 'on insert text', + }, + 'default insert text': { + actions: ({event}) => { + assertEvent(event, 'default insert text') + Editor.insertText(event.editor, event.text) }, - ], + }, 'insert span': { actions: ({context, event}) => { Transforms.insertNodes(event.editor, { @@ -329,3 +285,67 @@ export const editorMachine = setup({ }, }, }) + +/** + * @internal + */ +export function editorHasAnnotationMarks({ + context, + event, +}: { + context: EditorContext + event: InsertTextEvent +}) { + return (Editor.marks(event.editor)?.marks ?? []).some( + (mark) => !context.schema.decorators.includes(mark), + ) +} + +/** + * @internal + */ +export function selectionCollapsed({event}: {event: InsertTextEvent}) { + return ( + event.editor.selection != null && Range.isCollapsed(event.editor.selection) + ) +} + +/** + * @internal + */ +export function selectingSpan({event}: {event: InsertTextEvent}) { + if (!event.editor.selection) { + return false + } + + const [node] = Array.from( + Editor.nodes(event.editor, { + mode: 'lowest', + at: event.editor.selection.focus, + match: (n) => event.editor.isTextSpan(n), + voids: false, + }), + )[0] + + return node !== undefined +} + +/** + * @internal + */ +export function atTheEndOfSpan({event}: {event: InsertTextEvent}) { + if (!event.editor.selection) { + return false + } + + const [node] = Array.from( + Editor.nodes(event.editor, { + mode: 'lowest', + at: event.editor.selection.focus, + match: (n) => event.editor.isTextSpan(n), + voids: false, + }), + )[0] + + return node.text.length === event.editor.selection.focus.offset +} diff --git a/packages/editor/src/index.ts b/packages/editor/src/index.ts index 3d0f9d38..5f93362c 100644 --- a/packages/editor/src/index.ts +++ b/packages/editor/src/index.ts @@ -3,7 +3,10 @@ export {PortableTextEditable} from './editor/Editable' export type {PortableTextEditableProps} from './editor/Editable' export { editorMachine, + type DefaultInsertTextEvent, type EditorActor, + type EditorContext, + type InsertTextEvent, type MutationEvent, type PatchEvent, } from './editor/editor-machine'