8
0
Pārlūkot izejas kodu

Support for pasting images from Word in Safari added.

Krzysztof Krztoń 7 gadi atpakaļ
vecāks
revīzija
06e4342441

+ 94 - 21
packages/ckeditor5-paste-from-office/src/filters/image.js

@@ -7,10 +7,14 @@
  * @module paste-from-office/filters/image
  */
 
-import Matcher from '@ckeditor/ckeditor5-engine/src/view/matcher';
-import Range from '@ckeditor/ckeditor5-engine/src/view/range';
+/* globals XMLHttpRequest, FileReader */
+
+import ViewMatcher from '@ckeditor/ckeditor5-engine/src/view/matcher';
+import ViewRange from '@ckeditor/ckeditor5-engine/src/view/range';
 import UpcastWriter from '@ckeditor/ckeditor5-engine/src/view/upcastwriter';
 
+import ModelRange from '@ckeditor/ckeditor5-engine/src/model/range';
+
 import { convertHexToBase64 } from './utils';
 
 /**
@@ -19,8 +23,9 @@ import { convertHexToBase64 } from './utils';
  *
  * @param {module:engine/view/documentfragment~DocumentFragment} documentFragment Document fragment on which transform images.
  * @param {String} rtfData The RTF data from which images representation will be used.
+ * @param {module:engine/model/model~Model} model Editor model.
  */
-export function replaceImagesSourceWithBase64( documentFragment, rtfData ) {
+export function replaceImagesSourceWithBase64( documentFragment, rtfData, model ) {
 	if ( !documentFragment.childCount ) {
 		return;
 	}
@@ -31,12 +36,14 @@ export function replaceImagesSourceWithBase64( documentFragment, rtfData ) {
 	removeAllImgElementsRepresentingShapes( shapesIds, documentFragment, upcastWriter );
 	removeAllShapeElements( documentFragment, upcastWriter );
 
-	const imageElements = findAllImageElements( documentFragment );
+	const images = findAllImageElementsWithLocalSource( documentFragment );
 
-	if ( imageElements.length ) {
-		const imageData = extractImageDataFromRtf( rtfData );
+	if ( images.file.length ) {
+		replaceImagesFileSourceWithInlineRepresentation( images.file, extractImageDataFromRtf( rtfData ), upcastWriter );
+	}
 
-		replaceImagesSourceWithInlineRepresentation( imageElements, imageData, upcastWriter );
+	if ( images.blob.length ) {
+		replaceImagesBlobSourceWithInlineRepresentation( images.blob, model );
 	}
 }
 
@@ -45,9 +52,9 @@ export function replaceImagesSourceWithBase64( documentFragment, rtfData ) {
 // @param {module:engine/view/documentfragment~DocumentFragment} documentFragment Document fragment from which to extract shape ids.
 // @returns {Array.<String>} Array of shape ids.
 function findAllShapesIds( documentFragment ) {
-	const range = Range.createIn( documentFragment );
+	const range = ViewRange.createIn( documentFragment );
 
-	const shapeElementsMatcher = new Matcher( {
+	const shapeElementsMatcher = new ViewMatcher( {
 		name: /v:(.+)/
 	} );
 
@@ -72,9 +79,9 @@ function findAllShapesIds( documentFragment ) {
 // @param {module:engine/view/documentfragment~DocumentFragment} documentFragment Document fragment from which to remove `<img>` elements.
 // @param {module:engine/view/upcastwriter~UpcastWriter} writer
 function removeAllImgElementsRepresentingShapes( shapesIds, documentFragment, writer ) {
-	const range = Range.createIn( documentFragment );
+	const range = ViewRange.createIn( documentFragment );
 
-	const imageElementsMatcher = new Matcher( {
+	const imageElementsMatcher = new ViewMatcher( {
 		name: 'img'
 	} );
 
@@ -87,6 +94,8 @@ function removeAllImgElementsRepresentingShapes( shapesIds, documentFragment, wr
 
 			if ( shapes.length && shapes.every( shape => shapesIds.indexOf( shape ) > -1 ) ) {
 				imgs.push( el );
+			} else if ( !el.getAttribute( 'src' ) ) {
+				imgs.push( el );
 			}
 		}
 	}
@@ -101,9 +110,9 @@ function removeAllImgElementsRepresentingShapes( shapesIds, documentFragment, wr
 // @param {module:engine/view/documentfragment~DocumentFragment} documentFragment Document fragment from which to remove shape elements.
 // @param {module:engine/view/upcastwriter~UpcastWriter} writer
 function removeAllShapeElements( documentFragment, writer ) {
-	const range = Range.createIn( documentFragment );
+	const range = ViewRange.createIn( documentFragment );
 
-	const shapeElementsMatcher = new Matcher( {
+	const shapeElementsMatcher = new ViewMatcher( {
 		name: /v:(.+)/
 	} );
 
@@ -120,22 +129,31 @@ function removeAllShapeElements( documentFragment, writer ) {
 	}
 }
 
-// Finds all `<img>` elements in a given document fragment.
+// Finds all `<img>` elements in a given document fragment which have source pointing to local `file://` or `blob:` resource.
 //
 // @param {module:engine/view/documentfragment~DocumentFragment} documentFragment Document fragment in which to look for `<img>` elements.
-// @returns {Array.<module:engine/view/element~Element>} Array of found `<img>` elements.
-function findAllImageElements( documentFragment ) {
-	const range = Range.createIn( documentFragment );
+// @returns {Object} result All found images grouped by source type.
+// @returns {Array.<module:engine/view/element~Element>} result.file Array of found `<img>` elements with `file://` source.
+// @returns {Array.<module:engine/view/element~Element>} result.blob Array of found `<img>` elements with `blob:` source.
+function findAllImageElementsWithLocalSource( documentFragment ) {
+	const range = ViewRange.createIn( documentFragment );
 
-	const imageElementsMatcher = new Matcher( {
+	const imageElementsMatcher = new ViewMatcher( {
 		name: 'img'
 	} );
 
-	const imgs = [];
+	const imgs = {
+		file: [],
+		blob: []
+	};
 
 	for ( const value of range ) {
 		if ( imageElementsMatcher.match( value.item ) ) {
-			imgs.push( value.item );
+			if ( value.item.getAttribute( 'src' ).indexOf( 'file://' ) === 0 ) {
+				imgs.file.push( value.item );
+			} else if ( value.item.getAttribute( 'src' ).indexOf( 'blob:' ) === 0 ) {
+				imgs.blob.push( value.item );
+			}
 		}
 	}
 
@@ -189,7 +207,7 @@ function extractImageDataFromRtf( rtfData ) {
 // @param {Array.<Object>} imagesHexSources Array of images hex sources (usually the result of `extractImageDataFromRtf()` function).
 // The array should be the same length as `imageElements` parameter.
 // @param {module:engine/view/upcastwriter~UpcastWriter} upcastWriter
-function replaceImagesSourceWithInlineRepresentation( imageElements, imagesHexSources, upcastWriter ) {
+function replaceImagesFileSourceWithInlineRepresentation( imageElements, imagesHexSources, upcastWriter ) {
 	// Assume there is an equal amount of image elements and images HEX sources so they can be matched accordingly based on existing order.
 	if ( imageElements.length === imagesHexSources.length ) {
 		for ( let i = 0; i < imageElements.length; i++ ) {
@@ -201,3 +219,58 @@ function replaceImagesSourceWithInlineRepresentation( imageElements, imagesHexSo
 		}
 	}
 }
+
+// Extracts all view images sources data from blob url, converts it to base64 and replaces source in the corresponding model images.
+//
+// @param {Array.<module:engine/view/element~Element>} imageElements Image elements from which sources extract blob data.
+// @param {module:engine/model/model~Model} model Model containing corresponding image elements which sources will be updated.
+function replaceImagesBlobSourceWithInlineRepresentation( imageElements, model ) {
+	for ( const img of imageElements ) {
+		const src = img.getAttribute( 'src' );
+
+		fetchImageDataFromBlobUrlAsBase64( src )
+			.then( data => {
+				model.enqueueChange( 'transparent', writer => {
+					const root = model.document.getRoot();
+					const range = ModelRange.createIn( root );
+
+					for ( const value of range ) {
+						if ( value.item.is( 'element', 'image' ) && value.item.getAttribute( 'src' ) === src ) {
+							writer.setAttribute( 'src', data, value.item );
+						}
+					}
+				} );
+			} )
+			// In case of error we basically can't do nothing. Still images with blob URLs are locally
+			// visible so it is not noticeable until the content will be opened in a new browser tab.
+			.catch();
+	}
+}
+
+// Fetches blob data via XHR and converts it to base64 representation.
+//
+// @param {String} url Blob url from which to fetch blob data.
+// @returns {Promise} A promise which resolves once blob data is successfully fetched and converted to base64.
+function fetchImageDataFromBlobUrlAsBase64( url ) {
+	return new Promise( ( resolve, reject ) => {
+		const xhr = new XMLHttpRequest();
+
+		xhr.open( 'GET', url, true );
+
+		xhr.responseType = 'blob';
+
+		xhr.addEventListener( 'error', () => reject() );
+		xhr.addEventListener( 'abort', () => reject() );
+		xhr.addEventListener( 'load', () => {
+			const reader = new FileReader();
+
+			reader.onloadend = () => {
+				resolve( reader.result );
+			};
+
+			reader.readAsDataURL( xhr.response );
+		} );
+
+		xhr.send();
+	} );
+}

+ 4 - 3
packages/ckeditor5-paste-from-office/src/pastefromoffice.js

@@ -42,7 +42,7 @@ export default class PasteFromOffice extends Plugin {
 			const html = data.dataTransfer.getData( 'text/html' );
 
 			if ( isWordInput( html ) ) {
-				data.content = this._normalizeWordInput( html, data.dataTransfer );
+				data.content = this._normalizeWordInput( html, data.dataTransfer, editor.editing.model );
 			}
 		}, { priority: 'high' } );
 	}
@@ -55,13 +55,14 @@ export default class PasteFromOffice extends Plugin {
 	 * @protected
 	 * @param {String} input Word input.
 	 * @param {module:clipboard/datatransfer~DataTransfer} dataTransfer Data transfer instance.
+	 * @param {module:engine/model/model~Model} model Editor model.
 	 * @returns {module:engine/view/documentfragment~DocumentFragment} Normalized input.
 	 */
-	_normalizeWordInput( input, dataTransfer ) {
+	_normalizeWordInput( input, dataTransfer, model ) {
 		const { body, stylesString } = parseHtml( input );
 
 		transformListItemLikeElementsIntoLists( body, stylesString );
-		replaceImagesSourceWithBase64( body, dataTransfer.getData( 'text/rtf' ) );
+		replaceImagesSourceWithBase64( body, dataTransfer.getData( 'text/rtf' ), model );
 
 		return body;
 	}