Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/hanging-delete-top-level.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'slate': patch
'slate-react': patch
---

Limit whole-block deletion of a hanging selection, and native triple-click selections, to top-level blocks. Nested blocks such as table cells and list items keep the previous behavior, so cutting or dragging across cells in editors without `editor.isIsolated` no longer removes cells. After undoing a whole-block deletion, the default `scrollSelectionIntoView` scrolls to the restored text instead of the start of the following block.
36 changes: 22 additions & 14 deletions packages/slate-react/src/components/editable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1390,15 +1390,7 @@ export const Editable = forwardRef(
return
}

if (
event.detail === TRIPLE_CLICK &&
path.length >= 1 &&
Editor.above(editor, {
at: Editor.start(editor, path),
match: n =>
Node.isElement(n) && !!editor.isIsolated?.(n),
})
) {
if (event.detail === TRIPLE_CLICK && path.length >= 1) {
let blockPath = path
if (
!(
Expand All @@ -1414,9 +1406,17 @@ export const Editable = forwardRef(
blockPath = block?.[1] ?? path.slice(0, 1)
}

const range = Editor.range(editor, blockPath)
Transforms.select(editor, range)
return
const isIsolated = Editor.above(editor, {
at: Editor.start(editor, blockPath),
match: n =>
Node.isElement(n) && !!editor.isIsolated?.(n),
})

if (blockPath.length > 1 || isIsolated) {
const range = Editor.range(editor, blockPath)
Transforms.select(editor, range)
return
}
}

if (readOnly) {
Expand Down Expand Up @@ -2324,8 +2324,16 @@ export const defaultScrollSelectionIntoView = (
domRange: DOMRange
) => {
// Scroll to the focus point of the selection, in case the selection is expanded
const isBackward = !!editor.selection && Range.isBackward(editor.selection)
const domFocusPoint = domRange.cloneRange()
const { selection } = editor
const isBackward = !!selection && Range.isBackward(selection)
const unhung =
selection && !isBackward && Range.isExpanded(selection)
? Editor.unhangRange(editor, selection)
: selection
const domFocusPoint =
unhung && selection && !Range.equals(unhung, selection)
? ReactEditor.toDOMRange(editor, unhung)
: domRange.cloneRange()
domFocusPoint.collapse(isBackward)

if (domFocusPoint.getBoundingClientRect) {
Expand Down
39 changes: 13 additions & 26 deletions packages/slate-react/src/utils/move-range.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,48 +6,35 @@ const blockPathAt = (editor: Editor, point: Point): Path | undefined =>
match: n => Node.isElement(n) && Editor.isBlock(editor, n),
})?.[1]

const getWholeBlockPaths = (editor: Editor, range: Range): Path[] | null => {
export const getWholeTopLevelBlockPaths = (
editor: Editor,
range: Range
): Path[] | null => {
const [start, end] = Range.edges(range)
const startBlock = blockPathAt(editor, start)
const endBlock = blockPathAt(editor, end)

if (
!startBlock ||
!endBlock ||
!Path.isSibling(startBlock, endBlock) ||
!Path.isBefore(startBlock, endBlock) ||
!!Editor.above(editor, {
at: start,
match: n => Node.isElement(n) && !!editor.isIsolated?.(n),
mode: 'highest',
}) ||
startBlock.length !== 1 ||
endBlock.length !== 1 ||
startBlock[0] >= endBlock[0] ||
!Editor.isStart(editor, start, startBlock) ||
!Editor.isStart(editor, end, endBlock)
) {
return null
}

return Array.from(
{
length: endBlock[endBlock.length - 1] - startBlock[startBlock.length - 1],
},
(_, offset) => [
...Path.parent(startBlock),
startBlock[startBlock.length - 1] + offset,
]
)
}

export const getWholeTopLevelBlockPaths = (
editor: Editor,
range: Range
): Path[] | null => {
const paths = getWholeBlockPaths(editor, range)
return paths?.[0].length === 1 ? paths : null
return Array.from({ length: endBlock[0] - startBlock[0] }, (_, offset) => [
startBlock[0] + offset,
])
}

export const getMovableRange = (editor: Editor, range: Range): Range =>
getWholeBlockPaths(editor, range) ? range : Editor.unhangRange(editor, range)
getWholeTopLevelBlockPaths(editor, range)
? range
: Editor.unhangRange(editor, range)

export const deleteMovedRange = (editor: Editor, range: Range) => {
if (Range.isCollapsed(range)) {
Expand Down
12 changes: 4 additions & 8 deletions packages/slate-react/test/move-range.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,6 @@ const cell = (text: string) => block('table-cell', text)
const editorWith = (children: object[]) => {
const editor = createEditor()
editor.children = children as Descendant[]
editor.isIsolated = element =>
'type' in element && element.type === 'table-cell'
return editor
}

Expand Down Expand Up @@ -188,7 +186,7 @@ describe('moving a dragged or cut range', () => {
).toBeNull()
})

it('removes whole selected list items without merging their neighbors', () => {
it('unhangs instead of merging list items', () => {
const list = {
type: 'bulleted-list',
children: [
Expand All @@ -202,17 +200,15 @@ describe('moving a dragged or cut range', () => {
expect(result.fragment).toEqual([
{
type: 'bulleted-list',
children: [
{ type: 'list-item', children: ['two'] },
{ type: 'list-item', children: [''] },
],
children: [{ type: 'list-item', children: ['two'] }],
},
])
expect(result.children).toEqual([
{
type: 'bulleted-list',
children: [
{ type: 'list-item', children: ['one'] },
{ type: 'list-item', children: [''] },
{ type: 'list-item', children: ['three'] },
],
},
Expand Down Expand Up @@ -298,7 +294,7 @@ describe('moving a dragged or cut range', () => {
const deleteFragment = jest.fn()
editor.deleteFragment = deleteFragment

deleteMovedRange(editor, range([0, 0, 0], 0, [0, 1, 0], 0))
deleteMovedRange(editor, range([0, 0, 0], 1, [0, 1, 0], 1))

expect(deleteFragment).toHaveBeenCalledTimes(1)
expect(Editor.string(editor, [])).toBe('onetwo')
Expand Down
1 change: 1 addition & 0 deletions packages/slate/src/editor/delete-fragment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ export const deleteFragment: EditorInterface['deleteFragment'] = (
!isolated &&
startBlock &&
endBlock &&
startBlock.length === 1 &&
Path.isSibling(startBlock, endBlock) &&
Path.isBefore(startBlock, endBlock) &&
!Editor.isVoid(editor, Node.get(editor, endBlock) as Element) &&
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { createEditor, Editor, Transforms } from 'slate'

for (const nested of [false, true]) {
for (const isolated of [false, true]) {
it(`deletes a hanging whole block with nested=${nested}, isolated=${isolated}`, () => {
it(`deletes a hanging whole top-level block with nested=${nested}, isolated=${isolated}`, () => {
const editor = createEditor()
const blocks = ['one', 'two', 'three'].map(text => ({
children: [{ text }],
Expand All @@ -16,7 +16,8 @@ for (const nested of [false, true]) {
focus: { path: [...prefix, 2, 0], offset: 0 },
})
Editor.deleteFragment(editor, { direction: 'backward' })
const expected = isolated ? ['one', '', 'three'] : ['one', 'three']
const expected =
isolated || nested ? ['one', '', 'three'] : ['one', 'three']
assert.deepStrictEqual(
expected.map((_, index) => Editor.string(editor, [...prefix, index])),
expected
Expand Down
Loading