8
0
Просмотр исходного кода

Support for uploading base64 images.

Krzysztof Krztoń 7 лет назад
Родитель
Сommit
e19c6026d0
1 измененных файлов с 134 добавлено и 27 удалено
  1. 134 27
      packages/ckeditor5-image/src/imageupload/imageuploadediting.js

+ 134 - 27
packages/ckeditor5-image/src/imageupload/imageuploadediting.js

@@ -7,6 +7,8 @@
  * @module image/imageupload/imageuploadediting
  * @module image/imageupload/imageuploadediting
  */
  */
 
 
+/* global fetch, File */
+
 import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
 import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
 import FileRepository from '@ckeditor/ckeditor5-upload/src/filerepository';
 import FileRepository from '@ckeditor/ckeditor5-upload/src/filerepository';
 import Notification from '@ckeditor/ckeditor5-ui/src/notification/notification';
 import Notification from '@ckeditor/ckeditor5-ui/src/notification/notification';
@@ -74,6 +76,41 @@ export default class ImageUploadEditing extends Plugin {
 			} );
 			} );
 		} );
 		} );
 
 
+		// Handle images inserted or modified with base64 source.
+		doc.on( 'change', () => {
+			const changes = doc.differ.getChanges( { includeChangesInGraveyard: false } );
+			const imagesToUpload = [];
+
+			for ( const entry of changes ) {
+				let item = null;
+
+				if ( entry.type == 'insert' && entry.name == 'image' ) {
+					// Process entry item if it was an image insertion.
+					item = entry.position.nodeAfter;
+				} else if ( entry.type == 'attribute' && entry.attributeKey == 'src' ) {
+					// Process entry item if it was modification of `src` attribute of an image element.
+					// Such cases may happen when image with `blob` source is inserted and then have it
+					// converted to base64 data by clipboard pipeline.
+					const el = entry.range.start.nodeAfter;
+
+					// Check if modified element is an image element.
+					if ( el && el.is( 'image' ) ) {
+						item = el;
+					}
+				}
+
+				if ( item && !item.getAttribute( 'uploadId' ) && item.getAttribute( 'src' ) &&
+					item.getAttribute( 'src' ).match( /data:image\/\w+;base64/ ) ) {
+					imagesToUpload.push( item );
+				}
+			}
+
+			// Upload images with base64 sources.
+			if ( imagesToUpload.length ) {
+				this._uploadBase64Images( imagesToUpload, editor );
+			}
+		} );
+
 		// Prevents from the browser redirecting to the dropped image.
 		// Prevents from the browser redirecting to the dropped image.
 		editor.editing.view.document.on( 'dragover', ( evt, data ) => {
 		editor.editing.view.document.on( 'dragover', ( evt, data ) => {
 			data.preventDefault();
 			data.preventDefault();
@@ -156,33 +193,7 @@ export default class ImageUploadEditing extends Plugin {
 			.then( data => {
 			.then( data => {
 				model.enqueueChange( 'transparent', writer => {
 				model.enqueueChange( 'transparent', writer => {
 					writer.setAttributes( { uploadStatus: 'complete', src: data.default }, imageElement );
 					writer.setAttributes( { uploadStatus: 'complete', src: data.default }, imageElement );
-
-					// Srcset attribute for responsive images support.
-					let maxWidth = 0;
-					const srcsetAttribute = Object.keys( data )
-						// Filter out keys that are not integers.
-						.filter( key => {
-							const width = parseInt( key, 10 );
-
-							if ( !isNaN( width ) ) {
-								maxWidth = Math.max( maxWidth, width );
-
-								return true;
-							}
-						} )
-
-						// Convert each key to srcset entry.
-						.map( key => `${ data[ key ] } ${ key }w` )
-
-						// Join all entries.
-						.join( ', ' );
-
-					if ( srcsetAttribute != '' ) {
-						writer.setAttribute( 'srcset', {
-							data: srcsetAttribute,
-							width: maxWidth
-						}, imageElement );
-					}
+					this._parseAndSetSrcsetAttributeOnImage( data, imageElement, writer );
 				} );
 				} );
 
 
 				clean();
 				clean();
@@ -219,6 +230,85 @@ export default class ImageUploadEditing extends Plugin {
 			fileRepository.destroyLoader( loader );
 			fileRepository.destroyLoader( loader );
 		}
 		}
 	}
 	}
+
+	/**
+	 * Converts and uploads base64 `src` data of all given images. On successful upload
+	 * the image `src` attribute is replaced with the URL of the remote file.
+	 *
+	 * @protected
+	 * @param {Array.<module:engine/model/element~Element>} images Array of image elements to upload.
+	 * @param {module:core/editor/editor~Editor} editor The editor instance.
+	 */
+	_uploadBase64Images( images, editor ) {
+		const fileRepository = editor.plugins.get( FileRepository );
+
+		for ( const image of images ) {
+			const src = image.getAttribute( 'src' );
+			const ext = src.match( /data:image\/(\w+);base64/ )[ 1 ];
+
+			// Fetch works asynchronously and so does not block browser UI when processing data.
+			fetch( src )
+				.then( resource => resource.blob() )
+				.then( blob => {
+					const filename = `${ Number( new Date() ) }-image.${ ext }`;
+					const file = createFileFromBlob( blob, filename );
+
+					if ( !file ) {
+						throw new Error( 'File API not supported. Cannot create `File` from `Blob`.' );
+					}
+
+					return fileRepository.createLoader( file ).upload();
+				} )
+				.then( data => {
+					editor.model.enqueueChange( 'transparent', writer => {
+						writer.setAttribute( 'src', data.default, image );
+						this._parseAndSetSrcsetAttributeOnImage( data, image, writer );
+					} );
+				} )
+				.catch( () => {
+					// As upload happens in the background without direct user interaction,
+					// no errors notifications should be shown.
+				} );
+		}
+	}
+
+	/**
+	 * Creates `srcset` attribute based on a given file upload response and sets it as an attribute to a specific image element.
+	 *
+	 * @protected
+	 * @param {Object} data Data object from which `srcset` will be created.
+	 * @param {module:engine/model/element~Element} image The image element on which `srcset` attribute will be set.
+	 * @param {module:engine/model/writer~Writer} writer
+	 */
+	_parseAndSetSrcsetAttributeOnImage( data, image, writer ) {
+		// Srcset attribute for responsive images support.
+		let maxWidth = 0;
+
+		const srcsetAttribute = Object.keys( data )
+			// Filter out keys that are not integers.
+			.filter( key => {
+				const width = parseInt( key, 10 );
+
+				if ( !isNaN( width ) ) {
+					maxWidth = Math.max( maxWidth, width );
+
+					return true;
+				}
+			} )
+
+			// Convert each key to srcset entry.
+			.map( key => `${ data[ key ] } ${ key }w` )
+
+			// Join all entries.
+			.join( ', ' );
+
+		if ( srcsetAttribute != '' ) {
+			writer.setAttribute( 'srcset', {
+				data: srcsetAttribute,
+				width: maxWidth
+			}, image );
+		}
+	}
 }
 }
 
 
 // Returns `true` if non-empty `text/html` is included in the data transfer.
 // Returns `true` if non-empty `text/html` is included in the data transfer.
@@ -228,3 +318,20 @@ export default class ImageUploadEditing extends Plugin {
 export function isHtmlIncluded( dataTransfer ) {
 export function isHtmlIncluded( dataTransfer ) {
 	return Array.from( dataTransfer.types ).includes( 'text/html' ) && dataTransfer.getData( 'text/html' ) !== '';
 	return Array.from( dataTransfer.types ).includes( 'text/html' ) && dataTransfer.getData( 'text/html' ) !== '';
 }
 }
+
+// Creates `File` instance from the given `Blob` instance using specified filename.
+//
+// @param {Blob} blob The `Blob` instance from which file will be created.
+// @param {String} filename Filename used during file creation.
+// @returns {File|null} The `File` instance created from the given blob or `null` if `File API` is not available.
+function createFileFromBlob( blob, filename ) {
+	if ( typeof File === 'function' ) {
+		return new File( [ blob ], filename );
+	} else {
+		// Edge does not support `File` constructor ATM, see https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/9551546/.
+		// The `Blob` object could be used, however it causes the issue with upload itself where filename is read directly
+		// from a `File` instance. Since `Blob` instance does not provide one, the default "blob" filename is used which
+		// doesn't work well with most upload adapters (same name for every file + checking file type by extension fails).
+		return null;
+	}
+}