Skip to content
Draft
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
5 changes: 5 additions & 0 deletions .changeset/bumpy-spoons-check.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@portabletext/editor': patch
---

fix: issues with expanding selection down using Shift+ArrowDown
5 changes: 5 additions & 0 deletions .changeset/short-turtles-fetch.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@portabletext/editor': patch
---

fix(dom): respect snapshot selection in `.getSelectionRect`
35 changes: 35 additions & 0 deletions packages/editor/gherkin-spec/selection.feature
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,41 @@ Feature: Selection
Background:
Given one editor

Scenario Outline: Expanding selection down
Given the text <text>
When the caret is put <position>
And "{Shift>}{ArrowDown}{/Shift}" is pressed
And "{Shift>}{ArrowDown}{/Shift}" is pressed
Then <selection> is selected

Examples:
| text | position | selection |
| "foo\|bar\|baz" | before "foo" | "foo\|bar\|" |
| "foo\|>#:bar\|baz" | before "foo" | "foo\|>#:bar\|" |
| "foo\|>#:bar\|{image}" | before "foo" | "foo\|>#:bar" |

Scenario: Expanding selection down into block object
Given the text "foo|{image}|bar"
When the caret is put before "foo"
And "{Shift>}{ArrowDown}{/Shift}" is pressed
And "{Shift>}{ArrowDown}{/Shift}" is pressed
Then "foo|{image}" is selected

Scenario Outline: Expanding selection down through block objects
Given the text <text>
When the caret is put before "foo"
And "{Shift>}{ArrowDown}{/Shift}" is pressed
And "{Shift>}{ArrowDown}{/Shift}" is pressed
And "{Shift>}{ArrowDown}{/Shift}" is pressed
Then <selection> is selected

Examples:
| text | selection |
| "foo\|{image}\|bar" | "foo\|{image}\|bar" |
| "foo\|{image}\|bar\|baz" | "foo\|{image}\|bar\|" |
| "foo\|{image}\|bar\|{image}" | "foo\|{image}\|bar" |
| "foo\|{image}\|{image}\|bar" | "foo\|{image}\|{image}" |

Scenario: Expanding collapsed selection backwards from empty line
Given the text "foo|"
When the editor is focused
Expand Down
288 changes: 274 additions & 14 deletions packages/editor/src/behaviors/behavior.abstract.keyboard.ts
Original file line number Diff line number Diff line change
@@ -1,28 +1,20 @@
import {createKeyboardShortcut} from '@portabletext/keyboard-shortcuts'
import {isTextBlock} from '@portabletext/schema'
import {defaultKeyboardShortcuts} from '../editor/default-keyboard-shortcuts'
import {getFocusBlockObject} from '../selectors'
import {getFocusBlock} from '../selectors/selector.get-focus-block'
import {getFocusInlineObject} from '../selectors/selector.get-focus-inline-object'
import {getFocusTextBlock} from '../selectors/selector.get-focus-text-block'
import {getNextBlock} from '../selectors/selector.get-next-block'
import {getPreviousBlock} from '../selectors/selector.get-previous-block'
import {isSelectionCollapsed} from '../selectors/selector.is-selection-collapsed'
import {isSelectionExpanded} from '../selectors/selector.is-selection-expanded'
import {isEqualSelectionPoints} from '../utils'
import {getBlockEndPoint} from '../utils/util.get-block-end-point'
import {getBlockStartPoint} from '../utils/util.get-block-start-point'
import {isEmptyTextBlock} from '../utils/util.is-empty-text-block'
import {raise} from './behavior.types.action'
import {defineBehavior} from './behavior.types.behavior'

const shiftLeft = createKeyboardShortcut({
default: [
{
key: 'ArrowLeft',
shift: true,
meta: false,
ctrl: false,
alt: false,
},
],
})

export const abstractKeyboardBehaviors = [
/**
* When Backspace is pressed on an inline object, Slate will raise a
Expand Down Expand Up @@ -130,7 +122,10 @@ export const abstractKeyboardBehaviors = [
defineBehavior({
on: 'keyboard.keydown',
guard: ({snapshot, event}) => {
if (!snapshot.context.selection || !shiftLeft.guard(event.originEvent)) {
if (
!snapshot.context.selection ||
!defaultKeyboardShortcuts.shiftLeft.guard(event.originEvent)
) {
return false
}

Expand Down Expand Up @@ -186,4 +181,269 @@ export const abstractKeyboardBehaviors = [
],
],
}),

defineBehavior({
on: 'keyboard.keydown',
guard: ({snapshot, event}) => {
if (
!snapshot.context.selection ||
!defaultKeyboardShortcuts.shiftDown.guard(event.originEvent)
) {
return false
}

const focusBlockObject = getFocusBlockObject(snapshot)

if (!focusBlockObject) {
return false
}

const nextBlock = getNextBlock(snapshot)

if (!nextBlock) {
return false
}

if (!isTextBlock(snapshot.context, nextBlock.node)) {
return {
nextBlockEndPoint: getBlockEndPoint({
context: snapshot.context,
block: nextBlock,
}),
selection: snapshot.context.selection,
}
}

const nextNextBlock = getNextBlock({
...snapshot,
context: {
...snapshot.context,
selection: {
anchor: {
path: nextBlock.path,
offset: 0,
},
focus: {
path: nextBlock.path,
offset: 0,
},
},
},
})

const nextBlockEndPoint =
nextNextBlock && isTextBlock(snapshot.context, nextNextBlock.node)
? getBlockStartPoint({
context: snapshot.context,
block: nextNextBlock,
})
: getBlockEndPoint({
context: snapshot.context,
block: nextBlock,
})

return {nextBlockEndPoint, selection: snapshot.context.selection}
},
actions: [
(_, {nextBlockEndPoint, selection}) => [
raise({
type: 'select',
at: {anchor: selection.anchor, focus: nextBlockEndPoint},
}),
],
],
}),

defineBehavior({
on: 'keyboard.keydown',
guard: ({snapshot, event, dom}) => {
if (
!snapshot.context.selection ||
!defaultKeyboardShortcuts.shiftDown.guard(event.originEvent)
) {
return false
}

const focusTextBlock = getFocusTextBlock(snapshot)

if (!focusTextBlock) {
return false
}

const nextBlock = getNextBlock(snapshot)

if (!nextBlock) {
return false
}

if (isTextBlock(snapshot.context, nextBlock.node)) {
return false
}

const focusBlockEndPoint = getBlockEndPoint({
context: snapshot.context,
block: focusTextBlock,
})

if (
isEqualSelectionPoints(
snapshot.context.selection.focus,
focusBlockEndPoint,
)
) {
return false
}

// Find the DOM position of the current focus point
const focusRect = dom.getSelectionRect({
...snapshot,
context: {
...snapshot.context,
selection: {
anchor: snapshot.context.selection.focus,
focus: snapshot.context.selection.focus,
},
},
})
// Find the DOM position of the focus block end point
const endPointRect = dom.getSelectionRect({
...snapshot,
context: {
...snapshot.context,
selection: {
anchor: focusBlockEndPoint,
focus: focusBlockEndPoint,
},
},
})

if (!focusRect || !endPointRect) {
return false
}

if (endPointRect.top > focusRect.top) {
// If the end point is positioned further from the top than the current
// focus point, then we can deduce that the end point is on the next
// line. In this case, we don't want to interfere since the browser
// does right thing and expands the selection to the end of the current
// line.
return false
}

// If the end point is positioned at the same level as the current focus
// point, then we can deduce that the end point is on the same line. In
// this case, we want to expand the selection to the end point.
// This mitigates a Firefox bug where Shift+ArrowDown can expand
// further into the next block.
return {focusBlockEndPoint, selection: snapshot.context.selection}
},
actions: [
(_, {focusBlockEndPoint, selection}) => [
raise({
type: 'select',
at: {
anchor: selection.anchor,
focus: focusBlockEndPoint,
},
}),
],
],
}),

defineBehavior({
on: 'keyboard.keydown',
guard: ({snapshot, event, dom}) => {
if (
!snapshot.context.selection ||
!defaultKeyboardShortcuts.shiftDown.guard(event.originEvent)
) {
return false
}

const focusTextBlock = getFocusTextBlock(snapshot)

if (!focusTextBlock) {
return false
}

const nextBlock = getNextBlock(snapshot)

if (!nextBlock) {
return false
}

const focusBlockEndPoint = getBlockEndPoint({
context: snapshot.context,
block: focusTextBlock,
})

if (
isEqualSelectionPoints(
snapshot.context.selection.focus,
focusBlockEndPoint,
)
) {
return false
}

// Find the DOM position of the current focus point
const focusRect = dom.getSelectionRect({
...snapshot,
context: {
...snapshot.context,
selection: {
anchor: snapshot.context.selection.focus,
focus: snapshot.context.selection.focus,
},
},
})
// Find the DOM position of the focus block end point
const endPointRect = dom.getSelectionRect({
...snapshot,
context: {
...snapshot.context,
selection: {
anchor: focusBlockEndPoint,
focus: focusBlockEndPoint,
},
},
})

if (!focusRect || !endPointRect) {
return false
}

if (endPointRect.top > focusRect.top) {
// If the end point is positioned further from the top than the current
// focus point, then we can deduce that the end point is on the next
// line. In this case, we don't want to interfere since the browser
// does right thing and expands the selection to the end of the current
// line.
return false
}

// If the end point is positioned at the same level as the current focus
// point, then we can deduce that the end point is on the same line. In
// this case, we want to expand the selection to the end of the start
// block. This mitigates a Chromium bug where Shift+ArrowDown can expand
// further into the next block.
const nextBlockStartPoint = getBlockStartPoint({
context: snapshot.context,
block: nextBlock,
})

return {nextBlockStartPoint, selection: snapshot.context.selection}
},
actions: [
(_, {nextBlockStartPoint, selection}) => [
raise({
type: 'select',
at: {
anchor: selection.anchor,
focus: nextBlockStartPoint,
},
}),
],
],
}),
]
22 changes: 22 additions & 0 deletions packages/editor/src/editor/default-keyboard-shortcuts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,28 @@ export const defaultKeyboardShortcuts = {
},
],
}),
shiftDown: createKeyboardShortcut({
default: [
{
key: 'ArrowDown',
shift: true,
meta: false,
ctrl: false,
alt: false,
},
],
}),
shiftLeft: createKeyboardShortcut({
default: [
{
key: 'ArrowLeft',
shift: true,
meta: false,
ctrl: false,
alt: false,
},
],
}),
shiftTab: createKeyboardShortcut({
default: [
{
Expand Down
Loading
Loading