From b52e08b0eafdcf1c77439e282c9dc89a4c72fbf1 Mon Sep 17 00:00:00 2001 From: Joe Anderson Date: Mon, 3 Apr 2023 19:02:26 +0100 Subject: [PATCH] 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` --- .changeset/spicy-phones-nail.md | 7 ++ .changeset/wicked-weeks-kick.md | 5 ++ packages/slate-history/test/index.js | 10 ++- packages/slate/src/create-editor.ts | 2 + packages/slate/src/interfaces/editor.ts | 81 ++++++++++++++++++- packages/slate/src/transforms/selection.ts | 2 +- packages/slate/src/transforms/text.ts | 28 ++++--- packages/slate/test/index.js | 8 +- .../Editor/after/non-selectable-inline.tsx | 22 +++++ .../Editor/before/non-selectable-inline.tsx | 22 +++++ .../nodes/ignore-non-selectable/block.tsx | 19 +++++ .../nodes/ignore-non-selectable/inline.tsx | 20 +++++ .../positions/ignore-non-selectable/block.tsx | 15 ++++ .../ignore-non-selectable/inline.tsx | 28 +++++++ .../interfaces/Element/isElement/editor.tsx | 2 + .../Element/isElementList/full-editor.tsx | 2 + .../read-only-inline-after-reverse.tsx | 25 ++++++ .../voids-false/read-only-inline-within.tsx | 27 +++++++ .../voids-false/read-only-inline.tsx | 29 +++++++ .../integration/examples/inlines.test.ts | 28 +++++++ site/examples/custom-types.d.ts | 3 + site/examples/inlines.tsx | 51 +++++++++++- 22 files changed, 416 insertions(+), 20 deletions(-) create mode 100644 .changeset/spicy-phones-nail.md create mode 100644 .changeset/wicked-weeks-kick.md create mode 100644 packages/slate/test/interfaces/Editor/after/non-selectable-inline.tsx create mode 100644 packages/slate/test/interfaces/Editor/before/non-selectable-inline.tsx create mode 100644 packages/slate/test/interfaces/Editor/nodes/ignore-non-selectable/block.tsx create mode 100644 packages/slate/test/interfaces/Editor/nodes/ignore-non-selectable/inline.tsx create mode 100644 packages/slate/test/interfaces/Editor/positions/ignore-non-selectable/block.tsx create mode 100644 packages/slate/test/interfaces/Editor/positions/ignore-non-selectable/inline.tsx create mode 100644 packages/slate/test/transforms/delete/voids-false/read-only-inline-after-reverse.tsx create mode 100644 packages/slate/test/transforms/delete/voids-false/read-only-inline-within.tsx create mode 100644 packages/slate/test/transforms/insertText/voids-false/read-only-inline.tsx diff --git a/.changeset/spicy-phones-nail.md b/.changeset/spicy-phones-nail.md new file mode 100644 index 0000000000..454e19a34e --- /dev/null +++ b/.changeset/spicy-phones-nail.md @@ -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 diff --git a/.changeset/wicked-weeks-kick.md b/.changeset/wicked-weeks-kick.md new file mode 100644 index 0000000000..83e880af22 --- /dev/null +++ b/.changeset/wicked-weeks-kick.md @@ -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. diff --git a/packages/slate-history/test/index.js b/packages/slate-history/test/index.js index bb8d16c55f..b70d34504e 100644 --- a/packages/slate-history/test/index.js +++ b/packages/slate-history/test/index.js @@ -30,7 +30,7 @@ export const jsx = createHyperscript({ }) const withTest = editor => { - const { isInline, isVoid } = editor + const { isInline, isVoid, isElementReadOnly, isSelectable } = editor editor.isInline = element => { return element.inline === true ? true : isInline(element) @@ -40,5 +40,13 @@ const withTest = editor => { 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 } diff --git a/packages/slate/src/create-editor.ts b/packages/slate/src/create-editor.ts index 76bdbb4b25..d6b09efd77 100644 --- a/packages/slate/src/create-editor.ts +++ b/packages/slate/src/create-editor.ts @@ -25,7 +25,9 @@ export const createEditor = (): Editor => { operations: [], selection: null, marks: null, + isElementReadOnly: () => false, isInline: () => false, + isSelectable: () => true, isVoid: () => false, markableVoid: () => false, onChange: () => {}, diff --git a/packages/slate/src/interfaces/editor.ts b/packages/slate/src/interfaces/editor.ts index aa85593da3..fc0e9e33e7 100644 --- a/packages/slate/src/interfaces/editor.ts +++ b/packages/slate/src/interfaces/editor.ts @@ -59,7 +59,9 @@ export interface BaseEditor { marks: EditorMarks | null // 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 @@ -116,6 +118,12 @@ export interface EditorDirectedDeletionOptions { unit?: TextUnit } +export interface EditorElementReadOnlyOptions { + at?: Location + mode?: MaximizeMode + voids?: boolean +} + export interface EditorFragmentDeletionOptions { direction?: TextDirection } @@ -151,6 +159,7 @@ export interface EditorNodesOptions { universal?: boolean reverse?: boolean voids?: boolean + ignoreNonSelectable?: boolean } export interface EditorNormalizeOptions { @@ -185,6 +194,7 @@ export interface EditorPositionsOptions { unit?: TextUnitAdjustment reverse?: boolean voids?: boolean + ignoreNonSelectable?: boolean } export interface EditorPreviousOptions { @@ -241,6 +251,10 @@ export interface EditorInterface { options?: EditorFragmentDeletionOptions ) => void edges: (editor: Editor, at: Location) => [Point, Point] + elementReadOnly: ( + editor: Editor, + options?: EditorElementReadOnlyOptions + ) => NodeEntry | undefined end: (editor: Editor, at: Location) => Point first: (editor: Editor, at: Location) => NodeEntry fragment: (editor: Editor, at: Location) => Descendant[] @@ -257,9 +271,11 @@ export interface EditorInterface { isEditor: (value: any) => value is Editor isEnd: (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 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 @@ -509,6 +525,20 @@ export const Editor: EditorInterface = { 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 | undefined { + return Editor.above(editor, { + ...options, + match: n => Element.isElement(n) && Editor.isElementReadOnly(editor, n), + }) + }, + /** * Get the end point of a location. */ @@ -646,7 +676,9 @@ export const Editor: EditorInterface = { typeof value.insertFragment === 'function' && typeof value.insertNode === 'function' && 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' && @@ -701,6 +733,14 @@ export const Editor: EditorInterface = { 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. */ @@ -710,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. */ @@ -923,6 +971,7 @@ export const Editor: EditorInterface = { universal = false, reverse = false, voids = false, + ignoreNonSelectable = false, } = options let { match } = options @@ -951,14 +1000,32 @@ export const Editor: EditorInterface = { reverse, from, to, - pass: ([n]) => - voids ? false : Element.isElement(n) && Editor.isVoid(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[] = [] let hit: NodeEntry | 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. @@ -1304,6 +1371,7 @@ export const Editor: EditorInterface = { unit = 'offset', reverse = false, voids = false, + ignoreNonSelectable = false, } = options if (!at) { @@ -1343,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 */ @@ -1351,7 +1424,7 @@ export const Editor: EditorInterface = { // Void nodes are a special case, so by default we will always // yield their first point. If the `voids` option is set to true, // 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) continue } diff --git a/packages/slate/src/transforms/selection.ts b/packages/slate/src/transforms/selection.ts index 4816f807da..8acc5b97df 100644 --- a/packages/slate/src/transforms/selection.ts +++ b/packages/slate/src/transforms/selection.ts @@ -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 = {} if (edge == null || edge === 'anchor') { diff --git a/packages/slate/src/transforms/text.ts b/packages/slate/src/transforms/text.ts index 4833b04c9e..04a497775c 100644 --- a/packages/slate/src/transforms/text.ts +++ b/packages/slate/src/transforms/text.ts @@ -120,15 +120,17 @@ export const TextTransforms: TextTransforms = { const isAcrossBlocks = startBlock && endBlock && !Path.equals(startBlock[1], endBlock[1]) const isSingleText = Path.equals(start.path, end.path) - const startVoid = voids + const startNonEditable = voids ? null - : Editor.void(editor, { at: start, mode: 'highest' }) - const endVoid = voids + : Editor.void(editor, { at: start, mode: 'highest' }) ?? + Editor.elementReadOnly(editor, { at: start, mode: 'highest' }) + const endNonEditable = voids ? 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 (startVoid) { + if (startNonEditable) { const before = Editor.before(editor, start) if ( @@ -140,7 +142,7 @@ export const TextTransforms: TextTransforms = { } } - if (endVoid) { + if (endNonEditable) { const after = Editor.after(editor, end) if (after && endBlock && Path.isAncestor(endBlock[1], after.path)) { @@ -161,7 +163,10 @@ export const TextTransforms: TextTransforms = { } 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)) ) { matches.push(entry) @@ -175,7 +180,7 @@ export const TextTransforms: TextTransforms = { let removedText = '' - if (!isSingleText && !startVoid) { + if (!isSingleText && !startNonEditable) { const point = startRef.current! const [node] = Editor.leaf(editor, point) const { path } = point @@ -193,7 +198,7 @@ export const TextTransforms: TextTransforms = { .filter((r): r is Path => r !== null) .forEach(p => Transforms.removeNodes(editor, { at: p, voids })) - if (!endVoid) { + if (!endNonEditable) { const point = endRef.current! const [node] = Editor.leaf(editor, 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 } diff --git a/packages/slate/test/index.js b/packages/slate/test/index.js index 21cf698b8e..92cf7181f0 100644 --- a/packages/slate/test/index.js +++ b/packages/slate/test/index.js @@ -47,13 +47,19 @@ describe('slate', () => { }) }) const withTest = editor => { - const { isInline, isVoid } = editor + const { isInline, isVoid, isElementReadOnly, isSelectable } = editor editor.isInline = element => { return element.inline === true ? true : isInline(element) } editor.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 } export const jsx = createHyperscript({ diff --git a/packages/slate/test/interfaces/Editor/after/non-selectable-inline.tsx b/packages/slate/test/interfaces/Editor/after/non-selectable-inline.tsx new file mode 100644 index 0000000000..1f24e74148 --- /dev/null +++ b/packages/slate/test/interfaces/Editor/after/non-selectable-inline.tsx @@ -0,0 +1,22 @@ +/** @jsx jsx */ + +import { Editor } from 'slate' +import { jsx } from '../../..' + +export const input = ( + + + onetwothree + + +) + +export const test = editor => { + return Editor.after( + editor, + { path: [0, 0], offset: 3 }, + { ignoreNonSelectable: true } + ) +} + +export const output = { path: [0, 2], offset: 0 } diff --git a/packages/slate/test/interfaces/Editor/before/non-selectable-inline.tsx b/packages/slate/test/interfaces/Editor/before/non-selectable-inline.tsx new file mode 100644 index 0000000000..5403a0c88d --- /dev/null +++ b/packages/slate/test/interfaces/Editor/before/non-selectable-inline.tsx @@ -0,0 +1,22 @@ +/** @jsx jsx */ + +import { Editor } from 'slate' +import { jsx } from '../../..' + +export const input = ( + + + onetwothree + + +) + +export const test = editor => { + return Editor.before( + editor, + { path: [0, 2], offset: 0 }, + { ignoreNonSelectable: true } + ) +} + +export const output = { path: [0, 0], offset: 3 } diff --git a/packages/slate/test/interfaces/Editor/nodes/ignore-non-selectable/block.tsx b/packages/slate/test/interfaces/Editor/nodes/ignore-non-selectable/block.tsx new file mode 100644 index 0000000000..abf21606f8 --- /dev/null +++ b/packages/slate/test/interfaces/Editor/nodes/ignore-non-selectable/block.tsx @@ -0,0 +1,19 @@ +/** @jsx jsx */ +import { Editor, Text } from 'slate' +import { jsx } from '../../../..' + +export const input = ( + + one + +) +export const test = editor => { + return Array.from( + Editor.nodes(editor, { + at: [], + match: Text.isText, + ignoreNonSelectable: true, + }) + ) +} +export const output = [] diff --git a/packages/slate/test/interfaces/Editor/nodes/ignore-non-selectable/inline.tsx b/packages/slate/test/interfaces/Editor/nodes/ignore-non-selectable/inline.tsx new file mode 100644 index 0000000000..f81a9d9210 --- /dev/null +++ b/packages/slate/test/interfaces/Editor/nodes/ignore-non-selectable/inline.tsx @@ -0,0 +1,20 @@ +/** @jsx jsx */ +import { Editor, Text } from 'slate' +import { jsx } from '../../../..' + +export const input = ( + + + onetwothree + + +) +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]], +] diff --git a/packages/slate/test/interfaces/Editor/positions/ignore-non-selectable/block.tsx b/packages/slate/test/interfaces/Editor/positions/ignore-non-selectable/block.tsx new file mode 100644 index 0000000000..8cbfa9846d --- /dev/null +++ b/packages/slate/test/interfaces/Editor/positions/ignore-non-selectable/block.tsx @@ -0,0 +1,15 @@ +/** @jsx jsx */ +import { Editor } from 'slate' +import { jsx } from '../../../..' + +export const input = ( + + one + +) +export const test = editor => { + return Array.from( + Editor.positions(editor, { at: [], ignoreNonSelectable: true }) + ) +} +export const output = [] diff --git a/packages/slate/test/interfaces/Editor/positions/ignore-non-selectable/inline.tsx b/packages/slate/test/interfaces/Editor/positions/ignore-non-selectable/inline.tsx new file mode 100644 index 0000000000..9e334245e4 --- /dev/null +++ b/packages/slate/test/interfaces/Editor/positions/ignore-non-selectable/inline.tsx @@ -0,0 +1,28 @@ +/** @jsx jsx */ +import { Editor } from 'slate' +import { jsx } from '../../../..' + +export const input = ( + + + onetwothree + + +) +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 }, +] diff --git a/packages/slate/test/interfaces/Element/isElement/editor.tsx b/packages/slate/test/interfaces/Element/isElement/editor.tsx index 443ddb5df5..480dda9fc2 100644 --- a/packages/slate/test/interfaces/Element/isElement/editor.tsx +++ b/packages/slate/test/interfaces/Element/isElement/editor.tsx @@ -15,7 +15,9 @@ export const input = { insertFragment() {}, insertNode() {}, insertText() {}, + isElementReadOnly() {}, isInline() {}, + isSelectable() {}, isVoid() {}, normalizeNode() {}, onChange() {}, diff --git a/packages/slate/test/interfaces/Element/isElementList/full-editor.tsx b/packages/slate/test/interfaces/Element/isElementList/full-editor.tsx index cebb36e17f..a79498300f 100644 --- a/packages/slate/test/interfaces/Element/isElementList/full-editor.tsx +++ b/packages/slate/test/interfaces/Element/isElementList/full-editor.tsx @@ -16,7 +16,9 @@ export const input = [ insertFragment() {}, insertNode() {}, insertText() {}, + isElementReadOnly() {}, isInline() {}, + isSelectable() {}, isVoid() {}, normalizeNode() {}, onChange() {}, diff --git a/packages/slate/test/transforms/delete/voids-false/read-only-inline-after-reverse.tsx b/packages/slate/test/transforms/delete/voids-false/read-only-inline-after-reverse.tsx new file mode 100644 index 0000000000..214a829164 --- /dev/null +++ b/packages/slate/test/transforms/delete/voids-false/read-only-inline-after-reverse.tsx @@ -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 = ( + + + + read-only inline + + + +) +export const output = ( + + + + + + + +) diff --git a/packages/slate/test/transforms/delete/voids-false/read-only-inline-within.tsx b/packages/slate/test/transforms/delete/voids-false/read-only-inline-within.tsx new file mode 100644 index 0000000000..353f32daf1 --- /dev/null +++ b/packages/slate/test/transforms/delete/voids-false/read-only-inline-within.tsx @@ -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 = ( + + + + + read-only + inline + + + +) +export const output = ( + + + + + + + +) diff --git a/packages/slate/test/transforms/insertText/voids-false/read-only-inline.tsx b/packages/slate/test/transforms/insertText/voids-false/read-only-inline.tsx new file mode 100644 index 0000000000..df42ed7ec9 --- /dev/null +++ b/packages/slate/test/transforms/insertText/voids-false/read-only-inline.tsx @@ -0,0 +1,29 @@ +/** @jsx jsx */ +import { Transforms } from 'slate' +import { jsx } from '../../..' + +export const input = ( + + + + + read-only + inline + + + +) +export const run = editor => { + Transforms.insertText(editor, 'x') +} +export const output = ( + + + + + read-only + inline + + + +) diff --git a/playwright/integration/examples/inlines.test.ts b/playwright/integration/examples/inlines.test.ts index 0abed686cc..abd6f9d9aa 100644 --- a/playwright/integration/examples/inlines.test.ts +++ b/playwright/integration/examples/inlines.test.ts @@ -14,4 +14,32 @@ test.describe('Inlines example', () => { .innerText() ).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('.') + }) }) diff --git a/site/examples/custom-types.d.ts b/site/examples/custom-types.d.ts index 3d99b33a6e..6ffce57b4b 100644 --- a/site/examples/custom-types.d.ts +++ b/site/examples/custom-types.d.ts @@ -47,6 +47,8 @@ export type LinkElement = { type: 'link'; url: string; 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 MentionElement = { @@ -92,6 +94,7 @@ type CustomElement = | ImageElement | LinkElement | ButtonElement + | BadgeElement | ListItemElement | MentionElement | ParagraphElement diff --git a/site/examples/inlines.tsx b/site/examples/inlines.tsx index db1d20055a..2f410c73f0 100644 --- a/site/examples/inlines.tsx +++ b/site/examples/inlines.tsx @@ -38,7 +38,14 @@ const initialValue: Descendant[] = [ 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 { insertData, insertText, isInline } = editor + const { + insertData, + insertText, + isInline, + isElementReadOnly, + isSelectable, + } = editor 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 => { if (text && isUrl(text)) { @@ -283,6 +302,30 @@ const EditableButtonComponent = ({ attributes, children }) => { ) } +const BadgeComponent = ({ attributes, children, element }) => { + const selected = useSelected() + + return ( + + + {children} + + + ) +} + const Element = props => { const { attributes, children, element } = props switch (element.type) { @@ -290,6 +333,8 @@ const Element = props => { return case 'button': return + case 'badge': + return default: return

{children}

}