@portabletext/editor
Version:
Portable Text Editor made in React
286 lines (254 loc) • 6.82 kB
text/typescript
import {isHotkey} from '../internal-utils/is-hotkey'
import {isTextBlock} from '../internal-utils/parse-blocks'
import * as selectors from '../selectors'
import {isEmptyTextBlock} from '../utils/util.is-empty-text-block'
import {raise} from './behavior.types.action'
import {defineBehavior} from './behavior.types.behavior'
const arrowDownOnLonelyBlockObject = defineBehavior({
on: 'keyboard.keydown',
guard: ({snapshot, event}) => {
const isArrowDown = isHotkey('ArrowDown', event.originEvent)
if (!isArrowDown) {
return false
}
const collapsedSelection = selectors.isSelectionCollapsed(snapshot)
if (!collapsedSelection) {
return false
}
const focusBlockObject = selectors.getFocusBlockObject(snapshot)
const nextBlock = selectors.getNextBlock(snapshot)
return focusBlockObject && !nextBlock
},
actions: [
({snapshot}) => [
raise({
type: 'insert.block',
block: {
_type: snapshot.context.schema.block.name,
},
placement: 'after',
}),
],
],
})
const arrowUpOnLonelyBlockObject = defineBehavior({
on: 'keyboard.keydown',
guard: ({snapshot, event}) => {
const isArrowUp = isHotkey('ArrowUp', event.originEvent)
if (!isArrowUp) {
return false
}
const collapsedSelection = selectors.isSelectionCollapsed(snapshot)
if (!collapsedSelection) {
return false
}
const focusBlockObject = selectors.getFocusBlockObject(snapshot)
const previousBlock = selectors.getPreviousBlock(snapshot)
return focusBlockObject && !previousBlock
},
actions: [
({snapshot}) => [
raise({
type: 'insert.block',
block: {
_type: snapshot.context.schema.block.name,
},
placement: 'before',
}),
],
],
})
const breakingBlockObject = defineBehavior({
on: 'insert.break',
guard: ({snapshot}) => {
const focusBlockObject = selectors.getFocusBlockObject(snapshot)
const collapsedSelection = selectors.isSelectionCollapsed(snapshot)
return collapsedSelection && focusBlockObject !== undefined
},
actions: [
({snapshot}) => [
raise({
type: 'insert.block',
block: {
_type: snapshot.context.schema.block.name,
},
placement: 'after',
}),
],
],
})
const clickingAboveLonelyBlockObject = defineBehavior({
on: 'mouse.click',
guard: ({snapshot, event}) => {
if (snapshot.context.readOnly) {
return false
}
if (
snapshot.context.selection &&
!selectors.isSelectionCollapsed(snapshot)
) {
return false
}
const focusBlockObject = selectors.getFocusBlockObject({
...snapshot,
context: {
...snapshot.context,
selection: event.position.selection,
},
})
const previousBlock = selectors.getPreviousBlock({
...snapshot,
context: {
...snapshot.context,
selection: event.position.selection,
},
})
return (
event.position.isEditor &&
event.position.block === 'start' &&
focusBlockObject &&
!previousBlock
)
},
actions: [
({snapshot, event}) => [
raise({
type: 'select',
at: event.position.selection,
}),
raise({
type: 'insert.block',
block: {
_type: snapshot.context.schema.block.name,
},
placement: 'before',
select: 'start',
}),
],
],
})
const clickingBelowLonelyBlockObject = defineBehavior({
on: 'mouse.click',
guard: ({snapshot, event}) => {
if (snapshot.context.readOnly) {
return false
}
if (
snapshot.context.selection &&
!selectors.isSelectionCollapsed(snapshot)
) {
return false
}
const focusBlockObject = selectors.getFocusBlockObject({
...snapshot,
context: {
...snapshot.context,
selection: event.position.selection,
},
})
const nextBlock = selectors.getNextBlock({
...snapshot,
context: {
...snapshot.context,
selection: event.position.selection,
},
})
return (
event.position.isEditor &&
event.position.block === 'end' &&
focusBlockObject &&
!nextBlock
)
},
actions: [
({snapshot, event}) => [
raise({
type: 'select',
at: event.position.selection,
}),
raise({
type: 'insert.block',
block: {
_type: snapshot.context.schema.block.name,
},
placement: 'after',
select: 'start',
}),
],
],
})
const deletingEmptyTextBlockAfterBlockObject = defineBehavior({
on: 'delete.backward',
guard: ({snapshot}) => {
const focusTextBlock = selectors.getFocusTextBlock(snapshot)
const selectionCollapsed = selectors.isSelectionCollapsed(snapshot)
const previousBlock = selectors.getPreviousBlock(snapshot)
if (!focusTextBlock || !selectionCollapsed || !previousBlock) {
return false
}
if (
isEmptyTextBlock(snapshot.context, focusTextBlock.node) &&
!isTextBlock(snapshot.context, previousBlock.node)
) {
return {focusTextBlock, previousBlock}
}
return false
},
actions: [
(_, {focusTextBlock, previousBlock}) => [
raise({
type: 'delete.block',
at: focusTextBlock.path,
}),
raise({
type: 'select',
at: {
anchor: {path: previousBlock.path, offset: 0},
focus: {path: previousBlock.path, offset: 0},
},
}),
],
],
})
const deletingEmptyTextBlockBeforeBlockObject = defineBehavior({
on: 'delete.forward',
guard: ({snapshot}) => {
const focusTextBlock = selectors.getFocusTextBlock(snapshot)
const selectionCollapsed = selectors.isSelectionCollapsed(snapshot)
const nextBlock = selectors.getNextBlock(snapshot)
if (!focusTextBlock || !selectionCollapsed || !nextBlock) {
return false
}
if (
isEmptyTextBlock(snapshot.context, focusTextBlock.node) &&
!isTextBlock(snapshot.context, nextBlock.node)
) {
return {focusTextBlock, nextBlock}
}
return false
},
actions: [
(_, {focusTextBlock, nextBlock}) => [
raise({
type: 'delete.block',
at: focusTextBlock.path,
}),
raise({
type: 'select',
at: {
anchor: {path: nextBlock.path, offset: 0},
focus: {path: nextBlock.path, offset: 0},
},
}),
],
],
})
export const coreBlockObjectBehaviors = {
arrowDownOnLonelyBlockObject,
arrowUpOnLonelyBlockObject,
breakingBlockObject,
clickingAboveLonelyBlockObject,
clickingBelowLonelyBlockObject,
deletingEmptyTextBlockAfterBlockObject,
deletingEmptyTextBlockBeforeBlockObject,
}