Skip to content
This page is available as Markdown at /editor/concepts/behavior.md. For the full documentation index, see /llms.txt, or the complete corpus at /llms-full.txt.

Behaviors

Behaviors allow you to customize how users interact with the editor by hooking into events during the editing experience.

All behaviors follow this process:

  1. Listen for an event.
  2. Use a guard to decide if they should run or not.
  3. Trigger a set of actions to perform on the editor.

This pattern is influenced by Statecharts.

Behaviors are defined with the defineBehavior helper. Here’s an example event from the behavior guide:

defineBehavior({
on: 'insert.text',
guard: ({snapshot, event}) => event.text === 'a',
actions: [
({snapshot, event}) => [
{type: 'execute', event: {type: 'insert.text', text: 'A'}},
],
],
})

Revisiting the three step process above:

  • on listens for the event.
  • guard handles the conditional.
  • actions sends, or invokes, the desired actions.

Whenever you enter text into the editor, activate a toolbar button, or anything else happens in the editor it sends an event.

There are three categories of Behavior Events:

  • Native Events: Events that come from the browser or device directly.
  • Synthetic Events: Editor-specific events that directly modify the editor state.
  • Custom Events: Events that you create yourself.

The Behavior API uses events to trigger actions by listening for a specific event.

A guard is a condition that helps the behavior determine if it should perform the actions.

The guard key expects a response or false. The example above shows a simple truthy guard. Here it is again:

guard: ({snapshot, event}) => event.text === 'a'

Guards can also return parameters that you can access when firing an action.

guard: ({snapshot, event}) => {
if (event.text === 'a') {
return {
secret: 'secret text'
}
}
return false
},
actions: [
({snapshot, event}, {secret}) => [
/* ... */
]
]

Passing parameters allows you to reuse conditional behavior, such as selecting part of a string, without rewriting the logic.

Guards are optional. This means it’s possible to create an unconditional behavior that always runs when an event occurs. The “soft return” behavior build into the PTE is one example:

const softReturn = defineBehavior({
on: 'insert.soft break',
actions: [() => [execute({type: 'insert.text', text: '\n'})]],
})

This behavior listens for soft break events and uses the insert.text action to insert a \n instead to prevent splitting text blocks with Shift+Enter. These unconditional behaviors are rare and you’ll mostly encounter conditional behaviors.

Actions make things happen in the editor. This is where you change the standard behavior by modifying actions before they occur or by circumventing them completely. You’ve seen actions in some of the guard examples above.

The actions key expects an array of behavior action sets. As with guards, you have access to the event and snapshot.

So far we’ve only seen examples that invoke a single action, but you can send multiple actions or sets of actions from a single event.

To send an event back into the editor, use the raise action type or the raise helper. This is useful for chaining behaviors and default events.

// Approach A: With the type set to 'raise'
// Approach B: With the raise helper
import {defineBehavior, raise} from '@portabletext/editor/behaviors'
const raisedUppercaseA = defineBehavior({
on: 'insert.text',
guard: ({snapshot, event}) => event.text === 'a',
actions: [
({snapshot, event}) => [
{type: 'raise', event: {type: 'insert.text', text: 'A'}},
],
],
})
const raisedUppercaseA = defineBehavior({
on: 'insert.text',
guard: ({snapshot, event}) => event.text === 'a',
actions: [({snapshot, event}) => [raise({type: 'insert.text', text: 'A'})]],
})

When a Behavior’s guard matches an event that carries a native event (a keyboard.keydown, for example), the Behavior claims it: the editor calls preventDefault() and the browser’s default action is suppressed. The action sets can revise that claim. Every action set that returns actions casts a vote, and the last vote wins:

The action set Native default
contains raise/execute prevented
contains only forward passes down the chain
contains only effects prevented
returns nothing, or throws keeps the previous decision

Within one set, raise and execute outrank forward: a set that both mutates and forwards still prevents the default.

forward does not mean “let the browser act”. It means “no opinion at this level”: the event moves on to the next Behavior in the chain, which makes its own decision, and the browser’s default only survives when no Behavior along the chain claims the event.

Multiple action sets are also an undo boundary: each set after the first starts a new undo step. @portabletext/plugin-input-rule uses this to let the typed text land as its own undo step before a second set transforms it, so undo peels the transformation off first.

Selectors are pure functions that derive state from the editor snapshot (snapshot in the examples). A collection of selectors is included with the core library.

// import all selectors
import * as selectors from '@portabletext/editor/selectors'
// or individual ones
import {getFocusSpan, getFocusTextBlock} from '@portabletext/editor/selectors'

The core selectors are useful helper functions for checking conditions of the editor, finding selected text, and more. You can manually do anything a selector can do by parsing the editor snapshot.

Browse the existing behaviors, or check out the Behavior Recipes documentation for examples of real-world behaviors.