Skip to content

Commit 35885db

Browse files
GermanJablokendelljoseph
authored andcommitted
feat(richtext-lexical): make decoratorNodes and blocks selectable. Centralize selection and deletion logic (#10735)
- Blocks can now be selected (only inline blocks were possible before). - Any DecoratorNode that users create will have the necessary logic out of the box so that they are selected with a click and deleted with backspace/delete. - By having the code for selecting and deleting centralized, a lot of repetitive code was eliminated - More performant code due to the use of event delegation. There is only one listener, previously there was one for each decoratorNode. - Heuristics to exclude scenarios where you don't want to select the node: if it is inside the DecoratorNode, but is also inside a button, input, textarea, contentEditable, .react-select, .code-editor or .no-select-decorator. That last one was added as a means of opt-out. - Fix #10634 Note: arrow navigation will be introduced in a later PR. https://github.com/user-attachments/assets/92f91cad-4f70-4f72-a36f-c68afbe33c0d
1 parent 4e182ab commit 35885db

File tree

17 files changed

+231
-323
lines changed

17 files changed

+231
-323
lines changed

packages/richtext-lexical/src/features/blocks/client/component/index.scss

+4
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@
66
z-index: 1;
77
}
88

9+
[data-lexical-decorator='true']:has(.lexical-block) {
10+
width: auto;
11+
}
12+
913
.lexical-block-not-found {
1014
color: var(--theme-error-500);
1115
font-size: 1.1rem;

packages/richtext-lexical/src/features/blocks/client/componentInline/index.scss

+2-7
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
@layer payload-default {
44
.inline-block-container {
55
display: inline-block;
6+
margin-right: base(0.2);
7+
margin-left: base(0.2);
68
}
79

810
.inline-block.inline-block-not-found {
@@ -22,8 +24,6 @@
2224
border-radius: $style-radius-s;
2325
max-width: calc(var(--base) * 15);
2426
font-family: var(--font-body);
25-
margin-right: base(0.2);
26-
margin-left: base(0.2);
2727

2828
&::selection {
2929
background: transparent;
@@ -38,11 +38,6 @@
3838
overflow: hidden;
3939
}
4040

41-
&--selected {
42-
background: var(--theme-success-100);
43-
outline: 1px solid var(--theme-success-400);
44-
}
45-
4641
&__editButton.btn {
4742
margin: 0;
4843
}

packages/richtext-lexical/src/features/blocks/client/componentInline/index.tsx

+3-69
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,6 @@ const baseClass = 'inline-block'
66
import type { BlocksFieldClient, Data, FormState } from 'payload'
77

88
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
9-
import { useLexicalNodeSelection } from '@lexical/react/useLexicalNodeSelection'
10-
import { mergeRegister } from '@lexical/utils'
119
import { getTranslation } from '@payloadcms/translations'
1210
import {
1311
Button,
@@ -24,15 +22,7 @@ import {
2422
useTranslation,
2523
} from '@payloadcms/ui'
2624
import { abortAndIgnore } from '@payloadcms/ui/shared'
27-
import {
28-
$getNodeByKey,
29-
$getSelection,
30-
$isNodeSelection,
31-
CLICK_COMMAND,
32-
COMMAND_PRIORITY_LOW,
33-
KEY_BACKSPACE_COMMAND,
34-
KEY_DELETE_COMMAND,
35-
} from 'lexical'
25+
import { $getNodeByKey } from 'lexical'
3626

3727
import './index.scss'
3828

@@ -116,7 +106,6 @@ export const InlineBlockComponent: React.FC<Props> = (props) => {
116106
const { toggleDrawer } = useLexicalDrawer(drawerSlug, true)
117107

118108
const inlineBlockElemElemRef = useRef<HTMLDivElement | null>(null)
119-
const [isSelected, setSelected, clearSelection] = useLexicalNodeSelection(nodeKey)
120109
const { id, collectionSlug, getDocPreferences, globalSlug } = useDocumentInfo()
121110

122111
const componentMapRenderedBlockPath = `${schemaPath}.lexical_internal_feature.blocks.lexical_inline_blocks.${formData.blockType}`
@@ -153,56 +142,6 @@ export const InlineBlockComponent: React.FC<Props> = (props) => {
153142
})
154143
}, [editor, nodeKey])
155144

156-
const $onDelete = useCallback(
157-
(event: KeyboardEvent) => {
158-
const deleteSelection = $getSelection()
159-
if (isSelected && $isNodeSelection(deleteSelection)) {
160-
event.preventDefault()
161-
editor.update(() => {
162-
deleteSelection.getNodes().forEach((node) => {
163-
if ($isInlineBlockNode(node)) {
164-
node.remove()
165-
}
166-
})
167-
})
168-
}
169-
return false
170-
},
171-
[editor, isSelected],
172-
)
173-
const onClick = useCallback(
174-
(payload: MouseEvent) => {
175-
const event = payload
176-
// Check if inlineBlockElemElemRef.target or anything WITHIN inlineBlockElemElemRef.target was clicked
177-
if (
178-
event.target === inlineBlockElemElemRef.current ||
179-
inlineBlockElemElemRef.current?.contains(event.target as Node)
180-
) {
181-
if (event.shiftKey) {
182-
setSelected(!isSelected)
183-
} else {
184-
if (!isSelected) {
185-
clearSelection()
186-
setSelected(true)
187-
}
188-
}
189-
return true
190-
}
191-
192-
return false
193-
},
194-
[isSelected, setSelected, clearSelection],
195-
)
196-
197-
useEffect(() => {
198-
return mergeRegister(
199-
editor.registerCommand<MouseEvent>(CLICK_COMMAND, onClick, COMMAND_PRIORITY_LOW),
200-
201-
editor.registerCommand(KEY_DELETE_COMMAND, $onDelete, COMMAND_PRIORITY_LOW),
202-
editor.registerCommand(KEY_BACKSPACE_COMMAND, $onDelete, COMMAND_PRIORITY_LOW),
203-
)
204-
}, [clearSelection, editor, isSelected, nodeKey, $onDelete, setSelected, onClick])
205-
206145
const blockDisplayName = clientBlock?.labels?.singular
207146
? getTranslation(clientBlock?.labels.singular, i18n)
208147
: clientBlock?.slug
@@ -362,20 +301,15 @@ export const InlineBlockComponent: React.FC<Props> = (props) => {
362301
() =>
363302
({ children, className }: { children: React.ReactNode; className?: string }) => (
364303
<div
365-
className={[
366-
baseClass,
367-
baseClass + '-' + formData.blockType,
368-
isSelected && `${baseClass}--selected`,
369-
className,
370-
]
304+
className={[baseClass, baseClass + '-' + formData.blockType, className]
371305
.filter(Boolean)
372306
.join(' ')}
373307
ref={inlineBlockElemElemRef}
374308
>
375309
{children}
376310
</div>
377311
),
378-
[formData.blockType, isSelected],
312+
[formData.blockType],
379313
)
380314

381315
const Label = useMemo(() => {

packages/richtext-lexical/src/features/horizontalRule/client/component/index.tsx

-85
This file was deleted.

packages/richtext-lexical/src/features/horizontalRule/client/nodes/HorizontalRuleNode.tsx

+2-8
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,6 @@ import type { SerializedHorizontalRuleNode } from '../../server/nodes/Horizontal
88

99
import { HorizontalRuleServerNode } from '../../server/nodes/HorizontalRuleNode.js'
1010

11-
const HorizontalRuleComponent = React.lazy(() =>
12-
import('../../client/component/index.js').then((module) => ({
13-
default: module.HorizontalRuleComponent,
14-
})),
15-
)
16-
1711
export class HorizontalRuleNode extends HorizontalRuleServerNode {
1812
static override clone(node: HorizontalRuleServerNode): HorizontalRuleServerNode {
1913
return super.clone(node)
@@ -33,8 +27,8 @@ export class HorizontalRuleNode extends HorizontalRuleServerNode {
3327
/**
3428
* Allows you to render a React component within whatever createDOM returns.
3529
*/
36-
override decorate(): React.ReactElement {
37-
return <HorizontalRuleComponent nodeKey={this.__key} />
30+
override decorate() {
31+
return null
3832
}
3933

4034
override exportJSON(): SerializedLexicalNode {

packages/richtext-lexical/src/features/horizontalRule/client/plugin/index.scss

+1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
@layer payload-default {
44
.LexicalEditorTheme__hr {
5+
width: auto !important;
56
padding: 2px 2px;
67
border: none;
78
margin: 1rem 0;

packages/richtext-lexical/src/features/relationship/client/components/RelationshipComponent.tsx

+4-73
Original file line numberDiff line numberDiff line change
@@ -2,27 +2,16 @@
22
import type { ElementFormatType } from 'lexical'
33

44
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext.js'
5-
import { useLexicalNodeSelection } from '@lexical/react/useLexicalNodeSelection.js'
6-
import { mergeRegister } from '@lexical/utils'
75
import { getTranslation } from '@payloadcms/translations'
86
import { Button, useConfig, usePayloadAPI, useTranslation } from '@payloadcms/ui'
9-
import {
10-
$getNodeByKey,
11-
$getSelection,
12-
$isNodeSelection,
13-
CLICK_COMMAND,
14-
COMMAND_PRIORITY_LOW,
15-
KEY_BACKSPACE_COMMAND,
16-
KEY_DELETE_COMMAND,
17-
} from 'lexical'
18-
import React, { useCallback, useEffect, useReducer, useRef, useState } from 'react'
7+
import { $getNodeByKey } from 'lexical'
8+
import React, { useCallback, useReducer, useRef, useState } from 'react'
199

2010
import type { RelationshipData } from '../../server/nodes/RelationshipNode.js'
2111

2212
import { useEditorConfigContext } from '../../../../lexical/config/client/EditorConfigProvider.js'
2313
import { useLexicalDocumentDrawer } from '../../../../utilities/fieldsDrawer/useLexicalDocumentDrawer.js'
2414
import { INSERT_RELATIONSHIP_WITH_DRAWER_COMMAND } from '../drawer/commands.js'
25-
import { $isRelationshipNode } from '../nodes/RelationshipNode.js'
2615
import './index.scss'
2716

2817
const baseClass = 'lexical-relationship'
@@ -53,7 +42,6 @@ const Component: React.FC<Props> = (props) => {
5342
const relationshipElemRef = useRef<HTMLDivElement | null>(null)
5443

5544
const [editor] = useLexicalComposerContext()
56-
const [isSelected, setSelected, clearSelection] = useLexicalNodeSelection(nodeKey!)
5745
const {
5846
fieldProps: { readOnly },
5947
} = useEditorConfigContext()
@@ -65,9 +53,7 @@ const Component: React.FC<Props> = (props) => {
6553
getEntityConfig,
6654
} = useConfig()
6755

68-
const [relatedCollection, setRelatedCollection] = useState(() =>
69-
getEntityConfig({ collectionSlug: relationTo }),
70-
)
56+
const [relatedCollection] = useState(() => getEntityConfig({ collectionSlug: relationTo }))
7157

7258
const { i18n, t } = useTranslation()
7359
const [cacheBust, dispatchCacheBust] = useReducer((state) => state + 1, 0)
@@ -97,63 +83,8 @@ const Component: React.FC<Props> = (props) => {
9783
dispatchCacheBust()
9884
}, [cacheBust, setParams, closeDocumentDrawer])
9985

100-
const $onDelete = useCallback(
101-
(payload: KeyboardEvent) => {
102-
const deleteSelection = $getSelection()
103-
if (isSelected && $isNodeSelection(deleteSelection)) {
104-
const event: KeyboardEvent = payload
105-
event.preventDefault()
106-
editor.update(() => {
107-
deleteSelection.getNodes().forEach((node) => {
108-
if ($isRelationshipNode(node)) {
109-
node.remove()
110-
}
111-
})
112-
})
113-
}
114-
return false
115-
},
116-
[editor, isSelected],
117-
)
118-
const onClick = useCallback(
119-
(payload: MouseEvent) => {
120-
const event = payload
121-
// Check if relationshipElemRef.target or anything WITHIN relationshipElemRef.target was clicked
122-
if (
123-
event.target === relationshipElemRef.current ||
124-
relationshipElemRef.current?.contains(event.target as Node)
125-
) {
126-
if (event.shiftKey) {
127-
setSelected(!isSelected)
128-
} else {
129-
if (!isSelected) {
130-
clearSelection()
131-
setSelected(true)
132-
}
133-
}
134-
return true
135-
}
136-
137-
return false
138-
},
139-
[isSelected, setSelected, clearSelection],
140-
)
141-
142-
useEffect(() => {
143-
return mergeRegister(
144-
editor.registerCommand<MouseEvent>(CLICK_COMMAND, onClick, COMMAND_PRIORITY_LOW),
145-
146-
editor.registerCommand(KEY_DELETE_COMMAND, $onDelete, COMMAND_PRIORITY_LOW),
147-
editor.registerCommand(KEY_BACKSPACE_COMMAND, $onDelete, COMMAND_PRIORITY_LOW),
148-
)
149-
}, [clearSelection, editor, isSelected, nodeKey, $onDelete, setSelected, onClick])
150-
15186
return (
152-
<div
153-
className={[baseClass, isSelected && `${baseClass}--selected`].filter(Boolean).join(' ')}
154-
contentEditable={false}
155-
ref={relationshipElemRef}
156-
>
87+
<div className={baseClass} contentEditable={false} ref={relationshipElemRef}>
15788
<div className={`${baseClass}__wrap`}>
15889
<p className={`${baseClass}__label`}>
15990
{t('fields:labelRelationship', {

packages/richtext-lexical/src/features/relationship/client/components/index.scss

-5
Original file line numberDiff line numberDiff line change
@@ -43,11 +43,6 @@
4343
overflow: hidden;
4444
}
4545

46-
&--selected {
47-
box-shadow: $focus-box-shadow;
48-
outline: none;
49-
}
50-
5146
&__doc-drawer-toggler {
5247
text-decoration: underline;
5348
pointer-events: all;

0 commit comments

Comments
 (0)