@portabletext/editor
Version:
Portable Text Editor made in React
216 lines (199 loc) • 5.58 kB
text/typescript
import type {
ArrayDefinition,
ArraySchemaType,
PortableTextBlock,
} from '@sanity/types'
import type {ActorRef, EventObject, Snapshot} from 'xstate'
import type {Behavior} from '../behaviors/behavior.types.behavior'
import type {ExternalBehaviorEvent} from '../behaviors/behavior.types.event'
import {createCoreConverters} from '../converters/converters.core'
import {compileType} from '../internal-utils/schema'
import type {EditableAPI} from '../types/editor'
import {createSlateEditor, type SlateEditor} from './create-slate-editor'
import type {
EditorActor,
EditorEmittedEvent,
ExternalEditorEvent,
} from './editor-machine'
import {
compileSchemaDefinitionToLegacySchema,
legacySchemaToEditorSchema,
type SchemaDefinition,
} from './editor-schema'
import {getEditorSnapshot} from './editor-selector'
import type {EditorSnapshot} from './editor-snapshot'
import {defaultKeyGenerator} from './key-generator'
import {createLegacySchema} from './legacy-schema'
import {createEditableAPI} from './plugins/createWithEditableAPI'
/**
* @public
*/
export type EditorConfig = {
/**
* @beta
*/
behaviors?: Array<Behavior>
keyGenerator?: () => string
/**
* @deprecated Will be removed in the next major version
*/
maxBlocks?: number
readOnly?: boolean
initialValue?: Array<PortableTextBlock>
} & (
| {
schemaDefinition: SchemaDefinition
schema?: undefined
}
| {
schemaDefinition?: undefined
schema: ArraySchemaType<PortableTextBlock> | ArrayDefinition
}
)
/**
* @public
*/
export type EditorEvent = ExternalEditorEvent | ExternalBehaviorEvent
/**
* @public
*/
export type Editor = {
getSnapshot: () => EditorSnapshot
/**
* @beta
*/
registerBehavior: (config: {behavior: Behavior}) => () => void
send: (event: EditorEvent) => void
on: ActorRef<Snapshot<unknown>, EventObject, EditorEmittedEvent>['on']
}
export type InternalEditor = Editor & {
_internal: {
editable: EditableAPI
editorActor: EditorActor
slateEditor: SlateEditor
}
}
function compileSchemasFromEditorConfig(config: EditorConfig) {
const legacySchema = config.schemaDefinition
? compileSchemaDefinitionToLegacySchema(config.schemaDefinition)
: createLegacySchema(
config.schema.hasOwnProperty('jsonType')
? config.schema
: compileType(config.schema),
)
const schema = legacySchemaToEditorSchema(legacySchema)
return {
legacySchema,
schema,
}
}
export function editorConfigToMachineInput(config: EditorConfig) {
const {legacySchema, schema} = compileSchemasFromEditorConfig(config)
return {
behaviors: config.behaviors,
converters: createCoreConverters(legacySchema),
getLegacySchema: () => legacySchema,
keyGenerator: config.keyGenerator ?? defaultKeyGenerator,
maxBlocks: config.maxBlocks,
readOnly: config.readOnly,
schema,
initialValue: config.initialValue,
} as const
}
export function createInternalEditor(editorActor: EditorActor): InternalEditor {
const slateEditor = createSlateEditor({editorActor})
const editable = createEditableAPI(slateEditor.instance, editorActor)
return {
getSnapshot: () =>
getEditorSnapshot({
editorActorSnapshot: editorActor.getSnapshot(),
slateEditorInstance: slateEditor.instance,
}),
registerBehavior: (config) => {
editorActor.send({
type: 'add behavior',
behavior: config.behavior,
})
return () => {
editorActor.send({
type: 'remove behavior',
behavior: config.behavior,
})
}
},
send: (event) => {
switch (event.type) {
case 'add behavior':
case 'remove behavior':
case 'update behaviors':
case 'update key generator':
case 'update readOnly':
case 'patches':
case 'update value':
case 'update schema':
case 'update maxBlocks':
editorActor.send(event)
break
case 'blur':
editorActor.send({
type: 'blur',
editor: slateEditor.instance,
})
break
case 'focus':
editorActor.send({
type: 'focus',
editor: slateEditor.instance,
})
break
case 'insert.block object':
editorActor.send({
type: 'behavior event',
behaviorEvent: {
type: 'insert.block',
block: {
_type: event.blockObject.name,
...(event.blockObject.value ?? {}),
},
placement: event.placement,
},
editor: slateEditor.instance,
})
break
default:
editorActor.send({
type: 'behavior event',
behaviorEvent: event,
editor: slateEditor.instance,
})
}
},
on: (event, listener) => {
const subscription = editorActor.on(event, (event) => {
switch (event.type) {
case 'blurred':
case 'done loading':
case 'editable':
case 'error':
case 'focused':
case 'invalid value':
case 'loading':
case 'mutation':
case 'patch':
case 'read only':
case 'ready':
case 'selection':
case 'value changed':
listener(event)
break
}
})
return subscription
},
_internal: {
editable,
editorActor,
slateEditor,
},
}
}