@portabletext/editor
Version:
Portable Text Editor made in React
239 lines (223 loc) • 5.8 kB
text/typescript
import {useActorRef} from '@xstate/react'
import {isEqual} from 'lodash'
import {
assign,
fromCallback,
setup,
type AnyEventObject,
type CallbackLogicFunction,
} from 'xstate'
import {createDecoratorPairBehavior} from '../behaviors/behavior.decorator-pair'
import {effect, execute, forward} from '../behaviors/behavior.types.action'
import {defineBehavior} from '../behaviors/behavior.types.behavior'
import type {Editor} from '../editor'
import type {EditorSchema} from '../editor/editor-schema'
import {useEditor} from '../editor/use-editor'
import type {BlockOffset} from '../types/block-offset'
import * as utils from '../utils'
/**
* @beta
* @deprecated Install the plugin from `@portabletext/plugin-character-pair-decorator`
*/
export function DecoratorShortcutPlugin(config: {
decorator: ({schema}: {schema: EditorSchema}) => string | undefined
pair: {char: string; amount: number}
}) {
const editor = useEditor()
useActorRef(decoratorPairMachine, {
input: {
editor,
decorator: config.decorator,
pair: config.pair,
},
})
return null
}
type MarkdownEmphasisEvent =
| {
type: 'emphasis.add'
blockOffset: BlockOffset
}
| {
type: 'selection'
blockOffsets?: {
anchor: BlockOffset
focus: BlockOffset
}
}
| {
type: 'delete.backward'
}
const emphasisListener: CallbackLogicFunction<
AnyEventObject,
MarkdownEmphasisEvent,
{
decorator: ({schema}: {schema: EditorSchema}) => string | undefined
editor: Editor
pair: {char: string; amount: number}
}
> = ({sendBack, input}) => {
const unregister = input.editor.registerBehavior({
behavior: createDecoratorPairBehavior({
decorator: input.decorator,
pair: input.pair,
onDecorate: (offset) => {
sendBack({type: 'emphasis.add', blockOffset: offset})
},
}),
})
return unregister
}
const selectionListenerCallback: CallbackLogicFunction<
AnyEventObject,
MarkdownEmphasisEvent,
{editor: Editor}
> = ({sendBack, input}) => {
const unregister = input.editor.registerBehavior({
behavior: defineBehavior({
on: 'select',
guard: ({snapshot, event}) => {
if (!event.at) {
return {blockOffsets: undefined}
}
const anchor = utils.spanSelectionPointToBlockOffset({
context: snapshot.context,
selectionPoint: event.at.anchor,
})
const focus = utils.spanSelectionPointToBlockOffset({
context: snapshot.context,
selectionPoint: event.at.focus,
})
if (!anchor || !focus) {
return {blockOffsets: undefined}
}
return {
blockOffsets: {
anchor,
focus,
},
}
},
actions: [
({event}, {blockOffsets}) => [
{
type: 'effect',
effect: () => {
sendBack({type: 'selection', blockOffsets})
},
},
forward(event),
],
],
}),
})
return unregister
}
const deleteBackwardListenerCallback: CallbackLogicFunction<
AnyEventObject,
MarkdownEmphasisEvent,
{editor: Editor}
> = ({sendBack, input}) => {
const unregister = input.editor.registerBehavior({
behavior: defineBehavior({
on: 'delete.backward',
actions: [
() => [
execute({
type: 'history.undo',
}),
effect(() => {
sendBack({type: 'delete.backward'})
}),
],
],
}),
})
return unregister
}
const decoratorPairMachine = setup({
types: {
context: {} as {
decorator: ({schema}: {schema: EditorSchema}) => string | undefined
editor: Editor
offsetAfterEmphasis?: BlockOffset
pair: {char: string; amount: number}
},
input: {} as {
decorator: ({schema}: {schema: EditorSchema}) => string | undefined
editor: Editor
pair: {char: string; amount: number}
},
events: {} as MarkdownEmphasisEvent,
},
actors: {
'emphasis listener': fromCallback(emphasisListener),
'delete.backward listener': fromCallback(deleteBackwardListenerCallback),
'selection listener': fromCallback(selectionListenerCallback),
},
}).createMachine({
id: 'decorator pair',
context: ({input}) => ({
decorator: input.decorator,
editor: input.editor,
pair: input.pair,
}),
initial: 'idle',
states: {
'idle': {
invoke: [
{
src: 'emphasis listener',
input: ({context}) => ({
decorator: context.decorator,
editor: context.editor,
pair: context.pair,
}),
},
],
on: {
'emphasis.add': {
target: 'emphasis added',
actions: assign({
offsetAfterEmphasis: ({event}) => event.blockOffset,
}),
},
},
},
'emphasis added': {
exit: [
assign({
offsetAfterEmphasis: undefined,
}),
],
invoke: [
{
src: 'selection listener',
input: ({context}) => ({editor: context.editor}),
},
{
src: 'delete.backward listener',
input: ({context}) => ({editor: context.editor}),
},
],
on: {
'selection': {
target: 'idle',
guard: ({context, event}) => {
const selectionChanged = !isEqual(
{
anchor: context.offsetAfterEmphasis,
focus: context.offsetAfterEmphasis,
},
event.blockOffsets,
)
return selectionChanged
},
},
'delete.backward': {
target: 'idle',
},
},
},
},
})