diff --git a/package.json b/package.json index 05072366c..7f9a2a2a5 100644 --- a/package.json +++ b/package.json @@ -91,6 +91,10 @@ "@types/react": "$@types/react", "@types/react-dom": "$@types/react-dom", "@types/react-is": "$@types/react-is" - } + }, + "ignoredBuiltDependencies": [ + "esbuild", + "sharp" + ] } } diff --git a/packages/editor/src/internal-utils/apply-merge-node.ts b/packages/editor/src/internal-utils/apply-merge-node.ts index 0499430d9..fb837128e 100644 --- a/packages/editor/src/internal-utils/apply-merge-node.ts +++ b/packages/editor/src/internal-utils/apply-merge-node.ts @@ -3,28 +3,69 @@ import { Element, Node as NodeUtils, Path, - PathRef, - Point, - PointRef, Range, - RangeRef, Text, type Node, - type Operation, + type Point, } from '../slate' import type {PortableTextSlateEditor} from '../types/slate-editor' +import {rangeRefAffinities} from './range-ref-affinities' + +/** + * Transform a path for a merge_node operation. + * + * When a node at `mergePath` is merged into its previous sibling: + * - The merged node disappears, so paths at or after it shift back by 1 + * - Children of the merged node move into the previous sibling at `position` + */ +function transformPathForMerge( + path: Path, + mergePath: Path, + position: number, +): Path | null { + const p = [...path] + + if (Path.equals(mergePath, p) || Path.endsBefore(mergePath, p)) { + p[mergePath.length - 1] = p[mergePath.length - 1]! - 1 + } else if (Path.isAncestor(mergePath, p)) { + p[mergePath.length - 1] = p[mergePath.length - 1]! - 1 + p[mergePath.length] = p[mergePath.length]! + position + } + + return p +} + +/** + * Transform a point for a merge_node operation. + * + * If the point is inside the merged node, its offset shifts by `position` + * (the number of children/characters already in the merge target). + */ +function transformPointForMerge( + point: Point, + mergePath: Path, + position: number, + _affinity?: 'forward' | 'backward' | null, +): Point | null { + let {path, offset} = point + + if (Path.equals(mergePath, path)) { + offset += position + } + + path = transformPathForMerge(path, mergePath, position)! + + return {path, offset} +} /** * Merge a node at the given path into its previous sibling using only * patch-compliant operations (insert_text/insert_node + remove_node). * - * This replaces direct `editor.apply({type: 'merge_node', ...})` calls - * to eliminate merge_node from the operation vocabulary. - * - * Because merge_node has specific ref-transform semantics that can't be - * replicated by the incremental transforms of the decomposed operations, - * we pre-transform all active refs with the equivalent merge_node operation - * and then suppress ref transforms for the individual low-level operations. + * Because the decomposed operations would produce different ref-transform + * semantics than a single merge, we pre-transform all active refs with + * the merge semantics directly and suppress ref transforms for the + * individual low-level operations. * * For text nodes: appends the text to the previous sibling, then removes the node. * For element nodes: moves children into the previous sibling, then removes the node. @@ -33,40 +74,46 @@ export function applyMergeNode( editor: PortableTextSlateEditor, path: Path, position: number, - properties: Record, + _properties: Record, ): void { const node = NodeUtils.get(editor, path, editor.schema) const prevPath = Path.previous(path) - // Build the equivalent merge_node operation for ref transforms - const mergeOp: Operation = { - type: 'merge_node' as const, - path, - position, - properties, - } - - // Pre-transform all refs as if a merge_node happened + // Pre-transform all refs with merge semantics for (const ref of Editor.pathRefs(editor)) { - PathRef.transform(ref, mergeOp) + const current = ref.current + if (current) { + ref.current = transformPathForMerge(current, path, position) + } } for (const ref of Editor.pointRefs(editor)) { - PointRef.transform(ref, mergeOp) + const current = ref.current + if (current) { + ref.current = transformPointForMerge(current, path, position) + } } for (const ref of Editor.rangeRefs(editor)) { - RangeRef.transform(ref, mergeOp) + const current = ref.current + if (current) { + const [anchorAffinity, focusAffinity] = rangeRefAffinities(current, ref.affinity) + const anchor = transformPointForMerge(current.anchor, path, position, anchorAffinity) + const focus = transformPointForMerge(current.focus, path, position, focusAffinity) + if (anchor && focus) { + ref.current = {anchor, focus} + } else { + ref.current = null + ref.unref() + } + } } - // Pre-transform editor.selection as if a merge_node happened + // Pre-transform editor.selection if (editor.selection) { - const sel = {...editor.selection} - for (const [point, key] of Range.points(sel)) { - const result = Point.transform(point, mergeOp) - if (result) { - sel[key] = result - } + const anchor = transformPointForMerge(editor.selection.anchor, path, position) + const focus = transformPointForMerge(editor.selection.focus, path, position) + if (anchor && focus) { + editor.selection = {anchor, focus} } - editor.selection = sel } // Temporarily remove all refs so the decomposed operations don't @@ -81,9 +128,8 @@ export function applyMergeNode( // Save the pre-transformed selection const savedSelection = editor.selection - // Pre-transform DOM-layer pending state (pendingDiffs, pendingSelection, - // pendingAction) with the merge_node operation, then suppress transforms - // during the decomposed operations by temporarily clearing them. + // Pre-transform DOM-layer pending state with merge semantics, then + // suppress transforms during the decomposed operations by clearing them. // These properties are added by the DOM plugin at runtime. const editorAny = editor as unknown as Record const savedPendingDiffs = editorAny['pendingDiffs'] @@ -98,15 +144,7 @@ export function applyMergeNode( id: number path: Path }) => - transformTextDiffForMerge( - textDiff, - mergeOp as { - type: 'merge_node' - path: Path - position: number - properties: Record - }, - ), + transformTextDiffForMerge(textDiff, path, position), ) .filter(Boolean) } @@ -118,10 +156,8 @@ export function applyMergeNode( 'focus' in (savedPendingSelection as Record) ) { const sel = savedPendingSelection as Range - const anchor = Point.transform(sel.anchor, mergeOp, { - affinity: 'backward', - }) - const focus = Point.transform(sel.focus, mergeOp, {affinity: 'backward'}) + const anchor = transformPointForMerge(sel.anchor, path, position) + const focus = transformPointForMerge(sel.focus, path, position) editorAny['pendingSelection'] = anchor && focus ? {anchor, focus} : null } @@ -131,16 +167,12 @@ export function applyMergeNode( 'at' in (savedPendingAction as Record) ) { const action = savedPendingAction as {at: Point | Range} - if (Point.isPoint(action.at)) { - const at = Point.transform(action.at, mergeOp, {affinity: 'backward'}) + if ('offset' in action.at && typeof action.at.offset === 'number') { + const at = transformPointForMerge(action.at as Point, path, position) editorAny['pendingAction'] = at ? {...action, at} : null } else if (Range.isRange(action.at)) { - const anchor = Point.transform(action.at.anchor, mergeOp, { - affinity: 'backward', - }) - const focus = Point.transform(action.at.focus, mergeOp, { - affinity: 'backward', - }) + const anchor = transformPointForMerge(action.at.anchor, path, position) + const focus = transformPointForMerge(action.at.focus, path, position) editorAny['pendingAction'] = anchor && focus ? {...action, at: {anchor, focus}} : null } @@ -206,8 +238,7 @@ export function applyMergeNode( } /** - * Transform a text diff for a merge_node operation. - * This replicates the logic from slate-dom's transformTextDiff for merge_node. + * Transform a text diff for a merge operation. */ function transformTextDiffForMerge( textDiff: { @@ -215,12 +246,8 @@ function transformTextDiffForMerge( id: number path: Path }, - op: { - type: 'merge_node' - path: Path - position: number - properties: Record - }, + mergePath: Path, + position: number, ): { diff: {start: number; end: number; text: string} id: number @@ -228,8 +255,8 @@ function transformTextDiffForMerge( } | null { const {path, diff, id} = textDiff - if (!Path.equals(op.path, path)) { - const newPath = Path.transform(path, op) + if (!Path.equals(mergePath, path)) { + const newPath = transformPathForMerge(path, mergePath, position) if (!newPath) { return null } @@ -238,11 +265,11 @@ function transformTextDiffForMerge( return { diff: { - start: diff.start + op.position, - end: diff.end + op.position, + start: diff.start + position, + end: diff.end + position, text: diff.text, }, id, - path: Path.transform(path, op)!, + path: transformPathForMerge(path, mergePath, position)!, } } diff --git a/packages/editor/src/internal-utils/apply-move-node.ts b/packages/editor/src/internal-utils/apply-move-node.ts index e85aa9406..07d686aad 100644 --- a/packages/editor/src/internal-utils/apply-move-node.ts +++ b/packages/editor/src/internal-utils/apply-move-node.ts @@ -1,26 +1,93 @@ -import { - Editor, - Node, - Path, - PathRef, - Point, - PointRef, - Range, - RangeRef, -} from '../slate' +import {Editor, Node, Path} from '../slate' +import type {Point} from '../slate' import type {PortableTextSlateEditor} from '../types/slate-editor' +import {rangeRefAffinities} from './range-ref-affinities' + +/** + * Transform a path for a move_node operation. + * + * When a node at `fromPath` is moved to `toPath`: + * - Paths inside or equal to the moved node follow it to the new location + * - Paths at the old location shift to fill the gap + * - Paths at the new location shift to make room + * + * Note: `toPath` refers to the position in the tree AFTER the node at + * `fromPath` has been removed. + */ +function transformPathForMove( + path: Path, + fromPath: Path, + toPath: Path, +): Path | null { + const p = [...path] + + if (Path.equals(fromPath, toPath)) { + return p + } + + if (Path.isAncestor(fromPath, p) || Path.equals(fromPath, p)) { + const copy = toPath.slice() + + if (Path.endsBefore(fromPath, toPath) && fromPath.length < toPath.length) { + copy[fromPath.length - 1] = copy[fromPath.length - 1]! - 1 + } + + return copy.concat(p.slice(fromPath.length)) + } else if ( + Path.isSibling(fromPath, toPath) && + (Path.isAncestor(toPath, p) || Path.equals(toPath, p)) + ) { + if (Path.endsBefore(fromPath, p)) { + p[fromPath.length - 1] = p[fromPath.length - 1]! - 1 + } else { + p[fromPath.length - 1] = p[fromPath.length - 1]! + 1 + } + } else if ( + Path.endsBefore(toPath, p) || + Path.equals(toPath, p) || + Path.isAncestor(toPath, p) + ) { + if (Path.endsBefore(fromPath, p)) { + p[fromPath.length - 1] = p[fromPath.length - 1]! - 1 + } + + p[toPath.length - 1] = p[toPath.length - 1]! + 1 + } else if (Path.endsBefore(fromPath, p)) { + if (Path.equals(toPath, p)) { + p[toPath.length - 1] = p[toPath.length - 1]! + 1 + } + + p[fromPath.length - 1] = p[fromPath.length - 1]! - 1 + } + + return p +} + +/** + * Transform a point for a move_node operation. + * Move only affects paths, not offsets within text nodes. + */ +function transformPointForMove( + point: Point, + fromPath: Path, + toPath: Path, + _affinity?: 'forward' | 'backward' | null, +): Point | null { + const path = transformPathForMove(point.path, fromPath, toPath) + if (!path) { + return null + } + return {path, offset: point.offset} +} /** * Move a node from one path to another using only patch-compliant * operations (remove_node + insert_node). * - * This replaces direct `editor.apply({type: 'move_node', ...})` calls - * to eliminate move_node from the operation vocabulary. - * - * Because move_node has specific ref-transform semantics that can't be - * replicated by the incremental transforms of the decomposed operations, - * we pre-transform all active refs with the equivalent move_node operation - * and then suppress ref transforms for the individual low-level operations. + * Because the decomposed operations would produce different ref-transform + * semantics than a single move, we pre-transform all active refs with + * the move semantics directly and suppress ref transforms for the + * individual low-level operations. */ export function applyMoveNode( editor: PortableTextSlateEditor, @@ -39,31 +106,41 @@ export function applyMoveNode( const node = Node.get(editor, path, editor.schema) - // Build the equivalent move_node operation for ref transforms - const moveOp = { - type: 'move_node' as const, - path, - newPath, - } - - // Pre-transform all refs as if a move_node happened + // Pre-transform all refs with move semantics for (const ref of Editor.pathRefs(editor)) { - PathRef.transform(ref, moveOp) + const current = ref.current + if (current) { + ref.current = transformPathForMove(current, path, newPath) + } } for (const ref of Editor.pointRefs(editor)) { - PointRef.transform(ref, moveOp) + const current = ref.current + if (current) { + ref.current = transformPointForMove(current, path, newPath) + } } for (const ref of Editor.rangeRefs(editor)) { - RangeRef.transform(ref, moveOp) + const current = ref.current + if (current) { + const [anchorAffinity, focusAffinity] = rangeRefAffinities(current, ref.affinity) + const anchor = transformPointForMove(current.anchor, path, newPath, anchorAffinity) + const focus = transformPointForMove(current.focus, path, newPath, focusAffinity) + if (anchor && focus) { + ref.current = {anchor, focus} + } else { + ref.current = null + ref.unref() + } + } } - // Pre-transform editor.selection as if a move_node happened + // Pre-transform editor.selection if (editor.selection) { - const sel = {...editor.selection} - for (const [point, key] of Range.points(sel)) { - sel[key] = Point.transform(point, moveOp)! + const anchor = transformPointForMove(editor.selection.anchor, path, newPath) + const focus = transformPointForMove(editor.selection.focus, path, newPath) + if (anchor && focus) { + editor.selection = {anchor, focus} } - editor.selection = sel } // Temporarily remove all refs so the decomposed operations don't diff --git a/packages/editor/src/internal-utils/apply-split-node.ts b/packages/editor/src/internal-utils/apply-split-node.ts index 1211f37e5..729caa74a 100644 --- a/packages/editor/src/internal-utils/apply-split-node.ts +++ b/packages/editor/src/internal-utils/apply-split-node.ts @@ -3,27 +3,86 @@ import { Element, Node as NodeUtils, Path, - PathRef, - Point, - PointRef, - Range, - RangeRef, Text, type Node, + type Point, } from '../slate' import type {PortableTextSlateEditor} from '../types/slate-editor' +import {rangeRefAffinities} from './range-ref-affinities' + +/** + * Transform a path for a split_node operation. + * + * When a node at `splitPath` is split at `position`: + * - A new sibling appears after the split node, shifting later paths forward + * - Children at or after `position` move into the new sibling + * + * The `affinity` parameter controls what happens when the path equals the + * split path: 'forward' moves to the new node, 'backward' stays. + */ +function transformPathForSplit( + path: Path, + splitPath: Path, + position: number, + affinity: 'forward' | 'backward' = 'forward', +): Path | null { + const p = [...path] + + if (Path.equals(splitPath, p)) { + if (affinity === 'forward') { + p[p.length - 1] = p[p.length - 1]! + 1 + } + // backward: no change + } else if (Path.endsBefore(splitPath, p)) { + p[splitPath.length - 1] = p[splitPath.length - 1]! + 1 + } else if ( + Path.isAncestor(splitPath, p) && + path[splitPath.length]! >= position + ) { + p[splitPath.length - 1] = p[splitPath.length - 1]! + 1 + p[splitPath.length] = p[splitPath.length]! - position + } + + return p +} + +/** + * Transform a point for a split_node operation. + * + * If the point is inside the split node and at or after the split position, + * it moves into the new node with an adjusted offset. + */ +function transformPointForSplit( + point: Point, + splitPath: Path, + position: number, + affinity: 'forward' | 'backward' = 'forward', +): Point | null { + let {path, offset} = point + + if (Path.equals(splitPath, path)) { + if ( + position < offset || + (position === offset && affinity === 'forward') + ) { + offset -= position + path = transformPathForSplit(path, splitPath, position, 'forward')! + } + } else { + path = transformPathForSplit(path, splitPath, position, affinity)! + } + + return {path, offset} +} /** * Split a node at the given path and position using only patch-compliant * operations (remove_text/remove_node + insert_node). * - * This replaces direct `editor.apply({type: 'split_node', ...})` calls - * to eliminate split_node from the operation vocabulary. - * - * Because split_node has specific ref-transform semantics that can't be - * replicated by the incremental transforms of the decomposed operations, - * we pre-transform all active refs with the equivalent split_node operation - * and then suppress ref transforms for the individual low-level operations. + * Because the decomposed operations would produce different ref-transform + * semantics than a single split, we pre-transform all active refs with + * the split semantics directly and suppress ref transforms for the + * individual low-level operations. */ export function applySplitNode( editor: PortableTextSlateEditor, @@ -33,32 +92,41 @@ export function applySplitNode( ): void { const node = NodeUtils.get(editor, path, editor.schema) - // Build the equivalent split_node operation for ref transforms - const splitOp = { - type: 'split_node' as const, - path, - position, - properties, - } - - // Pre-transform all refs as if a split_node happened + // Pre-transform all refs with split semantics for (const ref of Editor.pathRefs(editor)) { - PathRef.transform(ref, splitOp) + const current = ref.current + if (current) { + ref.current = transformPathForSplit(current, path, position, ref.affinity ?? 'forward') + } } for (const ref of Editor.pointRefs(editor)) { - PointRef.transform(ref, splitOp) + const current = ref.current + if (current) { + ref.current = transformPointForSplit(current, path, position, ref.affinity ?? 'forward') + } } for (const ref of Editor.rangeRefs(editor)) { - RangeRef.transform(ref, splitOp) + const current = ref.current + if (current) { + const [anchorAffinity, focusAffinity] = rangeRefAffinities(current, ref.affinity) + const anchor = transformPointForSplit(current.anchor, path, position, anchorAffinity ?? 'forward') + const focus = transformPointForSplit(current.focus, path, position, focusAffinity ?? 'forward') + if (anchor && focus) { + ref.current = {anchor, focus} + } else { + ref.current = null + ref.unref() + } + } } - // Pre-transform editor.selection as if a split_node happened + // Pre-transform editor.selection if (editor.selection) { - const sel = {...editor.selection} - for (const [point, key] of Range.points(sel)) { - sel[key] = Point.transform(point, splitOp)! + const anchor = transformPointForSplit(editor.selection.anchor, path, position, 'forward') + const focus = transformPointForSplit(editor.selection.focus, path, position, 'forward') + if (anchor && focus) { + editor.selection = {anchor, focus} } - editor.selection = sel } // Temporarily remove all refs so the decomposed operations don't @@ -79,7 +147,6 @@ export function applySplitNode( if (Text.isText(node, editor.schema)) { const afterText = node.text.slice(position) const newNode = {...properties, text: afterText} as Node - editor.apply({ type: 'remove_text', path, diff --git a/packages/editor/src/internal-utils/range-ref-affinities.ts b/packages/editor/src/internal-utils/range-ref-affinities.ts new file mode 100644 index 000000000..47e2062f4 --- /dev/null +++ b/packages/editor/src/internal-utils/range-ref-affinities.ts @@ -0,0 +1,34 @@ +import {Range} from '../slate' +import type {RangeRef} from '../slate/interfaces/range-ref' + +/** + * Resolve the per-point affinities for a RangeRef. + * + * This replicates the logic from Range.transform: for 'inward' affinity on a + * forward range, the anchor uses 'forward' and the focus uses 'backward' + * (they contract toward each other). For 'outward', the opposite. For + * 'forward'/'backward'/null, both points use the same affinity. + */ +export function rangeRefAffinities( + range: Range, + affinity: RangeRef['affinity'], +): ['forward' | 'backward' | null, 'forward' | 'backward' | null] { + if (affinity === 'inward') { + const isCollapsed = Range.isCollapsed(range) + if (Range.isForward(range)) { + const anchorAffinity = 'forward' + return [anchorAffinity, isCollapsed ? anchorAffinity : 'backward'] + } else { + const anchorAffinity = 'backward' + return [anchorAffinity, isCollapsed ? anchorAffinity : 'forward'] + } + } else if (affinity === 'outward') { + if (Range.isForward(range)) { + return ['backward', 'forward'] + } else { + return ['forward', 'backward'] + } + } else { + return [affinity, affinity] + } +} diff --git a/packages/editor/src/slate-dom/utils/diff-text.ts b/packages/editor/src/slate-dom/utils/diff-text.ts index 9b2a8b13e..81854db73 100644 --- a/packages/editor/src/slate-dom/utils/diff-text.ts +++ b/packages/editor/src/slate-dom/utils/diff-text.ts @@ -266,15 +266,6 @@ export function transformPendingPoint( return null } - if ( - op.type === 'split_node' && - Path.equals(op.path, point.path) && - anchor.offset < op.position && - diff.start < op.position - ) { - return transformed - } - return { path: transformed.path, offset: transformed.offset + diff.text.length - diff.end + diff.start, diff --git a/packages/editor/src/slate/interfaces/operation.ts b/packages/editor/src/slate/interfaces/operation.ts index f478b6c73..a80a128ba 100644 --- a/packages/editor/src/slate/interfaces/operation.ts +++ b/packages/editor/src/slate/interfaces/operation.ts @@ -23,29 +23,6 @@ export type InsertTextOperation = ExtendedType< BaseInsertTextOperation > -export type BaseMergeNodeOperation = { - type: 'merge_node' - path: Path - position: number - properties: Partial -} - -export type MergeNodeOperation = ExtendedType< - 'MergeNodeOperation', - BaseMergeNodeOperation -> - -export type BaseMoveNodeOperation = { - type: 'move_node' - path: Path - newPath: Path -} - -export type MoveNodeOperation = ExtendedType< - 'MoveNodeOperation', - BaseMoveNodeOperation -> - export type BaseRemoveNodeOperation = { type: 'remove_node' path: Path @@ -103,25 +80,10 @@ export type SetSelectionOperation = ExtendedType< BaseSetSelectionOperation > -export type BaseSplitNodeOperation = { - type: 'split_node' - path: Path - position: number - properties: Partial -} - -export type SplitNodeOperation = ExtendedType< - 'SplitNodeOperation', - BaseSplitNodeOperation -> - export type NodeOperation = | InsertNodeOperation - | MergeNodeOperation - | MoveNodeOperation | RemoveNodeOperation | SetNodeOperation - | SplitNodeOperation export type SelectionOperation = SetSelectionOperation @@ -171,14 +133,6 @@ export const Operation: OperationInterface = { typeof value.text === 'string' && Path.isPath(value.path) ) - case 'merge_node': - return ( - typeof value.position === 'number' && - Path.isPath(value.path) && - isObject(value.properties) - ) - case 'move_node': - return Path.isPath(value.path) && Path.isPath(value.newPath) case 'remove_node': return Path.isPath(value.path) && isObject(value.node) case 'remove_text': @@ -199,12 +153,6 @@ export const Operation: OperationInterface = { (value.newProperties === null && Range.isRange(value.properties)) || (isObject(value.properties) && isObject(value.newProperties)) ) - case 'split_node': - return ( - Path.isPath(value.path) && - typeof value.position === 'number' && - isObject(value.properties) - ) default: return false } @@ -226,35 +174,6 @@ export const Operation: OperationInterface = { return {...op, type: 'remove_text'} } - case 'merge_node': { - return {...op, type: 'split_node', path: Path.previous(op.path)} - } - - case 'move_node': { - const {newPath, path} = op - - // PERF: in this case the move operation is a no-op anyways. - if (Path.equals(newPath, path)) { - return op - } - - // If the move happens completely within a single parent the path and - // newPath are stable with respect to each other. - if (Path.isSibling(path, newPath)) { - return {...op, path: newPath, newPath: path} - } - - // If the move does not happen within a single parent it is possible - // for the move to impact the true path to the location where the node - // was removed from and where it was inserted. We have to adjust for this - // and find the original path. We can accomplish this (only in non-sibling) - // moves by looking at the impact of the move operation on the node - // after the original move path. - const inversePath = Path.transform(path, op)! - const inverseNewPath = Path.transform(Path.next(path), op)! - return {...op, path: inversePath, newPath: inverseNewPath} - } - case 'remove_node': { return {...op, type: 'insert_node'} } @@ -288,9 +207,7 @@ export const Operation: OperationInterface = { } } - case 'split_node': { - return {...op, type: 'merge_node', path: Path.next(op.path)} - } + } }, } diff --git a/packages/editor/src/slate/interfaces/path-ref.ts b/packages/editor/src/slate/interfaces/path-ref.ts index b3dccc771..1bfa84f7a 100644 --- a/packages/editor/src/slate/interfaces/path-ref.ts +++ b/packages/editor/src/slate/interfaces/path-ref.ts @@ -22,13 +22,13 @@ export interface PathRefInterface { // eslint-disable-next-line no-redeclare export const PathRef: PathRefInterface = { transform(ref: PathRef, op: Operation): void { - const {current, affinity} = ref + const {current} = ref if (current == null) { return } - const path = Path.transform(current, op, {affinity}) + const path = Path.transform(current, op) ref.current = path if (path == null) { diff --git a/packages/editor/src/slate/interfaces/path.ts b/packages/editor/src/slate/interfaces/path.ts index fde805078..5396a924f 100644 --- a/packages/editor/src/slate/interfaces/path.ts +++ b/packages/editor/src/slate/interfaces/path.ts @@ -1,12 +1,8 @@ import type { InsertNodeOperation, - MergeNodeOperation, - MoveNodeOperation, Operation, RemoveNodeOperation, - SplitNodeOperation, } from '..' -import type {TextDirection} from '../types/types' /** * `Path` arrays are a list of indexes that describe a node's exact position in @@ -24,10 +20,6 @@ export interface PathLevelsOptions { reverse?: boolean } -export interface PathTransformOptions { - affinity?: TextDirection | null -} - export interface PathInterface { /** * Get a list of ancestor paths for a given path. @@ -118,19 +110,11 @@ export interface PathInterface { /** * Returns whether this operation can affect paths or not. Used as an - * optimization when updating dirty paths during normalization - * - * NOTE: This *must* be kept in sync with the implementation of 'transform' - * below + * optimization when updating dirty paths during normalization. */ operationCanTransformPath: ( operation: Operation, - ) => operation is - | InsertNodeOperation - | RemoveNodeOperation - | MergeNodeOperation - | SplitNodeOperation - | MoveNodeOperation + ) => operation is InsertNodeOperation | RemoveNodeOperation /** * Given a path, return a new path referring to the parent node above it. @@ -145,11 +129,7 @@ export interface PathInterface { /** * Transform a path by an operation. */ - transform: ( - path: Path, - operation: Operation, - options?: PathTransformOptions, - ) => Path | null + transform: (path: Path, operation: Operation) => Path | null } // eslint-disable-next-line no-redeclare @@ -285,18 +265,10 @@ export const Path: PathInterface = { operationCanTransformPath( operation: Operation, - ): operation is - | InsertNodeOperation - | RemoveNodeOperation - | MergeNodeOperation - | SplitNodeOperation - | MoveNodeOperation { + ): operation is InsertNodeOperation | RemoveNodeOperation { switch (operation.type) { case 'insert_node': case 'remove_node': - case 'merge_node': - case 'split_node': - case 'move_node': return true default: return false @@ -329,18 +301,13 @@ export const Path: PathInterface = { return path.slice(0, -1).concat(last - 1) }, - transform( - path: Path | null, - operation: Operation, - options: PathTransformOptions = {}, - ): Path | null { + transform(path: Path | null, operation: Operation): Path | null { if (!path) { return null } // PERF: use destructing instead of immer const p = [...path] - const {affinity = 'forward'} = options // PERF: Exit early if the operation is guaranteed not to have an effect. if (path.length === 0) { @@ -373,86 +340,6 @@ export const Path: PathInterface = { break } - - case 'merge_node': { - const {path: op, position} = operation - - if (Path.equals(op, p) || Path.endsBefore(op, p)) { - p[op.length - 1] = p[op.length - 1]! - 1 - } else if (Path.isAncestor(op, p)) { - p[op.length - 1] = p[op.length - 1]! - 1 - p[op.length] = p[op.length]! + position - } - - break - } - - case 'split_node': { - const {path: op, position} = operation - - if (Path.equals(op, p)) { - if (affinity === 'forward') { - p[p.length - 1] = p[p.length - 1]! + 1 - } else if (affinity === 'backward') { - // Nothing, because it still refers to the right path. - } else { - return null - } - } else if (Path.endsBefore(op, p)) { - p[op.length - 1] = p[op.length - 1]! + 1 - } else if (Path.isAncestor(op, p) && path[op.length]! >= position) { - p[op.length - 1] = p[op.length - 1]! + 1 - p[op.length] = p[op.length]! - position - } - - break - } - - case 'move_node': { - const {path: op, newPath: onp} = operation - - // If the old and new path are the same, it's a no-op. - if (Path.equals(op, onp)) { - return p - } - - if (Path.isAncestor(op, p) || Path.equals(op, p)) { - const copy = onp.slice() - - if (Path.endsBefore(op, onp) && op.length < onp.length) { - copy[op.length - 1] = copy[op.length - 1]! - 1 - } - - return copy.concat(p.slice(op.length)) - } else if ( - Path.isSibling(op, onp) && - (Path.isAncestor(onp, p) || Path.equals(onp, p)) - ) { - if (Path.endsBefore(op, p)) { - p[op.length - 1] = p[op.length - 1]! - 1 - } else { - p[op.length - 1] = p[op.length - 1]! + 1 - } - } else if ( - Path.endsBefore(onp, p) || - Path.equals(onp, p) || - Path.isAncestor(onp, p) - ) { - if (Path.endsBefore(op, p)) { - p[op.length - 1] = p[op.length - 1]! - 1 - } - - p[onp.length - 1] = p[onp.length - 1]! + 1 - } else if (Path.endsBefore(op, p)) { - if (Path.equals(onp, p)) { - p[onp.length - 1] = p[onp.length - 1]! + 1 - } - - p[op.length - 1] = p[op.length - 1]! - 1 - } - - break - } } return p diff --git a/packages/editor/src/slate/interfaces/point.ts b/packages/editor/src/slate/interfaces/point.ts index 1f880f0cf..c7a3a63e9 100644 --- a/packages/editor/src/slate/interfaces/point.ts +++ b/packages/editor/src/slate/interfaces/point.ts @@ -110,9 +110,8 @@ export const Point: PointInterface = { let {path, offset} = point switch (op.type) { - case 'insert_node': - case 'move_node': { - path = Path.transform(path, op, options)! + case 'insert_node': { + path = Path.transform(path, op)! break } @@ -128,15 +127,6 @@ export const Point: PointInterface = { break } - case 'merge_node': { - if (Path.equals(op.path, path)) { - offset += op.position - } - - path = Path.transform(path, op, options)! - break - } - case 'remove_text': { if (Path.equals(op.path, path) && op.offset <= offset) { offset -= Math.min(offset - op.offset, op.text.length) @@ -150,29 +140,7 @@ export const Point: PointInterface = { return null } - path = Path.transform(path, op, options)! - break - } - - case 'split_node': { - if (Path.equals(op.path, path)) { - if (op.position === offset && affinity == null) { - return null - } else if ( - op.position < offset || - (op.position === offset && affinity === 'forward') - ) { - offset -= op.position - - path = Path.transform(path, op, { - ...options, - affinity: 'forward', - })! - } - } else { - path = Path.transform(path, op, options)! - } - + path = Path.transform(path, op)! break } diff --git a/packages/editor/src/slate/types/custom-types.ts b/packages/editor/src/slate/types/custom-types.ts index 023d1c533..e7d5b8365 100644 --- a/packages/editor/src/slate/types/custom-types.ts +++ b/packages/editor/src/slate/types/custom-types.ts @@ -13,13 +13,10 @@ type ExtendableTypes = | 'Operation' | 'InsertNodeOperation' | 'InsertTextOperation' - | 'MergeNodeOperation' - | 'MoveNodeOperation' | 'RemoveNodeOperation' | 'RemoveTextOperation' | 'SetNodeOperation' | 'SetSelectionOperation' - | 'SplitNodeOperation' export interface CustomTypes { [key: string]: unknown