1(function () { 2 3 /** 4 * Handle pasting of files 5 * 6 * @param {ClipboardEvent} e 7 */ 8 function handlePaste(e) { 9 if (!document.getElementById('wiki__text')) return; // only when editing 10 11 const items = (e.clipboardData || e.originalEvent.clipboardData).items; 12 13 // When running prosemirror and pasting into its edit area, check for HTML paste first 14 if ( 15 typeof window.proseMirrorIsActive !== 'undefined' 16 && window.proseMirrorIsActive === true 17 && document.activeElement.tagName === 'DIV' 18 && document.activeElement.classList.contains('ProseMirror-focused') 19 ) { 20 for (let index in items) { 21 const item = items[index]; 22 if (item.kind === 'string' && item.type === 'text/html') { 23 e.preventDefault(); 24 e.stopPropagation(); 25 26 item.getAsString(async html => { 27 html = await processHTML(html); 28 const pm = window.Prosemirror.view; 29 const parser = window.Prosemirror.classes.DOMParser.fromSchema(pm.state.schema); 30 const nodes = parser.parse(html); 31 pm.dispatch(pm.state.tr.replaceSelectionWith(nodes)); 32 } 33 ); 34 35 return; // we found an HTML item, no need to continue 36 } 37 } 38 } 39 40 // if we're still here, handle files 41 for (let index in items) { 42 const item = items[index]; 43 44 if (item.kind === 'file') { 45 const reader = new FileReader(); 46 reader.onload = event => { 47 uploadData(event.target.result); 48 }; 49 reader.readAsDataURL(item.getAsFile()); 50 51 // we had at least one file, prevent default 52 e.preventDefault(); 53 e.stopPropagation(); 54 } 55 } 56 } 57 58 /** 59 * Creates and shows the progress dialog 60 * 61 * @returns {HTMLDivElement} 62 */ 63 function progressDialog() { 64 // create dialog 65 const offset = document.querySelectorAll('.plugin_imagepaste').length * 3; 66 const box = document.createElement('div'); 67 box.className = 'plugin_imagepaste'; 68 box.innerText = LANG.plugins.imgpaste.inprogress; 69 box.style.position = 'fixed'; 70 box.style.top = offset + 'em'; 71 box.style.left = '1em'; 72 document.querySelector('.dokuwiki').append(box); 73 return box; 74 } 75 76 /** 77 * Processes the given HTML and downloads all images 78 * 79 * @param html 80 * @returns {Promise<HTMLDivElement>} 81 */ 82 async function processHTML(html) { 83 const box = progressDialog(); 84 85 const div = document.createElement('div'); 86 div.innerHTML = html; 87 const imgs = Array.from(div.querySelectorAll('img')); 88 await Promise.all(imgs.map(async img => { 89 if (img.src.startsWith(DOKU_BASE)) return; // skip local images 90 if (!img.src.match(/^(https?:\/\/|data:)/i)) return; // we only handle http(s) and data URLs 91 92 try { 93 let result; 94 if (img.src.startsWith('data:')) { 95 result = await uploadDataURL(img.src); 96 } else { 97 result = await downloadData(img.src); 98 } 99 100 img.src = result.url; 101 img.className = 'media'; 102 img.dataset.relid = getRelativeID(result.id); 103 } catch (e) { 104 console.error(e); 105 } 106 })); 107 108 box.remove(); 109 return div; 110 } 111 112 /** 113 * Tell the backend to download the given URL and return the new ID 114 * 115 * @param {string} imgUrl 116 * @returns {Promise<object>} The JSON response 117 */ 118 async function downloadData(imgUrl) { 119 const formData = new FormData(); 120 formData.append('call', 'plugin_imgpaste'); 121 formData.append('url', imgUrl); 122 formData.append('id', JSINFO.id); 123 124 const response = await fetch( 125 DOKU_BASE + 'lib/exe/ajax.php', 126 { 127 method: 'POST', 128 body: formData 129 } 130 ); 131 132 if (!response.ok) { 133 throw new Error(response.statusText); 134 } 135 136 return await response.json(); 137 } 138 139 /** 140 * Tell the backend to create a file from the given dataURL and return the new ID 141 * 142 * @param {string} dataURL 143 * @returns {Promise<object>} The JSON response 144 */ 145 async function uploadDataURL(dataURL) { 146 const formData = new FormData(); 147 formData.append('call', 'plugin_imgpaste'); 148 formData.append('data', dataURL); 149 formData.append('id', JSINFO.id); 150 151 const response = await fetch( 152 DOKU_BASE + 'lib/exe/ajax.php', 153 { 154 method: 'POST', 155 body: formData 156 } 157 ); 158 159 if (!response.ok) { 160 throw new Error(response.statusText); 161 } 162 163 return await response.json(); 164 } 165 166 /** 167 * Uploads the given dataURL to the server and displays a progress dialog, inserting the syntax on success 168 * 169 * @param {string} dataURL 170 */ 171 async function uploadData(dataURL) { 172 const box = progressDialog(); 173 174 try { 175 const data = await uploadDataURL(dataURL); 176 box.classList.remove('info'); 177 box.classList.add('success'); 178 box.innerText = data.message; 179 setTimeout(() => { 180 box.remove(); 181 }, 1000); 182 insertSyntax(data.id); 183 } catch (e) { 184 box.classList.remove('info'); 185 box.classList.add('error'); 186 box.innerText = e.message; 187 setTimeout(() => { 188 box.remove(); 189 }, 1000); 190 } 191 } 192 193 /** 194 * Create a link ID for the given ID, preferrably relative to the current page 195 * 196 * @param {string} id 197 * @returns {string} 198 */ 199 function getRelativeID(id) { 200 // TODO remove the "if" check after LinkWizard.createRelativeID() is available in stable (after Kaos) 201 if (typeof LinkWizard !== 'undefined' && typeof LinkWizard.createRelativeID === 'function') { 202 id = LinkWizard.createRelativeID(JSINFO.id, id); 203 } else { 204 id = ':' + id; 205 } 206 return id; 207 } 208 209 /** 210 * Inserts the given ID into the current editor 211 * 212 * @todo add support for other editors like CKEditor 213 * @param {string} id The newly uploaded file ID 214 */ 215 function insertSyntax(id) { 216 id = getRelativeID(id); 217 218 if (typeof window.proseMirrorIsActive !== 'undefined' && window.proseMirrorIsActive === true) { 219 const pm = window.Prosemirror.view; 220 const imageNode = pm.state.schema.nodes.image.create({id: id}); 221 pm.dispatch(pm.state.tr.replaceSelectionWith(imageNode)); 222 } else { 223 insertAtCarret('wiki__text', '{{' + id + '}}'); 224 } 225 } 226 227 // main 228 window.addEventListener('paste', handlePaste, true); 229 230})(); 231