1- import { htmlEscape } from 'escape-goat' ;
2- import { POST } from '../../modules/fetch.js' ;
31import { imageInfo } from '../../utils/image.js' ;
4- import { getPastedContent , replaceTextareaSelection } from '../../utils/dom.js' ;
2+ import { replaceTextareaSelection } from '../../utils/dom.js' ;
53import { 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+ } ) ;
1727}
1828
1929class TextareaEditor {
@@ -82,48 +92,25 @@ class CodeMirrorEditor {
8292 }
8393}
8494
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 ) {
9196 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 ...)` ;
96101
97- const placeholder = `` ;
98102 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} ) ) ;
124105 }
125106}
126107
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+
127114function handleClipboardText ( textarea , e , { text, isShiftDown} ) {
128115 // pasting with "shift" means "paste as original content" in most applications
129116 if ( isShiftDown ) return ; // let the browser handle it
@@ -139,16 +126,37 @@ function handleClipboardText(textarea, e, {text, isShiftDown}) {
139126 // else, let the browser handle it
140127}
141128
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 ) ;
143143 easyMDE . codemirror . on ( 'paste' , ( _ , e ) => {
144144 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 ) ;
148156 } ) ;
149157}
150158
151- export function initTextareaPaste ( textarea , dropzone ) {
159+ export function initTextareaUpload ( textarea , dropzoneEl ) {
152160 let isShiftDown = false ;
153161 textarea . addEventListener ( 'keydown' , ( e ) => {
154162 if ( e . shiftKey ) isShiftDown = true ;
@@ -159,9 +167,17 @@ export function initTextareaPaste(textarea, dropzone) {
159167 textarea . addEventListener ( 'paste' , ( e ) => {
160168 const { images, text} = getPastedContent ( e ) ;
161169 if ( images . length ) {
162- handleClipboardImages ( new TextareaEditor ( textarea ) , dropzone , images , e ) ;
170+ handleUploadFiles ( new TextareaEditor ( textarea ) , dropzoneEl , images , e ) ;
163171 } else if ( text ) {
164172 handleClipboardText ( textarea , e , { text, isShiftDown} ) ;
165173 }
166174 } ) ;
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+ } ) ;
167183}
0 commit comments