Skip to content

Commit 59414bd

Browse files
authored
feat(richtext-lexical): support copy & pasting and drag & dopping files/images into the editor (#13868)
This PR adds support for inserting images into the rich text editor via both **copy & paste** and **drag & drop**, whether from local files or image DOM nodes. It leverages the bulk uploads UI to provide a smooth workflow for: - Selecting the target collection - Filling in any required fields defined on the uploads collection - Uploading multiple images at once This significantly improves the UX for adding images to rich text, and also works seamlessly when pasting images from external editors like Google Docs or Microsoft Word. Test pre-release: `3.57.0-internal.801ab5a` ## Showcase - drag & drop images from computer https://github.com/user-attachments/assets/c558c034-d2e4-40d8-9035-c0681389fb7b ## Showcase - copy & paste images from computer https://github.com/user-attachments/assets/f36faf94-5274-4151-b141-00aff2b0efa4 ## Showcase - copy & paste image DOM nodes https://github.com/user-attachments/assets/2839ed0f-3f28-4e8d-8b47-01d0cb947edc --- - To see the specific tasks where the Asana app for GitHub is being used, see below: - https://app.asana.com/0/0/1211217132290841
1 parent 062c1d7 commit 59414bd

File tree

23 files changed

+1059
-158
lines changed

23 files changed

+1059
-158
lines changed

.github/workflows/main.yml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -287,6 +287,10 @@ jobs:
287287
- folders
288288
- hooks
289289
- lexical__collections___LexicalFullyFeatured
290+
- lexical__collections___LexicalFullyFeatured__db
291+
- lexical__collections__LexicalHeadingFeature
292+
- lexical__collections__LexicalJSXConverter
293+
- lexical__collections__LexicalLinkFeature
290294
- lexical__collections__OnDemandForm
291295
- lexical__collections__Lexical__e2e__main
292296
- lexical__collections__Lexical__e2e__blocks
@@ -427,6 +431,10 @@ jobs:
427431
- folders
428432
- hooks
429433
- lexical__collections___LexicalFullyFeatured
434+
- lexical__collections___LexicalFullyFeatured__db
435+
- lexical__collections__LexicalHeadingFeature
436+
- lexical__collections__LexicalJSXConverter
437+
- lexical__collections__LexicalLinkFeature
430438
- lexical__collections__OnDemandForm
431439
- lexical__collections__Lexical__e2e__main
432440
- lexical__collections__Lexical__e2e__blocks
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
'use client'
2+
3+
import { ShimmerEffect } from '@payloadcms/ui'
4+
5+
import '../index.scss'
6+
7+
export const PendingUploadComponent = (): React.ReactNode => {
8+
return (
9+
<div className={'lexical-upload'}>
10+
<ShimmerEffect height={'95px'} width={'203px'} />
11+
</div>
12+
)
13+
}

packages/richtext-lexical/src/features/upload/client/nodes/UploadNode.tsx

Lines changed: 16 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,53 +1,25 @@
11
'use client'
2-
import type { SerializedDecoratorBlockNode } from '@lexical/react/LexicalDecoratorBlockNode.js'
3-
import type { DOMConversionMap, DOMConversionOutput, LexicalNode, Spread } from 'lexical'
2+
import type { DOMConversionMap, LexicalNode } from 'lexical'
43
import type { JSX } from 'react'
54

65
import ObjectID from 'bson-objectid'
76
import { $applyNodeReplacement } from 'lexical'
87
import * as React from 'react'
98

10-
import type { UploadData } from '../../server/nodes/UploadNode.js'
9+
import type {
10+
Internal_UploadData,
11+
SerializedUploadNode,
12+
UploadData,
13+
} from '../../server/nodes/UploadNode.js'
1114

12-
import { isGoogleDocCheckboxImg, UploadServerNode } from '../../server/nodes/UploadNode.js'
15+
import { $convertUploadElement } from '../../server/nodes/conversions.js'
16+
import { UploadServerNode } from '../../server/nodes/UploadNode.js'
17+
import { PendingUploadComponent } from '../component/pending/index.js'
1318

1419
const RawUploadComponent = React.lazy(() =>
1520
import('../../client/component/index.js').then((module) => ({ default: module.UploadComponent })),
1621
)
1722

18-
function $convertUploadElement(domNode: HTMLImageElement): DOMConversionOutput | null {
19-
if (
20-
domNode.hasAttribute('data-lexical-upload-relation-to') &&
21-
domNode.hasAttribute('data-lexical-upload-id')
22-
) {
23-
const id = domNode.getAttribute('data-lexical-upload-id')
24-
const relationTo = domNode.getAttribute('data-lexical-upload-relation-to')
25-
26-
if (id != null && relationTo != null) {
27-
const node = $createUploadNode({
28-
data: {
29-
fields: {},
30-
relationTo,
31-
value: id,
32-
},
33-
})
34-
return { node }
35-
}
36-
}
37-
const img = domNode
38-
if (img.src.startsWith('file:///') || isGoogleDocCheckboxImg(img)) {
39-
return null
40-
}
41-
// TODO: Auto-upload functionality here!
42-
//}
43-
return null
44-
}
45-
46-
export type SerializedUploadNode = {
47-
children?: never // required so that our typed editor state doesn't automatically add children
48-
type: 'upload'
49-
} & Spread<UploadData, SerializedDecoratorBlockNode>
50-
5123
export class UploadNode extends UploadServerNode {
5224
static override clone(node: UploadServerNode): UploadServerNode {
5325
return super.clone(node)
@@ -60,7 +32,7 @@ export class UploadNode extends UploadServerNode {
6032
static override importDOM(): DOMConversionMap<HTMLImageElement> {
6133
return {
6234
img: (node) => ({
63-
conversion: $convertUploadElement,
35+
conversion: (domNode) => $convertUploadElement(domNode, $createUploadNode),
6436
priority: 0,
6537
}),
6638
}
@@ -75,9 +47,10 @@ export class UploadNode extends UploadServerNode {
7547
serializedNode.version = 3
7648
}
7749

78-
const importedData: UploadData = {
50+
const importedData: Internal_UploadData = {
7951
id: serializedNode.id,
8052
fields: serializedNode.fields,
53+
pending: (serializedNode as Internal_UploadData).pending,
8154
relationTo: serializedNode.relationTo,
8255
value: serializedNode.value,
8356
}
@@ -89,6 +62,9 @@ export class UploadNode extends UploadServerNode {
8962
}
9063

9164
override decorate(): JSX.Element {
65+
if ((this.__data as Internal_UploadData).pending) {
66+
return <PendingUploadComponent />
67+
}
9268
return <RawUploadComponent data={this.__data} nodeKey={this.getKey()} />
9369
}
9470

@@ -105,6 +81,7 @@ export function $createUploadNode({
10581
if (!data?.id) {
10682
data.id = new ObjectID.default().toHexString()
10783
}
84+
10885
return $applyNodeReplacement(new UploadNode({ data: data as UploadData }))
10986
}
11087

0 commit comments

Comments
 (0)