@portabletext/editor
Version:
Portable Text Editor made in React
172 lines (149 loc) • 4.29 kB
text/typescript
import type {PortableTextObject, PortableTextSpan} from '@sanity/types'
import type {Path, Range} from 'slate'
import type {EditorContext, EditorSnapshot} from '../editor/editor-snapshot'
import {
getBlockKeyFromSelectionPoint,
getChildKeyFromSelectionPoint,
} from '../selection/selection-point'
import type {EditorSelectionPoint} from '../types/editor'
import {isEqualSelectionPoints} from '../utils'
import {blockOffsetToSpanSelectionPoint} from '../utils/util.block-offset'
import {isSpan, isTextBlock} from './parse-blocks'
export function toSlateRange(
snapshot: {
context: Pick<EditorContext, 'schema' | 'value' | 'selection'>
} & Pick<EditorSnapshot, 'blockIndexMap'>,
): Range | null {
if (!snapshot.context.selection) {
return null
}
if (
isEqualSelectionPoints(
snapshot.context.selection.anchor,
snapshot.context.selection.focus,
)
) {
const anchorPoint = toSlateSelectionPoint(
snapshot,
snapshot.context.selection.anchor,
snapshot.context.selection.backward ? 'forward' : 'backward',
)
if (!anchorPoint) {
return null
}
return {
anchor: anchorPoint,
focus: anchorPoint,
}
}
const anchorPoint = toSlateSelectionPoint(
snapshot,
snapshot.context.selection.anchor,
snapshot.context.selection.backward ? 'forward' : 'backward',
)
const focusPoint = toSlateSelectionPoint(
snapshot,
snapshot.context.selection.focus,
snapshot.context.selection.backward ? 'backward' : 'forward',
)
if (!anchorPoint || !focusPoint) {
return null
}
return {
anchor: anchorPoint,
focus: focusPoint,
}
}
function toSlateSelectionPoint(
snapshot: {
context: Pick<EditorContext, 'schema' | 'value'>
} & Pick<EditorSnapshot, 'blockIndexMap'>,
selectionPoint: EditorSelectionPoint,
direction: 'forward' | 'backward',
):
| {
path: Path
offset: number
}
| undefined {
const blockKey = getBlockKeyFromSelectionPoint(selectionPoint)
if (!blockKey) {
return undefined
}
const blockIndex = snapshot.blockIndexMap.get(blockKey)
if (blockIndex === undefined) {
return undefined
}
const block = snapshot.context.value.at(blockIndex)
if (!block) {
return undefined
}
if (!isTextBlock(snapshot.context, block)) {
return {
path: [blockIndex, 0],
offset: 0,
}
}
let childKey = getChildKeyFromSelectionPoint({
path: selectionPoint.path,
offset: 0,
})
// If the block is a text block, but there is no child key in the selection
// point path, then we can try to find a span selection point by the offset.
const spanSelectionPoint = !childKey
? blockOffsetToSpanSelectionPoint({
context: {
schema: snapshot.context.schema,
value: [block],
},
blockOffset: {
path: [{_key: blockKey}],
offset: selectionPoint.offset,
},
direction,
})
: undefined
childKey = spanSelectionPoint
? getChildKeyFromSelectionPoint(spanSelectionPoint)
: childKey
// If we still don't have a child key, then we have to resort to selecting
// the first child of the block (which by Slate convention is a span).
if (!childKey) {
return {
path: [blockIndex, 0],
offset: 0,
}
}
let offset = spanSelectionPoint?.offset ?? selectionPoint.offset
let childPath: Array<number> = []
let childIndex = -1
let pathChild: PortableTextSpan | PortableTextObject | undefined = undefined
for (const child of block.children) {
childIndex++
if (child._key === childKey) {
pathChild = child
if (isSpan(snapshot.context, child)) {
childPath = [childIndex]
} else {
childPath = [childIndex, 0]
offset = 0
}
break
}
}
// If we for some unforeseen reason didn't manage to produce a child path,
// then we have to resort to selecting the first child of the block (which
// by Slate convention is a span).
if (childPath.length === 0) {
return {
path: [blockIndex, 0],
offset: 0,
}
}
return {
path: [blockIndex].concat(childPath),
offset: isSpan(snapshot.context, pathChild)
? Math.min(pathChild.text.length, offset)
: offset,
}
}