Skip to content

Commit 78579ed

Browse files
authored
feat(richtext-lexical): various UX and performance improvements (#6467)
2 parents 73d0b20 + 7bcb4ba commit 78579ed

File tree

9 files changed

+234
-127
lines changed

9 files changed

+234
-127
lines changed

packages/email-nodemailer/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ It abstracts all of the email functionality that was in Payload by default in 2.
99
## Installation
1010

1111
```sh
12-
pnpm add @payloadcms/email-nodemailer` nodemailer
12+
pnpm add @payloadcms/email-nodemailer nodemailer
1313
```
1414

1515
## Usage

packages/email-resend/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ This adapter allows you to send emails using the [Resend](https://resend.com) RE
55
## Installation
66

77
```sh
8-
pnpm add @payloadcms/email-resend`
8+
pnpm add @payloadcms/email-resend
99
```
1010

1111
## Usage

packages/richtext-lexical/src/field/features/link/plugins/floatingLinkEditor/LinkEditor/index.tsx

Lines changed: 41 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ export function LinkEditor({ anchorElem }: { anchorElem: HTMLElement }): React.R
5252

5353
const [stateData, setStateData] = useState<{} | (LinkFields & { text: string })>({})
5454

55-
const { closeModal, toggleModal } = useModal()
55+
const { closeModal, isModalOpen, toggleModal } = useModal()
5656
const editDepth = useEditDepth()
5757
const [isLink, setIsLink] = useState(false)
5858
const [selectedNodes, setSelectedNodes] = useState<LexicalNode[]>([])
@@ -319,48 +319,50 @@ export function LinkEditor({ anchorElem }: { anchorElem: HTMLElement }): React.R
319319
)}
320320
</div>
321321
</div>
322-
<LinkDrawer
323-
drawerSlug={drawerSlug}
324-
handleModalSubmit={(fields: FormState, data: Data) => {
325-
closeModal(drawerSlug)
322+
{isModalOpen(drawerSlug) && (
323+
<LinkDrawer
324+
drawerSlug={drawerSlug}
325+
handleModalSubmit={(fields: FormState, data: Data) => {
326+
closeModal(drawerSlug)
326327

327-
const newLinkPayload = data as LinkFields & { text: string }
328+
const newLinkPayload = data as LinkFields & { text: string }
328329

329-
const bareLinkFields: LinkFields = {
330-
...newLinkPayload,
331-
}
332-
delete bareLinkFields.text
333-
334-
// See: https://github.com/facebook/lexical/pull/5536. This updates autolink nodes to link nodes whenever a change was made (which is good!).
335-
editor.update(() => {
336-
const selection = $getSelection()
337-
let linkParent = null
338-
if ($isRangeSelection(selection)) {
339-
linkParent = getSelectedNode(selection).getParent()
340-
} else {
341-
if (selectedNodes.length) {
342-
linkParent = selectedNodes[0].getParent()
343-
}
330+
const bareLinkFields: LinkFields = {
331+
...newLinkPayload,
344332
}
333+
delete bareLinkFields.text
334+
335+
// See: https://github.com/facebook/lexical/pull/5536. This updates autolink nodes to link nodes whenever a change was made (which is good!).
336+
editor.update(() => {
337+
const selection = $getSelection()
338+
let linkParent = null
339+
if ($isRangeSelection(selection)) {
340+
linkParent = getSelectedNode(selection).getParent()
341+
} else {
342+
if (selectedNodes.length) {
343+
linkParent = selectedNodes[0].getParent()
344+
}
345+
}
345346

346-
if (linkParent && $isAutoLinkNode(linkParent)) {
347-
const linkNode = $createLinkNode({
348-
fields: bareLinkFields,
349-
})
350-
linkParent.replace(linkNode, true)
351-
}
352-
})
353-
354-
// Needs to happen AFTER a potential auto link => link node conversion, as otherwise, the updated text to display may be lost due to
355-
// it being applied to the auto link node instead of the link node.
356-
editor.dispatchCommand(TOGGLE_LINK_COMMAND, {
357-
fields: bareLinkFields,
358-
selectedNodes,
359-
text: newLinkPayload.text,
360-
})
361-
}}
362-
stateData={stateData}
363-
/>
347+
if (linkParent && $isAutoLinkNode(linkParent)) {
348+
const linkNode = $createLinkNode({
349+
fields: bareLinkFields,
350+
})
351+
linkParent.replace(linkNode, true)
352+
}
353+
})
354+
355+
// Needs to happen AFTER a potential auto link => link node conversion, as otherwise, the updated text to display may be lost due to
356+
// it being applied to the auto link node instead of the link node.
357+
editor.dispatchCommand(TOGGLE_LINK_COMMAND, {
358+
fields: bareLinkFields,
359+
selectedNodes,
360+
text: newLinkPayload.text,
361+
})
362+
}}
363+
stateData={stateData}
364+
/>
365+
)}
364366
</React.Fragment>
365367
)
366368
}

packages/richtext-lexical/src/field/lexical/plugins/handles/AddBlockHandlePlugin/index.tsx

Lines changed: 7 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -113,33 +113,14 @@ function useAddBlockHandle(
113113

114114
useEffect(() => {
115115
if (menuRef.current && hoveredElement?.node) {
116-
editor.getEditorState().read(() => {
117-
// Check if blockNode is an empty text node
118-
let isEmptyParagraph = true
119-
if (
120-
hoveredElement.node.getType() !== 'paragraph' ||
121-
hoveredElement.node.getTextContent() !== ''
122-
) {
123-
isEmptyParagraph = false
124-
}
125-
126-
setHandlePosition(
127-
hoveredElement?.elem,
128-
menuRef.current,
129-
anchorElem,
130-
isEmptyParagraph
131-
? blockHandleHorizontalOffset
132-
: blockHandleHorizontalOffset - (editorConfig?.admin?.hideGutter ? 20 : 0),
133-
)
134-
})
116+
setHandlePosition(
117+
hoveredElement?.elem,
118+
menuRef.current,
119+
anchorElem,
120+
blockHandleHorizontalOffset,
121+
)
135122
}
136-
}, [
137-
anchorElem,
138-
hoveredElement,
139-
editor,
140-
blockHandleHorizontalOffset,
141-
editorConfig?.admin?.hideGutter,
142-
])
123+
}, [anchorElem, hoveredElement, blockHandleHorizontalOffset])
143124

144125
const handleAddClick = useCallback(
145126
(event) => {

packages/richtext-lexical/src/field/lexical/plugins/handles/DraggableBlockPlugin/index.scss

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@
4545
.draggable-block-target-line {
4646
pointer-events: none;
4747
background: var(--theme-elevation-200);
48-
border: 1px solid var(--theme-elevation-650);
48+
//border: 1px solid var(--theme-elevation-650);
4949
border-radius: 4px;
5050
height: 50px;
5151
position: absolute;

packages/richtext-lexical/src/field/lexical/plugins/handles/DraggableBlockPlugin/index.tsx

Lines changed: 77 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -57,11 +57,10 @@ function hideTargetLine(
5757
}
5858
if (lastTargetBlockElem) {
5959
lastTargetBlockElem.style.opacity = ''
60-
lastTargetBlockElem.style.transform = ''
6160
// Delete marginBottom and marginTop values we set
6261
lastTargetBlockElem.style.marginBottom = ''
6362
lastTargetBlockElem.style.marginTop = ''
64-
//lastTargetBlockElem.style.border = 'none'
63+
//lastTargetBlock.style.border = 'none'
6564
}
6665
}
6766

@@ -77,10 +76,15 @@ function useDraggableBlockMenu(
7776
const debugHighlightRef = useRef<HTMLDivElement>(null)
7877
const isDraggingBlockRef = useRef<boolean>(false)
7978
const [draggableBlockElem, setDraggableBlockElem] = useState<HTMLElement | null>(null)
80-
const [lastTargetBlockElem, setLastTargetBlockElem] = useState<HTMLElement | null>(null)
79+
const [lastTargetBlock, setLastTargetBlock] = useState<{
80+
boundingBox?: DOMRect
81+
elem: HTMLElement | null
82+
isBelow: boolean
83+
}>(null)
84+
8185
const { editorConfig } = useEditorConfigContext()
8286

83-
const blockHandleHorizontalOffset = editorConfig?.admin?.hideGutter ? -24 : -8
87+
const blockHandleHorizontalOffset = editorConfig?.admin?.hideGutter ? -44 : -8
8488

8589
useEffect(() => {
8690
/**
@@ -211,11 +215,15 @@ function useDraggableBlockMenu(
211215
}
212216

213217
if (draggableBlockElem !== targetBlockElem) {
214-
setTargetLine(
215-
blockHandleHorizontalOffset,
218+
const { isBelow, willStayInSamePosition } = setTargetLine(
219+
editorConfig?.admin?.hideGutter ? '0px' : '3rem',
220+
blockHandleHorizontalOffset +
221+
(editorConfig?.admin?.hideGutter
222+
? menuRef?.current?.getBoundingClientRect()?.width ?? 0
223+
: -menuRef?.current?.getBoundingClientRect()?.width ?? 0),
216224
targetLineElem,
217225
targetBlockElem,
218-
lastTargetBlockElem,
226+
lastTargetBlock,
219227
pageY,
220228
anchorElem,
221229
event,
@@ -227,12 +235,23 @@ function useDraggableBlockMenu(
227235
// Calling preventDefault() adds the green plus icon to the cursor,
228236
// indicating that the drop is allowed.
229237
event.preventDefault()
238+
239+
if (!willStayInSamePosition) {
240+
setLastTargetBlock({
241+
boundingBox: targetBlockElem.getBoundingClientRect(),
242+
elem: targetBlockElem,
243+
isBelow,
244+
})
245+
}
230246
} else {
231-
hideTargetLine(targetLineElem, lastTargetBlockElem)
247+
hideTargetLine(targetLineElem, lastTargetBlock?.elem)
248+
setLastTargetBlock({
249+
boundingBox: targetBlockElem.getBoundingClientRect(),
250+
elem: targetBlockElem,
251+
isBelow: false,
252+
})
232253
}
233254

234-
setLastTargetBlockElem(targetBlockElem)
235-
236255
return true
237256
}
238257

@@ -313,6 +332,51 @@ function useDraggableBlockMenu(
313332
if (draggableBlockElem !== null) {
314333
setDraggableBlockElem(null)
315334
}
335+
336+
// find all previous elements with lexical-block-highlighter class and remove them
337+
const allPrevHighlighters = document.querySelectorAll('.lexical-block-highlighter')
338+
allPrevHighlighters.forEach((highlighter) => {
339+
highlighter.remove()
340+
})
341+
342+
const newInsertedElem = editor.getElementByKey(draggedNode.getKey())
343+
setTimeout(() => {
344+
// add new temp html element to newInsertedElem with the same height and width and the class block-selected
345+
// to highlight the new inserted element
346+
const newInsertedElemRect = newInsertedElem.getBoundingClientRect()
347+
348+
const highlightElem = document.createElement('div')
349+
highlightElem.className = 'lexical-block-highlighter'
350+
351+
// if html data-theme is dark, set the highlighter color to white
352+
if (document.documentElement.getAttribute('data-theme') === 'dark') {
353+
highlightElem.style.backgroundColor = 'white'
354+
} else {
355+
highlightElem.style.backgroundColor = 'black'
356+
}
357+
358+
highlightElem.style.transition = 'opacity 0.1s ease-in-out'
359+
highlightElem.style.zIndex = '1'
360+
highlightElem.style.pointerEvents = 'none'
361+
highlightElem.style.boxSizing = 'border-box'
362+
highlightElem.style.borderRadius = '4px'
363+
highlightElem.style.position = 'absolute'
364+
document.body.appendChild(highlightElem)
365+
366+
highlightElem.style.opacity = '0.1'
367+
368+
highlightElem.style.height = `${newInsertedElemRect.height + 8}px`
369+
highlightElem.style.width = `${newInsertedElemRect.width + 8}px`
370+
highlightElem.style.top = `${newInsertedElemRect.top + window.scrollY - 4}px`
371+
highlightElem.style.left = `${newInsertedElemRect.left - 4}px`
372+
373+
setTimeout(() => {
374+
highlightElem.style.opacity = '0'
375+
setTimeout(() => {
376+
highlightElem.remove()
377+
}, 1000)
378+
}, 3000)
379+
}, 120)
316380
})
317381

318382
return true
@@ -332,8 +396,9 @@ function useDraggableBlockMenu(
332396
blockHandleHorizontalOffset,
333397
anchorElem,
334398
editor,
335-
lastTargetBlockElem,
399+
lastTargetBlock,
336400
draggableBlockElem,
401+
editorConfig?.admin?.hideGutter,
337402
])
338403

339404
function onDragStart(event: ReactDragEvent<HTMLDivElement>): void {
@@ -355,7 +420,7 @@ function useDraggableBlockMenu(
355420

356421
function onDragEnd(): void {
357422
isDraggingBlockRef.current = false
358-
hideTargetLine(targetLineRef.current, lastTargetBlockElem)
423+
hideTargetLine(targetLineRef.current, lastTargetBlock?.elem)
359424
}
360425

361426
return createPortal(

0 commit comments

Comments
 (0)