1- import { $getRoot , LexicalEditor } from "lexical" ;
1+ import { $createTextNode , $ getRoot, LexicalEditor } from "lexical" ;
22import { HtmlEditorController } from "../../HtmlEditorController" ;
3- import { HtmlEditorExtension , LexicalConfigNode , OptionalCallback } from "../types" ;
3+ import {
4+ HtmlEditorExtension ,
5+ LexicalConfigNode ,
6+ OptionalCallback ,
7+ } from "../types" ;
48import { ImageConverter } from "./ImageConverter" ;
59import { $createImageNode , ImageNode } from "./ImageNode" ;
610
7- export class ImageExtension < T extends object = { } > implements HtmlEditorExtension {
11+ export class ImageExtension < T extends object = { } >
12+ implements HtmlEditorExtension
13+ {
14+ name = "ImageExtension" ;
815 constructor ( public imageConverter : ImageConverter < T > ) { }
916
1017 registerExtension ( controller : HtmlEditorController ) : OptionalCallback {
1118 const abortController = new AbortController ( ) ;
1219 const element = controller . editableElement ;
1320
14- if ( ! element ) return ;
15-
16- element . addEventListener ( "dragover" , ( event ) => {
17- event . preventDefault ( ) ;
18- } , { signal : abortController . signal } ) ;
19-
20- element . addEventListener ( "drop" , ( event ) => {
21- event . preventDefault ( ) ;
22- const files = event . dataTransfer ?. files ;
23-
24- if ( ! files ?. length ) return ;
25- this . insertImageNodes ( files , controller . editor , this . imageConverter ) ;
26- } , { signal : abortController . signal } ) ;
27-
28- element . addEventListener ( "paste" , ( event ) => {
29- const files = event . clipboardData ?. files ;
30-
31- if ( ! files ?. length ) return ;
32- event . preventDefault ( ) ;
33- this . insertImageNodes ( files , controller . editor , this . imageConverter ) ;
34- } , { signal : abortController . signal } ) ;
35-
36- const unsubscribeUpdateListener = controller . editor . registerUpdateListener ( ( ) => {
37- if ( ! controller . editor || ! this . imageConverter ) return ;
38- this . replaceImagePlaceholders ( controller ) ;
39- } ) ;
21+ if ( ! element ) return ;
22+
23+ element . addEventListener (
24+ "dragover" ,
25+ ( event ) => {
26+ event . preventDefault ( ) ;
27+ } ,
28+ { signal : abortController . signal }
29+ ) ;
30+
31+ element . addEventListener (
32+ "drop" ,
33+ ( event ) => {
34+ event . preventDefault ( ) ;
35+ const files = event . dataTransfer ?. files ;
36+
37+ if ( ! files ?. length ) return ;
38+ this . insertImageNodes ( files , controller . editor , this . imageConverter ) ;
39+ } ,
40+ { signal : abortController . signal }
41+ ) ;
42+
43+ element . addEventListener (
44+ "paste" ,
45+ ( event ) => {
46+ const files = event . clipboardData ?. files ;
47+
48+ if ( ! files ?. length ) return ;
49+ event . preventDefault ( ) ;
50+ this . insertImageNodes ( files , controller . editor , this . imageConverter ) ;
51+ } ,
52+ { signal : abortController . signal }
53+ ) ;
54+
55+ const unsubscribeUpdateListener = controller . editor . registerUpdateListener (
56+ ( ) => {
57+ if ( ! controller . editor || ! this . imageConverter ) return ;
58+ this . replaceImagePlaceholders ( controller ) ;
59+ }
60+ ) ;
4061
4162 return ( ) => {
4263 abortController . abort ( ) ;
@@ -45,70 +66,93 @@ export class ImageExtension<T extends object = {}> implements HtmlEditorExtensio
4566 }
4667
4768 getNodes ( ) : LexicalConfigNode {
48- return [ ImageNode ]
49-
69+ return [ ImageNode ] ;
5070 }
5171
52- async insertImageNodes ( files : FileList , editor : LexicalEditor , imageConverter : ImageConverter < T > ) : Promise < void > {
53- const uploadPromises = Array . from ( files ) . filter ( file => file . type . startsWith ( "image/" ) ) . map ( file => {
54- try {
55- return imageConverter . uploadData ( file )
56- } catch ( error ) {
57- console . error ( "Image uploade failed." , error )
58- return null ;
59- }
60- } ) ;
61-
62- const uploadedFiles = ( await Promise . all ( uploadPromises ) ) . filter ( v => v !== null ) ;
63- if ( ! uploadedFiles . length ) return ;
64-
72+ async insertImageNodes (
73+ files : FileList ,
74+ editor : LexicalEditor ,
75+ imageConverter : ImageConverter < T >
76+ ) : Promise < void > {
77+ const uploadPromises = Array . from ( files )
78+ . filter ( ( file ) => file . type . startsWith ( "image/" ) )
79+ . map ( ( file ) => {
80+ try {
81+ return imageConverter . uploadData ( file ) ;
82+ } catch ( error ) {
83+ console . error ( "Image uploade failed." , error ) ;
84+ return null ;
85+ }
86+ } ) ;
87+
88+ const uploadedFiles = ( await Promise . all ( uploadPromises ) ) . filter (
89+ ( v ) => v !== null
90+ ) ;
91+ if ( ! uploadedFiles . length ) return ;
92+
6593 editor . update ( ( ) => {
66- uploadedFiles . forEach ( file => {
94+ uploadedFiles . forEach ( ( file ) => {
6795 const imageNode = $createImageNode ( file , imageConverter ) ;
68- $getRoot ( ) . append ( imageNode ) ;
69- } )
96+ $getRoot ( ) . append ( imageNode ) ;
97+ } ) ;
7098 } ) ;
7199 }
72100
73101 replaceImagePlaceholders ( controller : HtmlEditorController ) : void {
74102 const attachments = ( ( ) => {
75103 const value = controller . binding . getValue ( ) ;
76- if ( value )
77- return [ ...value . matchAll ( / d a t a - a t t a c h m e n t - i d = " ( \d + ) " / g) ] . map ( m => m [ 1 ] ) ;
104+ if ( value )
105+ return [ ...value . matchAll ( / d a t a - a t t a c h m e n t - i d = " ( \d + ) " / g) ] . map (
106+ ( m ) => m [ 1 ]
107+ ) ;
78108 return [ ] ;
79109 } ) ( ) ;
80110
81- if ( ! attachments . length ) return ;
82-
83- const editorState = controller . editor . getEditorState ( ) ;
84- let hasUpdatedNodes = false
111+ if ( ! attachments . length ) return ;
112+
113+ const editorState = controller . editor . getEditorState ( ) ;
114+ let hasUpdatedNodes = false ;
115+
85116 controller . editor . update ( ( ) => {
86117 const nodes = Array . from ( editorState . _nodeMap . values ( ) ) ;
87- if ( ! nodes . some ( v => isImagePlaceholderRegex ( v . getTextContent ( ) ) ) ) return ;
88- editorState . _nodeMap . forEach ( ( node ) => {
118+ if ( ! nodes . some ( ( v ) => isImagePlaceholderRegex ( v . getTextContent ( ) ) ) )
119+ return ;
120+ nodes . forEach ( ( node ) => {
121+ if ( node . getType ( ) === "text" ) {
89122 const text = node . getTextContent ( ) ;
90-
91- if ( node . getType ( ) === "text" && isImagePlaceholderRegex ( text ) ) {
92- const attachmentId = extractAttachmentId ( text ) ;
93- if ( attachmentId && ! attachments . includes ( attachmentId ) ) return ;
94- node . replace ( $createImageNode ( { attachmentId } as object , this . imageConverter ) ) ;
123+ const match = text . match ( IMAGE_PLACEHOLDER_REGEX ) ;
124+
125+ if ( match ) {
126+ const before = text . slice ( 0 , match . index ! ) ;
127+ const after = text . slice ( match . index ! + match [ 0 ] . length ) ;
128+ const attachmentId = match [ 1 ] ;
129+
130+ const imageNode = $createImageNode ( { attachmentId } as object , this . imageConverter ) ;
131+
132+ // Replace the text node with the image node
133+ const replaced = node . replace ( imageNode ) ;
134+
135+ // Insert neighbors around the image
136+ if ( before ) replaced . insertBefore ( $createTextNode ( before ) ) ;
137+ if ( after ) replaced . insertAfter ( $createTextNode ( after ) ) ;
138+
95139 hasUpdatedNodes = true ;
96140 }
141+ }
97142 } ) ;
98143 } , { discrete : true } ) ;
99144
100- if ( hasUpdatedNodes ) controller . saveHtml ( ) ;
145+ if ( hasUpdatedNodes ) controller . saveHtml ( ) ;
101146 }
102147}
103148
104- export const IMAGE_PLACEHOLDER_REGEX : RegExp = / ^ \[ I M A G E _ ( \d + ) \] $ / ;
149+ export const IMAGE_PLACEHOLDER_REGEX : RegExp = / \[ I M A G E _ ( \d + ) \] / ;
105150
106151export function extractAttachmentId ( text : string ) : string | null {
107152 const match = text . match ( IMAGE_PLACEHOLDER_REGEX ) ;
108- return match ? match [ 1 ] : null
153+ return match ? match [ 1 ] : null ;
109154}
110155
111156export function isImagePlaceholderRegex ( text : string ) : boolean {
112157 return IMAGE_PLACEHOLDER_REGEX . test ( text ) ;
113158}
114-
0 commit comments