Skip to content

Commit

Permalink
Add isSelectable
Browse files Browse the repository at this point in the history
  • Loading branch information
12joan committed Mar 21, 2023
1 parent 2b1499c commit b842575
Show file tree
Hide file tree
Showing 17 changed files with 210 additions and 25 deletions.
7 changes: 7 additions & 0 deletions .changeset/spicy-phones-nail.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'slate': minor
---

- Add `isSelectable` to `editor` (default true). A non-selectable element is skipped over when navigating using arrow keys.
- Add `ignoreNonSelectable` to `Editor.nodes`, `Editor.positions`, `Editor.after` and `Editor.before` (default false)
- `Transforms.move` ignores non-selectable elements
2 changes: 1 addition & 1 deletion .changeset/wicked-weeks-kick.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@
'slate': minor
---

Adds `isElementReadOnly` to `editor`. A read-only element behaves much like a void with regard to selection and deletion, but renders its `children` the same as any other non-void node.
- Add `isElementReadOnly` to `editor`. A read-only element behaves much like a void with regard to selection and deletion, but renders its `children` the same as any other non-void node.
6 changes: 5 additions & 1 deletion packages/slate-history/test/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ export const jsx = createHyperscript({
})

const withTest = editor => {
const { isInline, isVoid, isElementReadOnly } = editor
const { isInline, isVoid, isElementReadOnly, isSelectable } = editor

editor.isInline = element => {
return element.inline === true ? true : isInline(element)
Expand All @@ -44,5 +44,9 @@ const withTest = editor => {
return element.readOnly === true ? true : isElementReadOnly(element)
}

editor.isSelectable = element => {
return element.nonSelectable === true ? false : isSelectable(element)
}

return editor
}
1 change: 1 addition & 0 deletions packages/slate/src/create-editor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export const createEditor = (): Editor => {
marks: null,
isElementReadOnly: () => false,
isInline: () => false,
isSelectable: () => true,
isVoid: () => false,
markableVoid: () => false,
onChange: () => {},
Expand Down
47 changes: 41 additions & 6 deletions packages/slate/src/interfaces/editor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ export interface BaseEditor {
// Schema-specific node behaviors.
isElementReadOnly: (element: Element) => boolean
isInline: (element: Element) => boolean
isSelectable: (element: Element) => boolean
isVoid: (element: Element) => boolean
markableVoid: (element: Element) => boolean
normalizeNode: (entry: NodeEntry, options?: { operation?: Operation }) => void
Expand Down Expand Up @@ -158,6 +159,7 @@ export interface EditorNodesOptions<T extends Node> {
universal?: boolean
reverse?: boolean
voids?: boolean
ignoreNonSelectable?: boolean
}

export interface EditorNormalizeOptions {
Expand Down Expand Up @@ -192,6 +194,7 @@ export interface EditorPositionsOptions {
unit?: TextUnitAdjustment
reverse?: boolean
voids?: boolean
ignoreNonSelectable?: boolean
}

export interface EditorPreviousOptions<T extends Node> {
Expand Down Expand Up @@ -272,6 +275,7 @@ export interface EditorInterface {
isEmpty: (editor: Editor, element: Element) => boolean
isInline: (editor: Editor, value: Element) => boolean
isNormalizing: (editor: Editor) => boolean
isSelectable: (editor: Editor, element: Element) => boolean
isStart: (editor: Editor, point: Point, at: Location) => boolean
isVoid: (editor: Editor, value: Element) => boolean
last: (editor: Editor, at: Location) => NodeEntry
Expand Down Expand Up @@ -674,6 +678,7 @@ export const Editor: EditorInterface = {
typeof value.insertText === 'function' &&
typeof value.isElementReadOnly === 'function' &&
typeof value.isInline === 'function' &&
typeof value.isSelectable === 'function' &&
typeof value.isVoid === 'function' &&
typeof value.normalizeNode === 'function' &&
typeof value.onChange === 'function' &&
Expand Down Expand Up @@ -745,6 +750,14 @@ export const Editor: EditorInterface = {
return isNormalizing === undefined ? true : isNormalizing
},

/**
* Check if a value is a selectable `Element` object.
*/

isSelectable(editor: Editor, value: Element): boolean {
return editor.isSelectable(value)
},

/**
* Check if a point is the start point of a location.
*/
Expand Down Expand Up @@ -958,6 +971,7 @@ export const Editor: EditorInterface = {
universal = false,
reverse = false,
voids = false,
ignoreNonSelectable = false,
} = options
let { match } = options

Expand Down Expand Up @@ -986,17 +1000,32 @@ export const Editor: EditorInterface = {
reverse,
from,
to,
pass: ([n]) =>
voids
? false
: Element.isElement(n) &&
(Editor.isVoid(editor, n) || Editor.isElementReadOnly(editor, n)),
pass: ([node]) => {
if (!Element.isElement(node)) return false
if (
!voids &&
(Editor.isVoid(editor, node) ||
Editor.isElementReadOnly(editor, node))
)
return true
if (ignoreNonSelectable && !Editor.isSelectable(editor, node))
return true
return false
},
})

const matches: NodeEntry<T>[] = []
let hit: NodeEntry<T> | undefined

for (const [node, path] of nodeEntries) {
if (
ignoreNonSelectable &&
Element.isElement(node) &&
!Editor.isSelectable(editor, node)
) {
continue
}

const isLower = hit && Path.compare(path, hit[1]) === 0

// In highest mode any node lower than the last hit is not a match.
Expand Down Expand Up @@ -1342,6 +1371,7 @@ export const Editor: EditorInterface = {
unit = 'offset',
reverse = false,
voids = false,
ignoreNonSelectable = false,
} = options

if (!at) {
Expand Down Expand Up @@ -1381,7 +1411,12 @@ export const Editor: EditorInterface = {
// encounter the block node, then all of its text nodes, so when iterating
// through the blockText and leafText we just need to remember a window of
// one block node and leaf node, respectively.
for (const [node, path] of Editor.nodes(editor, { at, reverse, voids })) {
for (const [node, path] of Editor.nodes(editor, {
at,
reverse,
voids,
ignoreNonSelectable,
})) {
/*
* ELEMENT NODE - Yield position(s) for voids, collect blockText for blocks
*/
Expand Down
2 changes: 1 addition & 1 deletion packages/slate/src/transforms/selection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ export const SelectionTransforms: SelectionTransforms = {
}

const { anchor, focus } = selection
const opts = { distance, unit }
const opts = { distance, unit, ignoreNonSelectable: true }
const props: Partial<Range> = {}

if (edge == null || edge === 'anchor') {
Expand Down
5 changes: 4 additions & 1 deletion packages/slate/test/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ describe('slate', () => {
})
})
const withTest = editor => {
const { isInline, isVoid, isElementReadOnly } = editor
const { isInline, isVoid, isElementReadOnly, isSelectable } = editor
editor.isInline = element => {
return element.inline === true ? true : isInline(element)
}
Expand All @@ -57,6 +57,9 @@ const withTest = editor => {
editor.isElementReadOnly = element => {
return element.readOnly === true ? true : isElementReadOnly(element)
}
editor.isSelectable = element => {
return element.nonSelectable === true ? false : isSelectable(element)
}
return editor
}
export const jsx = createHyperscript({
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/** @jsx jsx */

import { Editor } from 'slate'
import { jsx } from '../../..'

export const input = (
<editor>
<block>
one<inline nonSelectable>two</inline>three
</block>
</editor>
)

export const test = editor => {
return Editor.after(
editor,
{ path: [0, 0], offset: 3 },
{ ignoreNonSelectable: true }
)
}

export const output = { path: [0, 2], offset: 0 }
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/** @jsx jsx */

import { Editor } from 'slate'
import { jsx } from '../../..'

export const input = (
<editor>
<block>
one<inline nonSelectable>two</inline>three
</block>
</editor>
)

export const test = editor => {
return Editor.before(
editor,
{ path: [0, 2], offset: 0 },
{ ignoreNonSelectable: true }
)
}

export const output = { path: [0, 0], offset: 3 }
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/** @jsx jsx */
import { Editor, Text } from 'slate'
import { jsx } from '../../../..'

export const input = (
<editor>
<block nonSelectable>one</block>
</editor>
)
export const test = editor => {
return Array.from(
Editor.nodes(editor, {
at: [],
match: Text.isText,
ignoreNonSelectable: true,
})
)
}
export const output = []
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/** @jsx jsx */
import { Editor, Text } from 'slate'
import { jsx } from '../../../..'

export const input = (
<editor>
<block>
one<inline nonSelectable>two</inline>three
</block>
</editor>
)
export const test = editor => {
return Array.from(Editor.nodes(editor, { at: [], ignoreNonSelectable: true }))
}
export const output = [
[input, []],
[input.children[0], [0]],
[input.children[0].children[0], [0, 0]],
[input.children[0].children[2], [0, 2]],
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
/** @jsx jsx */
import { Editor } from 'slate'
import { jsx } from '../../../..'

export const input = (
<editor>
<block nonSelectable>one</block>
</editor>
)
export const test = editor => {
return Array.from(
Editor.positions(editor, { at: [], ignoreNonSelectable: true })
)
}
export const output = []
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/** @jsx jsx */
import { Editor } from 'slate'
import { jsx } from '../../../..'

export const input = (
<editor>
<block>
one<inline nonSelectable>two</inline>three
</block>
</editor>
)
export const test = editor => {
return Array.from(
Editor.positions(editor, { at: [], ignoreNonSelectable: true })
)
}
export const output = [
{ path: [0, 0], offset: 0 },
{ path: [0, 0], offset: 1 },
{ path: [0, 0], offset: 2 },
{ path: [0, 0], offset: 3 },
{ path: [0, 2], offset: 0 },
{ path: [0, 2], offset: 1 },
{ path: [0, 2], offset: 2 },
{ path: [0, 2], offset: 3 },
{ path: [0, 2], offset: 4 },
{ path: [0, 2], offset: 5 },
]
3 changes: 2 additions & 1 deletion packages/slate/test/interfaces/Element/isElement/editor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,9 @@ export const input = {
insertFragment() {},
insertNode() {},
insertText() {},
isInline() {},
isElementReadOnly() {},
isInline() {},
isSelectable() {},
isVoid() {},
normalizeNode() {},
onChange() {},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,9 @@ export const input = [
insertFragment() {},
insertNode() {},
insertText() {},
isInline() {},
isElementReadOnly() {},
isInline() {},
isSelectable() {},
isVoid() {},
normalizeNode() {},
onChange() {},
Expand Down
22 changes: 10 additions & 12 deletions playwright/integration/examples/inlines.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,20 +28,18 @@ test.describe('Inlines example', () => {
selection.addRange(range)
})

const getBadgeIsSelected = async () => {
return (
(await badge.evaluate(e => e.dataset.playwrightSelected)) === 'true'
)
}
const getSelectionContainerText = () =>
page.evaluate(() => {
const selection = window.getSelection()
return selection.anchorNode.parentNode.innerText
})

expect(await getBadgeIsSelected()).toBe(false)
expect(await getSelectionContainerText()).toBe('.')
await page.keyboard.press('ArrowLeft')
expect(await getBadgeIsSelected()).toBe(true)
await page.keyboard.press('ArrowLeft')
expect(await getBadgeIsSelected()).toBe(false)
await page.keyboard.press('ArrowRight')
expect(await getBadgeIsSelected()).toBe(true)
expect(await getSelectionContainerText()).toBe(
'! Here is a read-only inline: '
)
await page.keyboard.press('ArrowRight')
expect(await getBadgeIsSelected()).toBe(false)
expect(await getSelectionContainerText()).toBe('.')
})
})
Loading

0 comments on commit b842575

Please sign in to comment.