mirror of
https://github.com/ianstormtaylor/slate.git
synced 2025-08-22 06:53:25 +02:00
Add support for read-only and non-selectable elements (#5374)
* Add `isElementReadOnly` fix delete while selected fix type while selected fix failing test add tests add e2e test linter fixes add changeset * fix yarn build:next * Add `isSelectable`
This commit is contained in:
7
.changeset/spicy-phones-nail.md
Normal file
7
.changeset/spicy-phones-nail.md
Normal file
@@ -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
|
5
.changeset/wicked-weeks-kick.md
Normal file
5
.changeset/wicked-weeks-kick.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
'slate': minor
|
||||||
|
---
|
||||||
|
|
||||||
|
- 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.
|
@@ -30,7 +30,7 @@ export const jsx = createHyperscript({
|
|||||||
})
|
})
|
||||||
|
|
||||||
const withTest = editor => {
|
const withTest = editor => {
|
||||||
const { isInline, isVoid } = editor
|
const { isInline, isVoid, isElementReadOnly, isSelectable } = editor
|
||||||
|
|
||||||
editor.isInline = element => {
|
editor.isInline = element => {
|
||||||
return element.inline === true ? true : isInline(element)
|
return element.inline === true ? true : isInline(element)
|
||||||
@@ -40,5 +40,13 @@ const withTest = editor => {
|
|||||||
return element.void === true ? true : isVoid(element)
|
return element.void === true ? true : isVoid(element)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
editor.isElementReadOnly = element => {
|
||||||
|
return element.readOnly === true ? true : isElementReadOnly(element)
|
||||||
|
}
|
||||||
|
|
||||||
|
editor.isSelectable = element => {
|
||||||
|
return element.nonSelectable === true ? false : isSelectable(element)
|
||||||
|
}
|
||||||
|
|
||||||
return editor
|
return editor
|
||||||
}
|
}
|
||||||
|
@@ -25,7 +25,9 @@ export const createEditor = (): Editor => {
|
|||||||
operations: [],
|
operations: [],
|
||||||
selection: null,
|
selection: null,
|
||||||
marks: null,
|
marks: null,
|
||||||
|
isElementReadOnly: () => false,
|
||||||
isInline: () => false,
|
isInline: () => false,
|
||||||
|
isSelectable: () => true,
|
||||||
isVoid: () => false,
|
isVoid: () => false,
|
||||||
markableVoid: () => false,
|
markableVoid: () => false,
|
||||||
onChange: () => {},
|
onChange: () => {},
|
||||||
|
@@ -59,7 +59,9 @@ export interface BaseEditor {
|
|||||||
marks: EditorMarks | null
|
marks: EditorMarks | null
|
||||||
|
|
||||||
// Schema-specific node behaviors.
|
// Schema-specific node behaviors.
|
||||||
|
isElementReadOnly: (element: Element) => boolean
|
||||||
isInline: (element: Element) => boolean
|
isInline: (element: Element) => boolean
|
||||||
|
isSelectable: (element: Element) => boolean
|
||||||
isVoid: (element: Element) => boolean
|
isVoid: (element: Element) => boolean
|
||||||
markableVoid: (element: Element) => boolean
|
markableVoid: (element: Element) => boolean
|
||||||
normalizeNode: (entry: NodeEntry, options?: { operation?: Operation }) => void
|
normalizeNode: (entry: NodeEntry, options?: { operation?: Operation }) => void
|
||||||
@@ -116,6 +118,12 @@ export interface EditorDirectedDeletionOptions {
|
|||||||
unit?: TextUnit
|
unit?: TextUnit
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface EditorElementReadOnlyOptions {
|
||||||
|
at?: Location
|
||||||
|
mode?: MaximizeMode
|
||||||
|
voids?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
export interface EditorFragmentDeletionOptions {
|
export interface EditorFragmentDeletionOptions {
|
||||||
direction?: TextDirection
|
direction?: TextDirection
|
||||||
}
|
}
|
||||||
@@ -151,6 +159,7 @@ export interface EditorNodesOptions<T extends Node> {
|
|||||||
universal?: boolean
|
universal?: boolean
|
||||||
reverse?: boolean
|
reverse?: boolean
|
||||||
voids?: boolean
|
voids?: boolean
|
||||||
|
ignoreNonSelectable?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface EditorNormalizeOptions {
|
export interface EditorNormalizeOptions {
|
||||||
@@ -185,6 +194,7 @@ export interface EditorPositionsOptions {
|
|||||||
unit?: TextUnitAdjustment
|
unit?: TextUnitAdjustment
|
||||||
reverse?: boolean
|
reverse?: boolean
|
||||||
voids?: boolean
|
voids?: boolean
|
||||||
|
ignoreNonSelectable?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface EditorPreviousOptions<T extends Node> {
|
export interface EditorPreviousOptions<T extends Node> {
|
||||||
@@ -241,6 +251,10 @@ export interface EditorInterface {
|
|||||||
options?: EditorFragmentDeletionOptions
|
options?: EditorFragmentDeletionOptions
|
||||||
) => void
|
) => void
|
||||||
edges: (editor: Editor, at: Location) => [Point, Point]
|
edges: (editor: Editor, at: Location) => [Point, Point]
|
||||||
|
elementReadOnly: (
|
||||||
|
editor: Editor,
|
||||||
|
options?: EditorElementReadOnlyOptions
|
||||||
|
) => NodeEntry<Element> | undefined
|
||||||
end: (editor: Editor, at: Location) => Point
|
end: (editor: Editor, at: Location) => Point
|
||||||
first: (editor: Editor, at: Location) => NodeEntry
|
first: (editor: Editor, at: Location) => NodeEntry
|
||||||
fragment: (editor: Editor, at: Location) => Descendant[]
|
fragment: (editor: Editor, at: Location) => Descendant[]
|
||||||
@@ -257,9 +271,11 @@ export interface EditorInterface {
|
|||||||
isEditor: (value: any) => value is Editor
|
isEditor: (value: any) => value is Editor
|
||||||
isEnd: (editor: Editor, point: Point, at: Location) => boolean
|
isEnd: (editor: Editor, point: Point, at: Location) => boolean
|
||||||
isEdge: (editor: Editor, point: Point, at: Location) => boolean
|
isEdge: (editor: Editor, point: Point, at: Location) => boolean
|
||||||
|
isElementReadOnly: (editor: Editor, element: Element) => boolean
|
||||||
isEmpty: (editor: Editor, element: Element) => boolean
|
isEmpty: (editor: Editor, element: Element) => boolean
|
||||||
isInline: (editor: Editor, value: Element) => boolean
|
isInline: (editor: Editor, value: Element) => boolean
|
||||||
isNormalizing: (editor: Editor) => boolean
|
isNormalizing: (editor: Editor) => boolean
|
||||||
|
isSelectable: (editor: Editor, element: Element) => boolean
|
||||||
isStart: (editor: Editor, point: Point, at: Location) => boolean
|
isStart: (editor: Editor, point: Point, at: Location) => boolean
|
||||||
isVoid: (editor: Editor, value: Element) => boolean
|
isVoid: (editor: Editor, value: Element) => boolean
|
||||||
last: (editor: Editor, at: Location) => NodeEntry
|
last: (editor: Editor, at: Location) => NodeEntry
|
||||||
@@ -509,6 +525,20 @@ export const Editor: EditorInterface = {
|
|||||||
return [Editor.start(editor, at), Editor.end(editor, at)]
|
return [Editor.start(editor, at), Editor.end(editor, at)]
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Match a read-only element in the current branch of the editor.
|
||||||
|
*/
|
||||||
|
|
||||||
|
elementReadOnly(
|
||||||
|
editor: Editor,
|
||||||
|
options: EditorElementReadOnlyOptions = {}
|
||||||
|
): NodeEntry<Element> | undefined {
|
||||||
|
return Editor.above(editor, {
|
||||||
|
...options,
|
||||||
|
match: n => Element.isElement(n) && Editor.isElementReadOnly(editor, n),
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the end point of a location.
|
* Get the end point of a location.
|
||||||
*/
|
*/
|
||||||
@@ -646,7 +676,9 @@ export const Editor: EditorInterface = {
|
|||||||
typeof value.insertFragment === 'function' &&
|
typeof value.insertFragment === 'function' &&
|
||||||
typeof value.insertNode === 'function' &&
|
typeof value.insertNode === 'function' &&
|
||||||
typeof value.insertText === 'function' &&
|
typeof value.insertText === 'function' &&
|
||||||
|
typeof value.isElementReadOnly === 'function' &&
|
||||||
typeof value.isInline === 'function' &&
|
typeof value.isInline === 'function' &&
|
||||||
|
typeof value.isSelectable === 'function' &&
|
||||||
typeof value.isVoid === 'function' &&
|
typeof value.isVoid === 'function' &&
|
||||||
typeof value.normalizeNode === 'function' &&
|
typeof value.normalizeNode === 'function' &&
|
||||||
typeof value.onChange === 'function' &&
|
typeof value.onChange === 'function' &&
|
||||||
@@ -701,6 +733,14 @@ export const Editor: EditorInterface = {
|
|||||||
return editor.isInline(value)
|
return editor.isInline(value)
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a value is a read-only `Element` object.
|
||||||
|
*/
|
||||||
|
|
||||||
|
isElementReadOnly(editor: Editor, value: Element): boolean {
|
||||||
|
return editor.isElementReadOnly(value)
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if the editor is currently normalizing after each operation.
|
* Check if the editor is currently normalizing after each operation.
|
||||||
*/
|
*/
|
||||||
@@ -710,6 +750,14 @@ export const Editor: EditorInterface = {
|
|||||||
return isNormalizing === undefined ? true : isNormalizing
|
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.
|
* Check if a point is the start point of a location.
|
||||||
*/
|
*/
|
||||||
@@ -923,6 +971,7 @@ export const Editor: EditorInterface = {
|
|||||||
universal = false,
|
universal = false,
|
||||||
reverse = false,
|
reverse = false,
|
||||||
voids = false,
|
voids = false,
|
||||||
|
ignoreNonSelectable = false,
|
||||||
} = options
|
} = options
|
||||||
let { match } = options
|
let { match } = options
|
||||||
|
|
||||||
@@ -951,14 +1000,32 @@ export const Editor: EditorInterface = {
|
|||||||
reverse,
|
reverse,
|
||||||
from,
|
from,
|
||||||
to,
|
to,
|
||||||
pass: ([n]) =>
|
pass: ([node]) => {
|
||||||
voids ? false : Element.isElement(n) && Editor.isVoid(editor, n),
|
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>[] = []
|
const matches: NodeEntry<T>[] = []
|
||||||
let hit: NodeEntry<T> | undefined
|
let hit: NodeEntry<T> | undefined
|
||||||
|
|
||||||
for (const [node, path] of nodeEntries) {
|
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
|
const isLower = hit && Path.compare(path, hit[1]) === 0
|
||||||
|
|
||||||
// In highest mode any node lower than the last hit is not a match.
|
// In highest mode any node lower than the last hit is not a match.
|
||||||
@@ -1304,6 +1371,7 @@ export const Editor: EditorInterface = {
|
|||||||
unit = 'offset',
|
unit = 'offset',
|
||||||
reverse = false,
|
reverse = false,
|
||||||
voids = false,
|
voids = false,
|
||||||
|
ignoreNonSelectable = false,
|
||||||
} = options
|
} = options
|
||||||
|
|
||||||
if (!at) {
|
if (!at) {
|
||||||
@@ -1343,7 +1411,12 @@ export const Editor: EditorInterface = {
|
|||||||
// encounter the block node, then all of its text nodes, so when iterating
|
// 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
|
// through the blockText and leafText we just need to remember a window of
|
||||||
// one block node and leaf node, respectively.
|
// 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
|
* ELEMENT NODE - Yield position(s) for voids, collect blockText for blocks
|
||||||
*/
|
*/
|
||||||
@@ -1351,7 +1424,7 @@ export const Editor: EditorInterface = {
|
|||||||
// Void nodes are a special case, so by default we will always
|
// Void nodes are a special case, so by default we will always
|
||||||
// yield their first point. If the `voids` option is set to true,
|
// yield their first point. If the `voids` option is set to true,
|
||||||
// then we will iterate over their content.
|
// then we will iterate over their content.
|
||||||
if (!voids && editor.isVoid(node)) {
|
if (!voids && (editor.isVoid(node) || editor.isElementReadOnly(node))) {
|
||||||
yield Editor.start(editor, path)
|
yield Editor.start(editor, path)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
@@ -92,7 +92,7 @@ export const SelectionTransforms: SelectionTransforms = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const { anchor, focus } = selection
|
const { anchor, focus } = selection
|
||||||
const opts = { distance, unit }
|
const opts = { distance, unit, ignoreNonSelectable: true }
|
||||||
const props: Partial<Range> = {}
|
const props: Partial<Range> = {}
|
||||||
|
|
||||||
if (edge == null || edge === 'anchor') {
|
if (edge == null || edge === 'anchor') {
|
||||||
|
@@ -120,15 +120,17 @@ export const TextTransforms: TextTransforms = {
|
|||||||
const isAcrossBlocks =
|
const isAcrossBlocks =
|
||||||
startBlock && endBlock && !Path.equals(startBlock[1], endBlock[1])
|
startBlock && endBlock && !Path.equals(startBlock[1], endBlock[1])
|
||||||
const isSingleText = Path.equals(start.path, end.path)
|
const isSingleText = Path.equals(start.path, end.path)
|
||||||
const startVoid = voids
|
const startNonEditable = voids
|
||||||
? null
|
? null
|
||||||
: Editor.void(editor, { at: start, mode: 'highest' })
|
: Editor.void(editor, { at: start, mode: 'highest' }) ??
|
||||||
const endVoid = voids
|
Editor.elementReadOnly(editor, { at: start, mode: 'highest' })
|
||||||
|
const endNonEditable = voids
|
||||||
? null
|
? null
|
||||||
: Editor.void(editor, { at: end, mode: 'highest' })
|
: Editor.void(editor, { at: end, mode: 'highest' }) ??
|
||||||
|
Editor.elementReadOnly(editor, { at: end, mode: 'highest' })
|
||||||
|
|
||||||
// If the start or end points are inside an inline void, nudge them out.
|
// If the start or end points are inside an inline void, nudge them out.
|
||||||
if (startVoid) {
|
if (startNonEditable) {
|
||||||
const before = Editor.before(editor, start)
|
const before = Editor.before(editor, start)
|
||||||
|
|
||||||
if (
|
if (
|
||||||
@@ -140,7 +142,7 @@ export const TextTransforms: TextTransforms = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (endVoid) {
|
if (endNonEditable) {
|
||||||
const after = Editor.after(editor, end)
|
const after = Editor.after(editor, end)
|
||||||
|
|
||||||
if (after && endBlock && Path.isAncestor(endBlock[1], after.path)) {
|
if (after && endBlock && Path.isAncestor(endBlock[1], after.path)) {
|
||||||
@@ -161,7 +163,10 @@ export const TextTransforms: TextTransforms = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
(!voids && Element.isElement(node) && Editor.isVoid(editor, node)) ||
|
(!voids &&
|
||||||
|
Element.isElement(node) &&
|
||||||
|
(Editor.isVoid(editor, node) ||
|
||||||
|
Editor.isElementReadOnly(editor, node))) ||
|
||||||
(!Path.isCommon(path, start.path) && !Path.isCommon(path, end.path))
|
(!Path.isCommon(path, start.path) && !Path.isCommon(path, end.path))
|
||||||
) {
|
) {
|
||||||
matches.push(entry)
|
matches.push(entry)
|
||||||
@@ -175,7 +180,7 @@ export const TextTransforms: TextTransforms = {
|
|||||||
|
|
||||||
let removedText = ''
|
let removedText = ''
|
||||||
|
|
||||||
if (!isSingleText && !startVoid) {
|
if (!isSingleText && !startNonEditable) {
|
||||||
const point = startRef.current!
|
const point = startRef.current!
|
||||||
const [node] = Editor.leaf(editor, point)
|
const [node] = Editor.leaf(editor, point)
|
||||||
const { path } = point
|
const { path } = point
|
||||||
@@ -193,7 +198,7 @@ export const TextTransforms: TextTransforms = {
|
|||||||
.filter((r): r is Path => r !== null)
|
.filter((r): r is Path => r !== null)
|
||||||
.forEach(p => Transforms.removeNodes(editor, { at: p, voids }))
|
.forEach(p => Transforms.removeNodes(editor, { at: p, voids }))
|
||||||
|
|
||||||
if (!endVoid) {
|
if (!endNonEditable) {
|
||||||
const point = endRef.current!
|
const point = endRef.current!
|
||||||
const [node] = Editor.leaf(editor, point)
|
const [node] = Editor.leaf(editor, point)
|
||||||
const { path } = point
|
const { path } = point
|
||||||
@@ -516,7 +521,10 @@ export const TextTransforms: TextTransforms = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!voids && Editor.void(editor, { at })) {
|
if (
|
||||||
|
(!voids && Editor.void(editor, { at })) ||
|
||||||
|
Editor.elementReadOnly(editor, { at })
|
||||||
|
) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -47,13 +47,19 @@ describe('slate', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
const withTest = editor => {
|
const withTest = editor => {
|
||||||
const { isInline, isVoid } = editor
|
const { isInline, isVoid, isElementReadOnly, isSelectable } = editor
|
||||||
editor.isInline = element => {
|
editor.isInline = element => {
|
||||||
return element.inline === true ? true : isInline(element)
|
return element.inline === true ? true : isInline(element)
|
||||||
}
|
}
|
||||||
editor.isVoid = element => {
|
editor.isVoid = element => {
|
||||||
return element.void === true ? true : isVoid(element)
|
return element.void === true ? true : isVoid(element)
|
||||||
}
|
}
|
||||||
|
editor.isElementReadOnly = element => {
|
||||||
|
return element.readOnly === true ? true : isElementReadOnly(element)
|
||||||
|
}
|
||||||
|
editor.isSelectable = element => {
|
||||||
|
return element.nonSelectable === true ? false : isSelectable(element)
|
||||||
|
}
|
||||||
return editor
|
return editor
|
||||||
}
|
}
|
||||||
export const jsx = createHyperscript({
|
export const jsx = createHyperscript({
|
||||||
|
@@ -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 }
|
@@ -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 }
|
@@ -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 = []
|
@@ -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]],
|
||||||
|
]
|
@@ -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 = []
|
@@ -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 },
|
||||||
|
]
|
@@ -15,7 +15,9 @@ export const input = {
|
|||||||
insertFragment() {},
|
insertFragment() {},
|
||||||
insertNode() {},
|
insertNode() {},
|
||||||
insertText() {},
|
insertText() {},
|
||||||
|
isElementReadOnly() {},
|
||||||
isInline() {},
|
isInline() {},
|
||||||
|
isSelectable() {},
|
||||||
isVoid() {},
|
isVoid() {},
|
||||||
normalizeNode() {},
|
normalizeNode() {},
|
||||||
onChange() {},
|
onChange() {},
|
||||||
|
@@ -16,7 +16,9 @@ export const input = [
|
|||||||
insertFragment() {},
|
insertFragment() {},
|
||||||
insertNode() {},
|
insertNode() {},
|
||||||
insertText() {},
|
insertText() {},
|
||||||
|
isElementReadOnly() {},
|
||||||
isInline() {},
|
isInline() {},
|
||||||
|
isSelectable() {},
|
||||||
isVoid() {},
|
isVoid() {},
|
||||||
normalizeNode() {},
|
normalizeNode() {},
|
||||||
onChange() {},
|
onChange() {},
|
||||||
|
@@ -0,0 +1,25 @@
|
|||||||
|
/** @jsx jsx */
|
||||||
|
import { Transforms } from 'slate'
|
||||||
|
import { jsx } from '../../..'
|
||||||
|
|
||||||
|
export const run = editor => {
|
||||||
|
Transforms.delete(editor, { reverse: true })
|
||||||
|
}
|
||||||
|
export const input = (
|
||||||
|
<editor>
|
||||||
|
<block>
|
||||||
|
<text />
|
||||||
|
<inline readOnly>read-only inline</inline>
|
||||||
|
<cursor />
|
||||||
|
</block>
|
||||||
|
</editor>
|
||||||
|
)
|
||||||
|
export const output = (
|
||||||
|
<editor>
|
||||||
|
<block>
|
||||||
|
<text>
|
||||||
|
<cursor />
|
||||||
|
</text>
|
||||||
|
</block>
|
||||||
|
</editor>
|
||||||
|
)
|
@@ -0,0 +1,27 @@
|
|||||||
|
/** @jsx jsx */
|
||||||
|
import { Transforms } from 'slate'
|
||||||
|
import { jsx } from '../../..'
|
||||||
|
|
||||||
|
export const run = editor => {
|
||||||
|
Transforms.delete(editor, { reverse: true })
|
||||||
|
}
|
||||||
|
export const input = (
|
||||||
|
<editor>
|
||||||
|
<block>
|
||||||
|
<text />
|
||||||
|
<inline readOnly>
|
||||||
|
read-only <cursor />
|
||||||
|
inline
|
||||||
|
</inline>
|
||||||
|
</block>
|
||||||
|
</editor>
|
||||||
|
)
|
||||||
|
export const output = (
|
||||||
|
<editor>
|
||||||
|
<block>
|
||||||
|
<text>
|
||||||
|
<cursor />
|
||||||
|
</text>
|
||||||
|
</block>
|
||||||
|
</editor>
|
||||||
|
)
|
@@ -0,0 +1,29 @@
|
|||||||
|
/** @jsx jsx */
|
||||||
|
import { Transforms } from 'slate'
|
||||||
|
import { jsx } from '../../..'
|
||||||
|
|
||||||
|
export const input = (
|
||||||
|
<editor>
|
||||||
|
<block>
|
||||||
|
<text />
|
||||||
|
<inline readOnly>
|
||||||
|
read-only <cursor />
|
||||||
|
inline
|
||||||
|
</inline>
|
||||||
|
</block>
|
||||||
|
</editor>
|
||||||
|
)
|
||||||
|
export const run = editor => {
|
||||||
|
Transforms.insertText(editor, 'x')
|
||||||
|
}
|
||||||
|
export const output = (
|
||||||
|
<editor>
|
||||||
|
<block>
|
||||||
|
<text />
|
||||||
|
<inline readOnly>
|
||||||
|
read-only <cursor />
|
||||||
|
inline
|
||||||
|
</inline>
|
||||||
|
</block>
|
||||||
|
</editor>
|
||||||
|
)
|
@@ -14,4 +14,32 @@ test.describe('Inlines example', () => {
|
|||||||
.innerText()
|
.innerText()
|
||||||
).toContain('hyperlink')
|
).toContain('hyperlink')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('arrow keys skip over read-only inline', async ({ page }) => {
|
||||||
|
const badge = await page.locator('text=Approved >> xpath=../../..')
|
||||||
|
|
||||||
|
// Put cursor after the badge
|
||||||
|
await badge.evaluate(badgeElement => {
|
||||||
|
const range = document.createRange()
|
||||||
|
range.setStartAfter(badgeElement, 0)
|
||||||
|
range.setEndAfter(badgeElement, 0)
|
||||||
|
const selection = window.getSelection()
|
||||||
|
selection.removeAllRanges()
|
||||||
|
selection.addRange(range)
|
||||||
|
})
|
||||||
|
|
||||||
|
const getSelectionContainerText = () =>
|
||||||
|
page.evaluate(() => {
|
||||||
|
const selection = window.getSelection()
|
||||||
|
return selection.anchorNode.parentNode.innerText
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(await getSelectionContainerText()).toBe('.')
|
||||||
|
await page.keyboard.press('ArrowLeft')
|
||||||
|
expect(await getSelectionContainerText()).toBe(
|
||||||
|
'! Here is a read-only inline: '
|
||||||
|
)
|
||||||
|
await page.keyboard.press('ArrowRight')
|
||||||
|
expect(await getSelectionContainerText()).toBe('.')
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
3
site/examples/custom-types.d.ts
vendored
3
site/examples/custom-types.d.ts
vendored
@@ -47,6 +47,8 @@ export type LinkElement = { type: 'link'; url: string; children: Descendant[] }
|
|||||||
|
|
||||||
export type ButtonElement = { type: 'button'; children: Descendant[] }
|
export type ButtonElement = { type: 'button'; children: Descendant[] }
|
||||||
|
|
||||||
|
export type BadgeElement = { type: 'badge'; children: Descendant[] }
|
||||||
|
|
||||||
export type ListItemElement = { type: 'list-item'; children: Descendant[] }
|
export type ListItemElement = { type: 'list-item'; children: Descendant[] }
|
||||||
|
|
||||||
export type MentionElement = {
|
export type MentionElement = {
|
||||||
@@ -92,6 +94,7 @@ type CustomElement =
|
|||||||
| ImageElement
|
| ImageElement
|
||||||
| LinkElement
|
| LinkElement
|
||||||
| ButtonElement
|
| ButtonElement
|
||||||
|
| BadgeElement
|
||||||
| ListItemElement
|
| ListItemElement
|
||||||
| MentionElement
|
| MentionElement
|
||||||
| ParagraphElement
|
| ParagraphElement
|
||||||
|
@@ -38,7 +38,14 @@ const initialValue: Descendant[] = [
|
|||||||
children: [{ text: 'editable button' }],
|
children: [{ text: 'editable button' }],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
text: '!',
|
text: '! Here is a read-only inline: ',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'badge',
|
||||||
|
children: [{ text: 'Approved' }],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: '.',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@@ -108,10 +115,22 @@ const InlinesExample = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const withInlines = editor => {
|
const withInlines = editor => {
|
||||||
const { insertData, insertText, isInline } = editor
|
const {
|
||||||
|
insertData,
|
||||||
|
insertText,
|
||||||
|
isInline,
|
||||||
|
isElementReadOnly,
|
||||||
|
isSelectable,
|
||||||
|
} = editor
|
||||||
|
|
||||||
editor.isInline = element =>
|
editor.isInline = element =>
|
||||||
['link', 'button'].includes(element.type) || isInline(element)
|
['link', 'button', 'badge'].includes(element.type) || isInline(element)
|
||||||
|
|
||||||
|
editor.isElementReadOnly = element =>
|
||||||
|
element.type === 'badge' || isElementReadOnly(element)
|
||||||
|
|
||||||
|
editor.isSelectable = element =>
|
||||||
|
element.type !== 'badge' && isSelectable(element)
|
||||||
|
|
||||||
editor.insertText = text => {
|
editor.insertText = text => {
|
||||||
if (text && isUrl(text)) {
|
if (text && isUrl(text)) {
|
||||||
@@ -283,6 +302,30 @@ const EditableButtonComponent = ({ attributes, children }) => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const BadgeComponent = ({ attributes, children, element }) => {
|
||||||
|
const selected = useSelected()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
{...attributes}
|
||||||
|
contentEditable={false}
|
||||||
|
className={css`
|
||||||
|
background-color: green;
|
||||||
|
color: white;
|
||||||
|
padding: 2px 6px;
|
||||||
|
border-radius: 2px;
|
||||||
|
font-size: 0.9em;
|
||||||
|
${selected && 'box-shadow: 0 0 0 3px #ddd;'}
|
||||||
|
`}
|
||||||
|
data-playwright-selected={selected}
|
||||||
|
>
|
||||||
|
<InlineChromiumBugfix />
|
||||||
|
{children}
|
||||||
|
<InlineChromiumBugfix />
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
const Element = props => {
|
const Element = props => {
|
||||||
const { attributes, children, element } = props
|
const { attributes, children, element } = props
|
||||||
switch (element.type) {
|
switch (element.type) {
|
||||||
@@ -290,6 +333,8 @@ const Element = props => {
|
|||||||
return <LinkComponent {...props} />
|
return <LinkComponent {...props} />
|
||||||
case 'button':
|
case 'button':
|
||||||
return <EditableButtonComponent {...props} />
|
return <EditableButtonComponent {...props} />
|
||||||
|
case 'badge':
|
||||||
|
return <BadgeComponent {...props} />
|
||||||
default:
|
default:
|
||||||
return <p {...attributes}>{children}</p>
|
return <p {...attributes}>{children}</p>
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user