diff --git a/packages/editor/src/editor/Editable.tsx b/packages/editor/src/editor/Editable.tsx index 8f5e1a392..64520f6c9 100644 --- a/packages/editor/src/editor/Editable.tsx +++ b/packages/editor/src/editor/Editable.tsx @@ -295,7 +295,7 @@ export const PortableTextEditable = forwardRef< // The selection is usually automatically emitted to change$ by the withPortableTextSelections plugin whenever there is a set_selection operation applied. if (!slateEditor.operations.some((o) => o.type === 'set_selection')) { editorActor.send({ - type: 'selection', + type: 'notify.selection', selection: normalizedSelection, }) } @@ -465,7 +465,7 @@ export const PortableTextEditable = forwardRef< event.preventDefault() // Resolve it as promise (can be either async promise or sync return value) - editorActor.send({type: 'loading'}) + editorActor.send({type: 'notify.loading'}) Promise.resolve(onPasteResult) .then((result) => { @@ -494,7 +494,7 @@ export const PortableTextEditable = forwardRef< return error }) .finally(() => { - editorActor.send({type: 'done loading'}) + editorActor.send({type: 'notify.done loading'}) }) } else if (event.nativeEvent.clipboardData) { editorActor.send({ @@ -525,12 +525,12 @@ export const PortableTextEditable = forwardRef< Transforms.select(slateEditor, Editor.start(slateEditor, [])) slateEditor.onChange() } - editorActor.send({type: 'focused', event}) + editorActor.send({type: 'notify.focused', event}) const newSelection = PortableTextEditor.getSelection(portableTextEditor) // If the selection is the same, emit it explicitly here as there is no actual onChange event triggered. if (selection === newSelection) { editorActor.send({ - type: 'selection', + type: 'notify.selection', selection, }) } @@ -581,7 +581,7 @@ export const PortableTextEditable = forwardRef< onBlur(event) } if (!event.isPropagationStopped()) { - editorActor.send({type: 'blurred', event}) + editorActor.send({type: 'notify.blurred', event}) } }, [editorActor, onBlur], diff --git a/packages/editor/src/editor/components/Synchronizer.tsx b/packages/editor/src/editor/components/Synchronizer.tsx index 96baad8d1..735806236 100644 --- a/packages/editor/src/editor/components/Synchronizer.tsx +++ b/packages/editor/src/editor/components/Synchronizer.tsx @@ -67,7 +67,22 @@ export function Synchronizer(props: SynchronizerProps) { useEffect(() => { const subscription = syncActorRef.on('*', (event) => { - props.editorActor.send(event) + switch (event.type) { + case 'invalid value': + props.editorActor.send({ + ...event, + type: 'notify.invalid value', + }) + break + case 'value changed': + props.editorActor.send({ + ...event, + type: 'notify.value changed', + }) + break + default: + props.editorActor.send(event) + } }) return () => { diff --git a/packages/editor/src/editor/create-editor.ts b/packages/editor/src/editor/create-editor.ts index c36266023..d11cd0c09 100644 --- a/packages/editor/src/editor/create-editor.ts +++ b/packages/editor/src/editor/create-editor.ts @@ -11,10 +11,13 @@ import { type EventObject, type Snapshot, } from 'xstate' -import type {Behavior, CustomBehaviorEvent} from '../behaviors/behavior.types' +import type { + Behavior, + CustomBehaviorEvent, + SyntheticBehaviorEvent, +} from '../behaviors/behavior.types' import {coreConverters} from '../converters/converters.core' import {compileType} from '../internal-utils/schema' -import type {PickFromUnion} from '../type-utils' import type {EditableAPI} from '../types/editor' import {createEditorSchema} from './create-editor-schema' import {createSlateEditor, type SlateEditor} from './create-slate-editor' @@ -23,7 +26,7 @@ import { editorMachine, type EditorActor, type EditorEmittedEvent, - type InternalEditorEvent, + type ExternalEditorEvent, } from './editor-machine' import {getEditorSnapshot} from './editor-selector' import type {EditorSnapshot} from './editor-snapshot' @@ -60,51 +63,8 @@ export type EditorConfig = { * @public */ export type EditorEvent = - | PickFromUnion< - InternalEditorEvent, - 'type', - | 'annotation.add' - | 'annotation.remove' - | 'annotation.toggle' - | 'block.set' - | 'block.unset' - | 'blur' - | 'data transfer.set' - | 'decorator.add' - | 'decorator.remove' - | 'decorator.toggle' - | 'delete.block' - | 'delete.text' - | 'deserialization.failure' - | 'deserialization.success' - | 'focus' - | 'insert.block' - | 'insert.block object' - | 'insert.inline object' - | 'insert.span' - | 'insert.text block' - | 'list item.add' - | 'list item.remove' - | 'list item.toggle' - | 'move.block' - | 'move.block down' - | 'move.block up' - | 'select' - | 'select.next block' - | 'select.previous block' - | 'serialization.failure' - | 'serialization.success' - | 'style.add' - | 'style.remove' - | 'style.toggle' - | 'text block.set' - | 'text block.unset' - | 'patches' - | 'update behaviors' - | 'update key generator' - | 'update readOnly' - | 'update value' - > + | ExternalEditorEvent + | SyntheticBehaviorEvent | CustomBehaviorEvent /** diff --git a/packages/editor/src/editor/editor-machine.ts b/packages/editor/src/editor/editor-machine.ts index 346129d79..5365bb06b 100644 --- a/packages/editor/src/editor/editor-machine.ts +++ b/packages/editor/src/editor/editor-machine.ts @@ -21,7 +21,7 @@ import { type SyntheticBehaviorEvent, } from '../behaviors/behavior.types' import type {Converter} from '../converters/converter.types' -import type {OmitFromUnion, PickFromUnion} from '../type-utils' +import type {NamespaceEvent} from '../type-utils' import type { EditorSelection, InvalidValueResolution, @@ -33,16 +33,6 @@ import {withApplyingBehaviorActions} from './with-applying-behavior-actions' export * from 'xstate/guards' -/** - * @internal - */ -export type EditorActor = ActorRefFrom - -/** - * @internal - */ -export type PatchEvent = {type: 'patch'; patch: Patch} - /** * @public */ @@ -66,26 +56,9 @@ export type MutationEvent = { } /** - * @internal + * @public */ -export type InternalEditorEvent = - | {type: 'normalizing'} - | {type: 'done normalizing'} - | {type: 'done syncing initial value'} - | { - type: 'behavior event' - behaviorEvent: SyntheticBehaviorEvent | NativeBehaviorEvent - editor: PortableTextSlateEditor - defaultActionCallback?: () => void - nativeEvent?: {preventDefault: () => void} - } - | { - type: 'custom behavior event' - behaviorEvent: CustomBehaviorEvent - editor: PortableTextSlateEditor - nativeEvent?: {preventDefault: () => void} - } - | CustomBehaviorEvent +export type ExternalEditorEvent = | { type: 'add behavior' behavior: Behavior @@ -118,53 +91,21 @@ export type InternalEditorEvent = type: 'update maxBlocks' maxBlocks: number | undefined } - | OmitFromUnion< - InternalEditorEmittedEvent, - 'type', - 'ready' | 'read only' | 'editable' - > + | PatchesEvent /** * @public */ -export type EditorEmittedEvent = PickFromUnion< - InternalEditorEmittedEvent, - 'type', - | 'blurred' - | 'done loading' - | 'editable' - | 'error' - | 'focused' - | 'invalid value' - | 'loading' - | 'mutation' - | 'patch' - | 'read only' - | 'ready' - | 'selection' - | 'value changed' -> - -/** - * @internal - */ -export type InternalEditorEmittedEvent = - | {type: 'ready'} - | PatchEvent - | PatchesEvent - | MutationEvent +export type EditorEmittedEvent = | { - type: 'unset' - previousValue: Array + type: 'blurred' + event: FocusEvent } | { - type: 'value changed' - value: Array | undefined + type: 'done loading' } | { - type: 'invalid value' - resolution: InvalidValueResolution | null - value: Array | undefined + type: 'editable' } | { type: 'error' @@ -172,59 +113,96 @@ export type InternalEditorEmittedEvent = description: string data: unknown } - | {type: 'selection'; selection: EditorSelection} - | {type: 'blurred'; event: FocusEvent} - | {type: 'focused'; event: FocusEvent} - | {type: 'loading'} - | {type: 'done loading'} - | {type: 'read only'} - | {type: 'editable'} - | PickFromUnion< - SyntheticBehaviorEvent, - 'type', - | 'annotation.add' - | 'annotation.remove' - | 'annotation.toggle' - | 'block.set' - | 'block.unset' - | 'blur' - | 'data transfer.set' - | 'decorator.add' - | 'decorator.remove' - | 'decorator.toggle' - | 'delete.backward' - | 'delete.block' - | 'delete.forward' - | 'delete.text' - | 'deserialization.failure' - | 'deserialization.success' - | 'focus' - | 'insert.block' - | 'insert.block object' - | 'insert.inline object' - | 'insert.span' - | 'insert.text block' - | 'list item.add' - | 'list item.remove' - | 'list item.toggle' - | 'move.block' - | 'move.block down' - | 'move.block up' - | 'select' - | 'select.next block' - | 'select.previous block' - | 'serialization.failure' - | 'serialization.success' - | 'style.add' - | 'style.remove' - | 'style.toggle' - | 'text block.set' - | 'text block.unset' - > + | { + type: 'focused' + event: FocusEvent + } + | { + type: 'invalid value' + resolution: InvalidValueResolution | null + value: Array | undefined + } + | { + type: 'loading' + } + | MutationEvent + | PatchEvent + | { + type: 'read only' + } + | { + type: 'ready' + } + | { + type: 'selection' + selection: EditorSelection + } + | { + type: 'value changed' + value: Array | undefined + } + +type PatchEvent = { + type: 'patch' + patch: Patch +} + +type UnsetEvent = { + type: 'unset' + previousValue: Array +} + +/** + * @internal + */ +export type EditorActor = ActorRefFrom + +/** + * @internal + */ +export type InternalEditorEvent = + | { + type: 'normalizing' + } + | { + type: 'done normalizing' + } + | { + type: 'done syncing initial value' + } + | { + type: 'behavior event' + behaviorEvent: SyntheticBehaviorEvent | NativeBehaviorEvent + editor: PortableTextSlateEditor + defaultActionCallback?: () => void + nativeEvent?: {preventDefault: () => void} + } + | { + type: 'custom behavior event' + behaviorEvent: CustomBehaviorEvent + editor: PortableTextSlateEditor + nativeEvent?: {preventDefault: () => void} + } + | CustomBehaviorEvent + | ExternalEditorEvent + | MutationEvent + | NamespaceEvent + | NamespaceEvent + | PatchEvent + | SyntheticBehaviorEvent + +/** + * @internal + */ +export type InternalEditorEmittedEvent = + | EditorEmittedEvent + | PatchesEvent + | UnsetEvent | { type: 'custom.*' event: CustomBehaviorEvent } + | SyntheticBehaviorEvent /** * @internal @@ -514,23 +492,32 @@ export const editorMachine = setup({ value: input.value, }), on: { - 'add behavior': {actions: 'add behavior to context'}, - 'remove behavior': {actions: 'remove behavior from context'}, - 'unset': {actions: emit(({event}) => event)}, - 'value changed': {actions: emit(({event}) => event)}, - 'invalid value': {actions: emit(({event}) => event)}, - 'error': {actions: emit(({event}) => event)}, - 'selection': { + 'notify.blurred': { + actions: emit(({event}) => ({...event, type: 'blurred'})), + }, + 'notify.done loading': {actions: emit({type: 'done loading'})}, + 'notify.error': {actions: emit(({event}) => ({...event, type: 'error'}))}, + 'notify.invalid value': { + actions: emit(({event}) => ({...event, type: 'invalid value'})), + }, + 'notify.focused': { + actions: emit(({event}) => ({...event, type: 'focused'})), + }, + 'notify.selection': { actions: [ assign({selection: ({event}) => event.selection}), - emit(({event}) => event), + emit(({event}) => ({...event, type: 'selection'})), ], }, - 'blurred': {actions: emit(({event}) => event)}, - 'focused': {actions: emit(({event}) => event)}, - 'loading': {actions: emit({type: 'loading'})}, + 'notify.unset': {actions: emit(({event}) => ({...event, type: 'unset'}))}, + 'notify.loading': {actions: emit({type: 'loading'})}, + 'notify.value changed': { + actions: emit(({event}) => ({...event, type: 'value changed'})), + }, + + 'add behavior': {actions: 'add behavior to context'}, + 'remove behavior': {actions: 'remove behavior from context'}, 'patches': {actions: emit(({event}) => event)}, - 'done loading': {actions: emit({type: 'done loading'})}, 'update behaviors': {actions: 'assign behaviors'}, 'update key generator': { actions: assign({keyGenerator: ({event}) => event.keyGenerator}), diff --git a/packages/editor/src/editor/plugins/create-with-event-listeners.ts b/packages/editor/src/editor/plugins/create-with-event-listeners.ts index a8e86323f..969b5cf59 100644 --- a/packages/editor/src/editor/plugins/create-with-event-listeners.ts +++ b/packages/editor/src/editor/plugins/create-with-event-listeners.ts @@ -17,6 +17,24 @@ export function createWithEventListeners( subscriptions.push(() => { const subscription = editorActor.on('*', (event) => { switch (event.type) { + // These events are not relevant for Behaviors + case 'blurred': + case 'done loading': + case 'editable': + case 'error': + case 'focused': + case 'invalid value': + case 'loading': + case 'mutation': + case 'patch': + case 'patches': + case 'read only': + case 'ready': + case 'selection': + case 'value changed': + case 'unset': + break + case 'custom.*': editorActor.send({ type: 'custom behavior event', @@ -25,44 +43,7 @@ export function createWithEventListeners( }) break - case 'annotation.add': - case 'annotation.remove': - case 'annotation.toggle': - case 'block.set': - case 'block.unset': - case 'blur': - case 'data transfer.set': - case 'decorator.add': - case 'decorator.remove': - case 'decorator.toggle': - case 'delete.backward': - case 'delete.block': - case 'delete.forward': - case 'delete.text': - case 'deserialization.failure': - case 'deserialization.success': - case 'focus': - case 'insert.block': - case 'insert.block object': - case 'insert.inline object': - case 'insert.span': - case 'insert.text block': - case 'list item.add': - case 'list item.remove': - case 'list item.toggle': - case 'move.block': - case 'move.block down': - case 'move.block up': - case 'select': - case 'select.next block': - case 'select.previous block': - case 'serialization.failure': - case 'serialization.success': - case 'style.add': - case 'style.remove': - case 'style.toggle': - case 'text block.set': - case 'text block.unset': + default: editorActor.send({ type: 'behavior event', behaviorEvent: event, diff --git a/packages/editor/src/editor/plugins/createWithPatches.ts b/packages/editor/src/editor/plugins/createWithPatches.ts index a1f8cdafb..08f7f7c53 100644 --- a/packages/editor/src/editor/plugins/createWithPatches.ts +++ b/packages/editor/src/editor/plugins/createWithPatches.ts @@ -271,7 +271,7 @@ export function createWithPatches({ ) { patches = [...patches, unset([])] editorActor.send({ - type: 'unset', + type: 'notify.unset', previousValue: fromSlateValue( previousChildren, schemaTypes.block.name, diff --git a/packages/editor/src/editor/plugins/createWithPortableTextSelections.ts b/packages/editor/src/editor/plugins/createWithPortableTextSelections.ts index 542f0c763..9243352da 100644 --- a/packages/editor/src/editor/plugins/createWithPortableTextSelections.ts +++ b/packages/editor/src/editor/plugins/createWithPortableTextSelections.ts @@ -45,9 +45,9 @@ export function createWithPortableTextSelections( ) } if (ptRange) { - editorActor.send({type: 'selection', selection: ptRange}) + editorActor.send({type: 'notify.selection', selection: ptRange}) } else { - editorActor.send({type: 'selection', selection: null}) + editorActor.send({type: 'notify.selection', selection: null}) } } prevSelection = editor.selection diff --git a/packages/editor/src/index.ts b/packages/editor/src/index.ts index 609feb8ee..573044af7 100644 --- a/packages/editor/src/index.ts +++ b/packages/editor/src/index.ts @@ -26,10 +26,10 @@ export type {EditorSchema} from './editor/define-schema' export {PortableTextEditable} from './editor/Editable' export type {PortableTextEditableProps} from './editor/Editable' export {EditorEventListener} from './editor/editor-event-listener' -export { - type EditorEmittedEvent, - type MutationEvent, - type PatchesEvent, +export type { + EditorEmittedEvent, + MutationEvent, + PatchesEvent, } from './editor/editor-machine' export { EditorProvider, diff --git a/packages/editor/src/type-utils.ts b/packages/editor/src/type-utils.ts index 009acee41..db3201840 100644 --- a/packages/editor/src/type-utils.ts +++ b/packages/editor/src/type-utils.ts @@ -15,3 +15,13 @@ export type OmitFromUnion< TTagKey extends keyof TUnion, TOmittedTags extends TUnion[TTagKey], > = TUnion extends Record ? never : TUnion + +export type NamespaceEvent = TEvent extends { + type: infer TEventType +} + ? { + [K in keyof TEvent]: K extends 'type' + ? `${TNamespace}.${TEventType & string}` + : TEvent[K] + } + : never