diff --git a/packages/slate-react/src/plugin/with-react.ts b/packages/slate-react/src/plugin/with-react.ts index f61a25c80..ed22c52a0 100644 --- a/packages/slate-react/src/plugin/with-react.ts +++ b/packages/slate-react/src/plugin/with-react.ts @@ -5,6 +5,7 @@ import { ReactEditor } from './react-editor' import { Key } from '../utils/key' import { EDITOR_TO_ON_CHANGE, NODE_TO_KEY } from '../utils/weak-maps' import { isDOMText, getPlainText } from '../utils/dom' +import { findCurrentLineRange } from '../utils/lines' /** * `withReact` adds React and DOM specific behaviors to the editor. @@ -17,7 +18,35 @@ import { isDOMText, getPlainText } from '../utils/dom' export const withReact = (editor: T) => { const e = editor as T & ReactEditor - const { apply, onChange } = e + const { apply, onChange, deleteBackward } = e + + e.deleteBackward = unit => { + if (unit !== 'line') { + return deleteBackward(unit) + } + + if (editor.selection && Range.isCollapsed(editor.selection)) { + const parentBlockEntry = Editor.above(editor, { + match: n => Editor.isBlock(editor, n), + at: editor.selection, + }) + + if (parentBlockEntry) { + const [, parentBlockPath] = parentBlockEntry + const parentElementRange = Editor.range( + editor, + parentBlockPath, + editor.selection.anchor + ) + + const currentLineRange = findCurrentLineRange(e, parentElementRange) + + if (!Range.isCollapsed(currentLineRange)) { + Transforms.delete(editor, { at: currentLineRange }) + } + } + } + } e.apply = (op: Operation) => { const matches: [Path, Key][] = [] diff --git a/packages/slate-react/src/utils/lines.ts b/packages/slate-react/src/utils/lines.ts new file mode 100644 index 000000000..960d77a55 --- /dev/null +++ b/packages/slate-react/src/utils/lines.ts @@ -0,0 +1,79 @@ +/** + * Utilities for single-line deletion + */ + +import { Range, Editor } from 'slate' +import { ReactEditor } from '..' + +const doRectsIntersect = (rect: DOMRect, compareRect: DOMRect) => { + const middle = (compareRect.top + compareRect.bottom) / 2 + + return rect.top <= middle && rect.bottom >= middle +} + +const areRangesSameLine = ( + editor: ReactEditor, + range1: Range, + range2: Range +) => { + const rect1 = ReactEditor.toDOMRange(editor, range1).getBoundingClientRect() + const rect2 = ReactEditor.toDOMRange(editor, range2).getBoundingClientRect() + + return doRectsIntersect(rect1, rect2) && doRectsIntersect(rect2, rect1) +} + +/** + * A helper utility that returns the end portion of a `Range` + * which is located on a single line. + * + * @param {Editor} editor The editor object to compare against + * @param {Range} parentRange The parent range to compare against + * @returns {Range} A valid portion of the parentRange which is one a single line + */ +export const findCurrentLineRange = ( + editor: ReactEditor, + parentRange: Range +): Range => { + const parentRangeBoundary = Editor.range(editor, Range.end(parentRange)) + const positions = Array.from(Editor.positions(editor, { at: parentRange })) + + let left = 0 + let right = positions.length + let middle = Math.floor(right / 2) + + if ( + areRangesSameLine( + editor, + Editor.range(editor, positions[left]), + parentRangeBoundary + ) + ) { + return Editor.range(editor, positions[left], parentRangeBoundary) + } + + if (positions.length < 2) { + return Editor.range( + editor, + positions[positions.length - 1], + parentRangeBoundary + ) + } + + while (middle !== positions.length && middle !== left) { + if ( + areRangesSameLine( + editor, + Editor.range(editor, positions[middle]), + parentRangeBoundary + ) + ) { + right = middle + } else { + left = middle + } + + middle = Math.floor((left + right) / 2) + } + + return Editor.range(editor, positions[right], parentRangeBoundary) +}