1+ import LoadingImage from "./blots/image.js" ;
2+
3+ class ImageUploader {
4+ constructor ( quill , options ) {
5+ this . quill = quill ;
6+ this . options = options ;
7+ this . range = null ;
8+ this . placeholderDelta = null ;
9+
10+ if ( typeof this . options . upload !== "function" )
11+ console . warn (
12+ "[Missing config] upload function that returns a promise is required"
13+ ) ;
14+
15+ var toolbar = this . quill . getModule ( "toolbar" ) ;
16+ if ( toolbar ) {
17+ toolbar . addHandler ( "image" , this . selectLocalImage . bind ( this ) ) ;
18+ }
19+
20+ this . handleDrop = this . handleDrop . bind ( this ) ;
21+ this . handlePaste = this . handlePaste . bind ( this ) ;
22+
23+ this . quill . root . addEventListener ( "drop" , this . handleDrop , false ) ;
24+ this . quill . root . addEventListener ( "paste" , this . handlePaste , false ) ;
25+ }
26+
27+ selectLocalImage ( ) {
28+ this . quill . focus ( ) ;
29+ this . range = this . quill . getSelection ( ) ;
30+ this . fileHolder = document . createElement ( "input" ) ;
31+ this . fileHolder . setAttribute ( "type" , "file" ) ;
32+ this . fileHolder . setAttribute ( "accept" , "image/*" ) ;
33+ this . fileHolder . setAttribute ( "style" , "visibility:hidden" ) ;
34+
35+ this . fileHolder . onchange = this . fileChanged . bind ( this ) ;
36+
37+ document . body . appendChild ( this . fileHolder ) ;
38+
39+ this . fileHolder . click ( ) ;
40+
41+ window . requestAnimationFrame ( ( ) => {
42+ document . body . removeChild ( this . fileHolder ) ;
43+ } ) ;
44+ }
45+
46+ handleDrop ( evt ) {
47+ if (
48+ evt . dataTransfer &&
49+ evt . dataTransfer . files &&
50+ evt . dataTransfer . files . length
51+ ) {
52+ evt . stopPropagation ( ) ;
53+ evt . preventDefault ( ) ;
54+ if ( document . caretRangeFromPoint ) {
55+ const selection = document . getSelection ( ) ;
56+ const range = document . caretRangeFromPoint ( evt . clientX , evt . clientY ) ;
57+ if ( selection && range ) {
58+ selection . setBaseAndExtent (
59+ range . startContainer ,
60+ range . startOffset ,
61+ range . startContainer ,
62+ range . startOffset
63+ ) ;
64+ }
65+ } else {
66+ const selection = document . getSelection ( ) ;
67+ const range = document . caretPositionFromPoint ( evt . clientX , evt . clientY ) ;
68+ if ( selection && range ) {
69+ selection . setBaseAndExtent (
70+ range . offsetNode ,
71+ range . offset ,
72+ range . offsetNode ,
73+ range . offset
74+ ) ;
75+ }
76+ }
77+
78+ this . quill . focus ( ) ;
79+ this . range = this . quill . getSelection ( ) ;
80+ let file = evt . dataTransfer . files [ 0 ] ;
81+
82+ setTimeout ( ( ) => {
83+ this . quill . focus ( ) ;
84+ this . range = this . quill . getSelection ( ) ;
85+ this . readAndUploadFile ( file ) ;
86+ } , 0 ) ;
87+ }
88+ }
89+
90+ handlePaste ( evt ) {
91+ let clipboard = evt . clipboardData || window . clipboardData ;
92+
93+ // IE 11 is .files other browsers are .items
94+ if ( clipboard && ( clipboard . items || clipboard . files ) ) {
95+ let items = clipboard . items || clipboard . files ;
96+ const IMAGE_MIME_REGEX = / ^ i m a g e \/ ( j p e ? g | g i f | p n g | s v g | w e b p ) $ / i;
97+
98+ for ( let i = 0 ; i < items . length ; i ++ ) {
99+ if ( IMAGE_MIME_REGEX . test ( items [ i ] . type ) ) {
100+ let file = items [ i ] . getAsFile ? items [ i ] . getAsFile ( ) : items [ i ] ;
101+
102+ if ( file ) {
103+ this . quill . focus ( ) ;
104+ this . range = this . quill . getSelection ( ) ;
105+ evt . preventDefault ( ) ;
106+ setTimeout ( ( ) => {
107+ this . quill . focus ( ) ;
108+ this . range = this . quill . getSelection ( ) ;
109+ this . readAndUploadFile ( file ) ;
110+ } , 0 ) ;
111+ }
112+ }
113+ }
114+ }
115+ }
116+
117+ readAndUploadFile ( file ) {
118+ let isUploadReject = false ;
119+
120+ const fileReader = new FileReader ( ) ;
121+
122+ fileReader . addEventListener (
123+ "load" ,
124+ ( ) => {
125+ if ( ! isUploadReject ) {
126+ let base64ImageSrc = fileReader . result ;
127+ this . insertBase64Image ( base64ImageSrc ) ;
128+ }
129+ } ,
130+ false
131+ ) ;
132+
133+ if ( file ) {
134+ fileReader . readAsDataURL ( file ) ;
135+ }
136+
137+ this . options . upload ( file ) . then (
138+ ( imageUrl ) => {
139+ this . insertToEditor ( imageUrl ) ;
140+ } ,
141+ ( error ) => {
142+ isUploadReject = true ;
143+ this . removeBase64Image ( ) ;
144+ console . warn ( error ) ;
145+ }
146+ ) ;
147+ }
148+
149+ fileChanged ( ) {
150+ const file = this . fileHolder . files [ 0 ] ;
151+ this . readAndUploadFile ( file ) ;
152+ }
153+
154+ insertBase64Image ( url ) {
155+ const range = this . range ;
156+
157+ this . placeholderDelta = this . quill . insertEmbed (
158+ range . index ,
159+ LoadingImage . blotName ,
160+ `${ url } ` ,
161+ "user"
162+ ) ;
163+ }
164+
165+ insertToEditor ( url ) {
166+ const range = this . range ;
167+
168+ const lengthToDelete = this . calculatePlaceholderInsertLength ( ) ;
169+
170+ // Delete the placeholder image
171+ this . quill . deleteText ( range . index , lengthToDelete , "user" ) ;
172+ // Insert the server saved image
173+ this . quill . insertEmbed ( range . index , "image" , `${ url } ` , "user" ) ;
174+
175+ range . index ++ ;
176+ this . quill . setSelection ( range , "user" ) ;
177+ }
178+
179+ // The length of the insert delta from insertBase64Image can vary depending on what part of the line the insert occurs
180+ calculatePlaceholderInsertLength ( ) {
181+ return this . placeholderDelta . ops . reduce ( ( accumulator , deltaOperation ) => {
182+ if ( deltaOperation . hasOwnProperty ( 'insert' ) )
183+ accumulator ++ ;
184+
185+ return accumulator ;
186+ } , 0 ) ;
187+ }
188+
189+ removeBase64Image ( ) {
190+ const range = this . range ;
191+ const lengthToDelete = this . calculatePlaceholderInsertLength ( ) ;
192+
193+ this . quill . deleteText ( range . index , lengthToDelete , "user" ) ;
194+ }
195+ }
196+
197+ window . ImageUploader = ImageUploader ;
198+ export default ImageUploader ;
0 commit comments