1
- import { htmlEscape } from 'escape-goat' ;
2
- import { POST } from '../../modules/fetch.js' ;
3
1
import { imageInfo } from '../../utils/image.js' ;
4
- import { getPastedContent , replaceTextareaSelection } from '../../utils/dom.js' ;
2
+ import { replaceTextareaSelection } from '../../utils/dom.js' ;
5
3
import { isUrl } from '../../utils/url.js' ;
6
-
7
- async function uploadFile ( file , uploadUrl ) {
8
- const formData = new FormData ( ) ;
9
- formData . append ( 'file' , file , file . name ) ;
10
-
11
- const res = await POST ( uploadUrl , { data : formData } ) ;
12
- return await res . json ( ) ;
13
- }
14
-
15
- export function triggerEditorContentChanged ( target ) {
16
- target . dispatchEvent ( new CustomEvent ( 'ce-editor-content-changed' , { bubbles : true } ) ) ;
4
+ import { triggerEditorContentChanged } from './EditorMarkdown.js' ;
5
+ import {
6
+ DropzoneCustomEventRemovedFile ,
7
+ DropzoneCustomEventUploadDone ,
8
+ generateMarkdownLinkForAttachment ,
9
+ } from '../dropzone.js' ;
10
+
11
+ let uploadIdCounter = 0 ;
12
+
13
+ function uploadFile ( dropzoneEl , file ) {
14
+ return new Promise ( ( resolve ) => {
15
+ const curUploadId = uploadIdCounter ++ ;
16
+ file . _giteaUploadId = curUploadId ;
17
+ const dropzoneInst = dropzoneEl . dropzone ;
18
+ const onUploadDone = ( { file} ) => {
19
+ if ( file . _giteaUploadId === curUploadId ) {
20
+ dropzoneInst . off ( DropzoneCustomEventUploadDone , onUploadDone ) ;
21
+ resolve ( ) ;
22
+ }
23
+ } ;
24
+ dropzoneInst . on ( DropzoneCustomEventUploadDone , onUploadDone ) ;
25
+ dropzoneInst . handleFiles ( [ file ] ) ;
26
+ } ) ;
17
27
}
18
28
19
29
class TextareaEditor {
@@ -82,48 +92,25 @@ class CodeMirrorEditor {
82
92
}
83
93
}
84
94
85
- async function handleClipboardImages ( editor , dropzone , images , e ) {
86
- const uploadUrl = dropzone . getAttribute ( 'data-upload-url' ) ;
87
- const filesContainer = dropzone . querySelector ( '.files' ) ;
88
-
89
- if ( ! dropzone || ! uploadUrl || ! filesContainer || ! images . length ) return ;
90
-
95
+ async function handleUploadFiles ( editor , dropzoneEl , files , e ) {
91
96
e . preventDefault ( ) ;
92
- e . stopPropagation ( ) ;
93
-
94
- for ( const img of images ) {
95
- const name = img . name . slice ( 0 , img . name . lastIndexOf ( '.' ) ) ;
97
+ for ( const file of files ) {
98
+ const name = file . name . slice ( 0 , file . name . lastIndexOf ( '.' ) ) ;
99
+ const { width , dppx } = await imageInfo ( file ) ;
100
+ const placeholder = `[ ${ name } ](uploading ...)` ;
96
101
97
- const placeholder = `` ;
98
102
editor . insertPlaceholder ( placeholder ) ;
99
-
100
- const { uuid} = await uploadFile ( img , uploadUrl ) ;
101
- const { width, dppx} = await imageInfo ( img ) ;
102
-
103
- let text ;
104
- if ( width > 0 && dppx > 1 ) {
105
- // Scale down images from HiDPI monitors. This uses the <img> tag because it's the only
106
- // method to change image size in Markdown that is supported by all implementations.
107
- // Make the image link relative to the repo path, then the final URL is "/sub-path/owner/repo/attachments/{uuid}"
108
- const url = `attachments/${ uuid } ` ;
109
- text = `<img width="${ Math . round ( width / dppx ) } " alt="${ htmlEscape ( name ) } " src="${ htmlEscape ( url ) } ">` ;
110
- } else {
111
- // Markdown always renders the image with a relative path, so the final URL is "/sub-path/owner/repo/attachments/{uuid}"
112
- // TODO: it should also use relative path for consistency, because absolute is ambiguous for "/sub-path/attachments" or "/attachments"
113
- const url = `/attachments/${ uuid } ` ;
114
- text = `` ;
115
- }
116
- editor . replacePlaceholder ( placeholder , text ) ;
117
-
118
- const input = document . createElement ( 'input' ) ;
119
- input . setAttribute ( 'name' , 'files' ) ;
120
- input . setAttribute ( 'type' , 'hidden' ) ;
121
- input . setAttribute ( 'id' , uuid ) ;
122
- input . value = uuid ;
123
- filesContainer . append ( input ) ;
103
+ await uploadFile ( dropzoneEl , file ) ; // the "file" will get its "uuid" during the upload
104
+ editor . replacePlaceholder ( placeholder , generateMarkdownLinkForAttachment ( file , { width, dppx} ) ) ;
124
105
}
125
106
}
126
107
108
+ export function removeAttachmentLinksFromMarkdown ( text , fileUuid ) {
109
+ text = text . replace ( new RegExp ( `!?\\[([^\\]]+)\\]\\(/?attachments/${ fileUuid } \\)` , 'g' ) , '' ) ;
110
+ text = text . replace ( new RegExp ( `<img[^>]+src="/?attachments/${ fileUuid } "[^>]*>` , 'g' ) , '' ) ;
111
+ return text ;
112
+ }
113
+
127
114
function handleClipboardText ( textarea , e , { text, isShiftDown} ) {
128
115
// pasting with "shift" means "paste as original content" in most applications
129
116
if ( isShiftDown ) return ; // let the browser handle it
@@ -139,16 +126,37 @@ function handleClipboardText(textarea, e, {text, isShiftDown}) {
139
126
// else, let the browser handle it
140
127
}
141
128
142
- export function initEasyMDEPaste ( easyMDE , dropzone ) {
129
+ // extract text and images from "paste" event
130
+ function getPastedContent ( e ) {
131
+ const images = [ ] ;
132
+ for ( const item of e . clipboardData ?. items ?? [ ] ) {
133
+ if ( item . type ?. startsWith ( 'image/' ) ) {
134
+ images . push ( item . getAsFile ( ) ) ;
135
+ }
136
+ }
137
+ const text = e . clipboardData ?. getData ?. ( 'text' ) ?? '' ;
138
+ return { text, images} ;
139
+ }
140
+
141
+ export function initEasyMDEPaste ( easyMDE , dropzoneEl ) {
142
+ const editor = new CodeMirrorEditor ( easyMDE . codemirror ) ;
143
143
easyMDE . codemirror . on ( 'paste' , ( _ , e ) => {
144
144
const { images} = getPastedContent ( e ) ;
145
- if ( images . length ) {
146
- handleClipboardImages ( new CodeMirrorEditor ( easyMDE . codemirror ) , dropzone , images , e ) ;
147
- }
145
+ if ( ! images . length ) return ;
146
+ handleUploadFiles ( editor , dropzoneEl , images , e ) ;
147
+ } ) ;
148
+ easyMDE . codemirror . on ( 'drop' , ( _ , e ) => {
149
+ if ( ! e . dataTransfer . files . length ) return ;
150
+ handleUploadFiles ( editor , dropzoneEl , e . dataTransfer . files , e ) ;
151
+ } ) ;
152
+ dropzoneEl . dropzone . on ( DropzoneCustomEventRemovedFile , ( { fileUuid} ) => {
153
+ const oldText = easyMDE . codemirror . getValue ( ) ;
154
+ const newText = removeAttachmentLinksFromMarkdown ( oldText , fileUuid ) ;
155
+ if ( oldText !== newText ) easyMDE . codemirror . setValue ( newText ) ;
148
156
} ) ;
149
157
}
150
158
151
- export function initTextareaPaste ( textarea , dropzone ) {
159
+ export function initTextareaUpload ( textarea , dropzoneEl ) {
152
160
let isShiftDown = false ;
153
161
textarea . addEventListener ( 'keydown' , ( e ) => {
154
162
if ( e . shiftKey ) isShiftDown = true ;
@@ -159,9 +167,17 @@ export function initTextareaPaste(textarea, dropzone) {
159
167
textarea . addEventListener ( 'paste' , ( e ) => {
160
168
const { images, text} = getPastedContent ( e ) ;
161
169
if ( images . length ) {
162
- handleClipboardImages ( new TextareaEditor ( textarea ) , dropzone , images , e ) ;
170
+ handleUploadFiles ( new TextareaEditor ( textarea ) , dropzoneEl , images , e ) ;
163
171
} else if ( text ) {
164
172
handleClipboardText ( textarea , e , { text, isShiftDown} ) ;
165
173
}
166
174
} ) ;
175
+ textarea . addEventListener ( 'drop' , ( e ) => {
176
+ if ( ! e . dataTransfer . files . length ) return ;
177
+ handleUploadFiles ( new TextareaEditor ( textarea ) , dropzoneEl , e . dataTransfer . files , e ) ;
178
+ } ) ;
179
+ dropzoneEl . dropzone . on ( DropzoneCustomEventRemovedFile , ( { fileUuid} ) => {
180
+ const newText = removeAttachmentLinksFromMarkdown ( textarea . value , fileUuid ) ;
181
+ if ( textarea . value !== newText ) textarea . value = newText ;
182
+ } ) ;
167
183
}
0 commit comments