Przeglądaj źródła

Process local images inside clipboard pipeline and integrate with image upload flow.

Krzysztof Krztoń 7 lat temu
rodzic
commit
be4b8df909

+ 1 - 1
packages/ckeditor5-image/package.json

@@ -10,6 +10,7 @@
     "ckeditor5-plugin"
     "ckeditor5-plugin"
   ],
   ],
   "dependencies": {
   "dependencies": {
+    "@ckeditor/ckeditor5-clipboard": "^10.0.3",
     "@ckeditor/ckeditor5-core": "^11.0.1",
     "@ckeditor/ckeditor5-core": "^11.0.1",
     "@ckeditor/ckeditor5-engine": "^11.0.0",
     "@ckeditor/ckeditor5-engine": "^11.0.0",
     "@ckeditor/ckeditor5-theme-lark": "^11.1.0",
     "@ckeditor/ckeditor5-theme-lark": "^11.1.0",
@@ -20,7 +21,6 @@
   },
   },
   "devDependencies": {
   "devDependencies": {
     "@ckeditor/ckeditor5-basic-styles": "^10.0.3",
     "@ckeditor/ckeditor5-basic-styles": "^10.0.3",
-    "@ckeditor/ckeditor5-clipboard": "^10.0.3",
     "@ckeditor/ckeditor5-editor-classic": "^11.0.1",
     "@ckeditor/ckeditor5-editor-classic": "^11.0.1",
     "@ckeditor/ckeditor5-enter": "^10.1.2",
     "@ckeditor/ckeditor5-enter": "^10.1.2",
     "@ckeditor/ckeditor5-essentials": "^10.1.2",
     "@ckeditor/ckeditor5-essentials": "^10.1.2",

+ 54 - 89
packages/ckeditor5-image/src/imageupload/imageuploadediting.js

@@ -7,14 +7,13 @@
  * @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';
+import { upcastAttributeToAttribute } from '@ckeditor/ckeditor5-engine/src/conversion/upcast-converters';
 
 
 import ImageUploadCommand from '../../src/imageupload/imageuploadcommand';
 import ImageUploadCommand from '../../src/imageupload/imageuploadcommand';
-import { isImageType } from '../../src/imageupload/utils';
+import { isImageType, isLocalImage, wrapImageToFetch } from '../../src/imageupload/utils';
 
 
 /**
 /**
  * The editing part of the image upload feature.
  * The editing part of the image upload feature.
@@ -36,6 +35,7 @@ export default class ImageUploadEditing extends Plugin {
 		const editor = this.editor;
 		const editor = this.editor;
 		const doc = editor.model.document;
 		const doc = editor.model.document;
 		const schema = editor.model.schema;
 		const schema = editor.model.schema;
+		const conversion = editor.conversion;
 		const fileRepository = editor.plugins.get( FileRepository );
 		const fileRepository = editor.plugins.get( FileRepository );
 
 
 		// Setup schema to allow uploadId and uploadStatus for images.
 		// Setup schema to allow uploadId and uploadStatus for images.
@@ -46,6 +46,16 @@ export default class ImageUploadEditing extends Plugin {
 		// Register imageUpload command.
 		// Register imageUpload command.
 		editor.commands.add( 'imageUpload', new ImageUploadCommand( editor ) );
 		editor.commands.add( 'imageUpload', new ImageUploadCommand( editor ) );
 
 
+		// Register upcast converter for uploadId.
+		conversion.for( 'upcast' )
+			.add( upcastAttributeToAttribute( {
+				view: {
+					name: 'img',
+					key: 'uploadId'
+				},
+				model: 'uploadId'
+			} ) );
+
 		// Handle pasted images.
 		// Handle pasted images.
 		// For every image file, a new file loader is created and a placeholder image is
 		// For every image file, a new file loader is created and a placeholder image is
 		// inserted into the content. Then, those images are uploaded once they appear in the model
 		// inserted into the content. Then, those images are uploaded once they appear in the model
@@ -76,39 +86,52 @@ 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 = [];
+		// Handle HTML pasted with images with base64 or blob sources.
+		// For every image file, a new file loader is created and a placeholder image is
+		// inserted into the content. Then, those images are uploaded once they appear in the model
+		// (see Document#change listener below).
+		this.listenTo( editor.plugins.get( 'Clipboard' ), 'inputTransformation', ( evt, data ) => {
+			const view = editor.editing.view;
 
 
-			for ( const entry of changes ) {
-				let item = null;
+			const fetchableImages = Array.from( view.createRangeIn( data.content ) )
+				.filter( value => isLocalImage( value.item ) && !value.item.getAttribute( 'uploadProcessed' ) )
+				.map( ( value, index ) => wrapImageToFetch( value.item, index ) );
 
 
-				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 ( !fetchableImages.length ) {
+				return;
+			}
 
 
-				if ( item && !item.getAttribute( 'uploadId' ) && item.getAttribute( 'src' ) &&
-					item.getAttribute( 'src' ).match( /data:image\/\w+;base64/ ) ) {
-					imagesToUpload.push( item );
+			evt.stop();
+
+			Promise.all( fetchableImages ).then( items => {
+				for ( const item of items ) {
+					if ( !item.file ) {
+						// Failed to fetch image or create a file instance, remove image element.
+						view.change( writer => {
+							writer.remove( item.image );
+						} );
+					} else {
+						const loader = fileRepository.createLoader( item.file );
+
+						if ( loader ) {
+							view.change( writer => {
+								writer.setAttribute( 'src', '', item.image );
+								writer.setAttribute( 'uploadId', loader.id, item.image );
+							} );
+						} else {
+							view.change( writer => {
+								// Set attribute so the image will not be processed 2nd time.
+								writer.setAttribute( 'uploadProcessed', true, item.image );
+							} );
+						}
+					}
 				}
 				}
-			}
 
 
-			// Upload images with base64 sources.
-			if ( imagesToUpload.length ) {
-				this._uploadBase64Images( imagesToUpload, editor );
-			}
+				editor.plugins.get( 'Clipboard' ).fire( 'inputTransformation', {
+					content: data.content,
+					dataTransfer: data.dataTransfer
+				} );
+			} );
 		} );
 		} );
 
 
 		// Prevents from the browser redirecting to the dropped image.
 		// Prevents from the browser redirecting to the dropped image.
@@ -232,47 +255,6 @@ export default class ImageUploadEditing extends Plugin {
 	}
 	}
 
 
 	/**
 	/**
-	 * 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.
 	 * Creates `srcset` attribute based on a given file upload response and sets it as an attribute to a specific image element.
 	 *
 	 *
 	 * @protected
 	 * @protected
@@ -318,20 +300,3 @@ 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 ) {
-	try {
-		return new File( [ blob ], filename );
-	} catch ( err ) {
-		// Edge does not support `File` constructor ATM, see https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/9551546/.
-		// However, the `File` function is present (so cannot be checked with `!window.File` or `typeof File === 'function'`), but
-		// calling it with `new File( ... )` throws an error. This try-catch prevents that. Also when the function will
-		// be implemented correctly in Edge the code will start working without any changes (see #247).
-		return null;
-	}
-}

+ 75 - 0
packages/ckeditor5-image/src/imageupload/utils.js

@@ -7,6 +7,8 @@
  * @module image/imageupload/utils
  * @module image/imageupload/utils
  */
  */
 
 
+/* global fetch, File */
+
 /**
 /**
  * Checks if a given file is an image.
  * Checks if a given file is an image.
  *
  *
@@ -18,3 +20,76 @@ export function isImageType( file ) {
 
 
 	return types.test( file.type );
 	return types.test( file.type );
 }
 }
+
+/**
+ * Creates a promise which fetches the image local source (base64 or blob) and returns as a `File` object.
+ *
+ * @param {module:engine/view/element~Element} image Image which source to fetch.
+ * @param {Number} index Image index used as image name suffix.
+ * @returns {Promise} A promise which resolves when image source is fetched and converted to `File` instance.
+ * It resolves with object holding initial image element (as `image`) and its file source (as `file`). If
+ * the `file` attribute is null, it means fetching failed.
+ */
+export function wrapImageToFetch( image, index ) {
+	return new Promise( resolve => {
+		// Fetch works asynchronously and so does not block browser UI when processing data.
+		fetch( image.getAttribute( 'src' ) )
+			.then( resource => resource.blob() )
+			.then( blob => {
+				const ext = getImageFileType( blob, image.getAttribute( 'src' ) );
+				const filename = `${ Number( new Date() ) }-image${ index }.${ ext }`;
+				const file = createFileFromBlob( blob, filename );
+
+				resolve( { image, file } );
+			} )
+			.catch( () => {
+				// We always resolve a promise so `Promise.all` will not reject if one of many fetch fails.
+				resolve( { image, file: null } );
+			} );
+	} );
+}
+
+/**
+ * Checks whether given node is an image element with local source (base64 or blob).
+ *
+ * @param {module:engine/view/node~Node} node Node to check.
+ * @returns {Boolean}
+ */
+export function isLocalImage( node ) {
+	return node.is( 'element', 'img' ) && node.getAttribute( 'src' ) &&
+		( node.getAttribute( 'src' ).match( /data:image\/\w+;base64,/g ) ||
+		node.getAttribute( 'src' ).match( /blob:/g ) );
+}
+
+// Extracts image type based on its blob representation or its source.
+//
+// @param {String} src Image src attribute value.
+// @param {Blob} blob Image blob representation.
+// @returns {String}
+function getImageFileType( blob, src ) {
+	if ( blob.type ) {
+		return blob.type.replace( 'image/', '' );
+	} else if ( src.match( /data:image\/(\w+);base64/ ) ) {
+		return src.match( /data:image\/(\w+);base64/ )[ 1 ].toLowerCase();
+	} else {
+		// Fallback to 'jpeg' as common extension.
+		return 'jpeg';
+	}
+}
+
+// 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 ) {
+	try {
+		return new File( [ blob ], filename );
+	} catch ( err ) {
+		// Edge does not support `File` constructor ATM, see https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/9551546/.
+		// However, the `File` function is present (so cannot be checked with `!window.File` or `typeof File === 'function'`), but
+		// calling it with `new File( ... )` throws an error. This try-catch prevents that. Also when the function will
+		// be implemented correctly in Edge the code will start working without any changes (see #247).
+		return null;
+	}
+}