Răsfoiți Sursa

Merge pull request #16 from ckeditor/t/6

Feature: Added support for multiple file upload. Closes #6.
Piotr Jasiun 8 ani în urmă
părinte
comite
28ae11a75c

+ 2 - 1
packages/ckeditor5-upload/src/imageupload.js

@@ -40,7 +40,8 @@ export default class ImageUpload extends Plugin {
 				label: t( 'Insert image' ),
 				icon: imageIcon,
 				tooltip: true,
-				acceptedType: 'image/*'
+				acceptedType: 'image/*',
+				allowMultipleFiles: true
 			} );
 
 			view.on( 'done', ( evt, files ) => {

+ 24 - 3
packages/ckeditor5-upload/src/imageuploadcommand.js

@@ -6,6 +6,7 @@
 import ModelDocumentFragment from '@ckeditor/ckeditor5-engine/src/model/documentfragment';
 import ModelElement from '@ckeditor/ckeditor5-engine/src/model/element';
 import ModelRange from '@ckeditor/ckeditor5-engine/src/model/range';
+import ModelPosition from '@ckeditor/ckeditor5-engine/src/model/position';
 import ModelSelection from '@ckeditor/ckeditor5-engine/src/model/selection';
 import FileRepository from './filerepository';
 import { isImageType } from './utils';
@@ -35,6 +36,7 @@ export default class ImageUploadCommand extends Command {
 		const doc = editor.document;
 		const batch = options.batch || doc.batch();
 		const file = options.file;
+		const selection = doc.selection;
 		const fileRepository = editor.plugins.get( FileRepository );
 
 		if ( !isImageType( file ) ) {
@@ -42,17 +44,36 @@ export default class ImageUploadCommand extends Command {
 		}
 
 		doc.enqueueChanges( () => {
+			let insertPosition;
+			const selectedElement = selection.getSelectedElement();
+
+			// If selected element is placed directly in root - put image after it.
+			if ( selectedElement && selectedElement.parent.is( 'rootElement' ) ) {
+				insertPosition = ModelPosition.createAfter( selectedElement );
+			} else {
+				// If selection is inside some block - put image before it.
+				const firstBlock = doc.selection.getSelectedBlocks().next().value;
+
+				if ( firstBlock ) {
+					insertPosition = ModelPosition.createBefore( firstBlock );
+				}
+			}
+
+			// No position to insert.
+			if ( !insertPosition ) {
+				return;
+			}
+
 			const imageElement = new ModelElement( 'image', {
 				uploadId: fileRepository.createLoader( file ).id
 			} );
 			const documentFragment = new ModelDocumentFragment( [ imageElement ] );
-
-			const firstBlock = doc.selection.getSelectedBlocks().next().value;
-			const range = ModelRange.createFromParentsAndOffsets( firstBlock, 0, firstBlock, 0 );
+			const range = new ModelRange( insertPosition );
 			const insertSelection = new ModelSelection();
 			insertSelection.setRanges( [ range ] );
 
 			editor.data.insertContent( documentFragment, insertSelection, batch );
+			selection.setRanges( [ ModelRange.createOn( imageElement ) ] );
 		} );
 	}
 }

+ 19 - 1
packages/ckeditor5-upload/src/ui/filedialogbuttonview.js

@@ -44,6 +44,15 @@ export default class FileDialogButtonView extends ButtonView {
 		 */
 		this.fileInputView.bind( 'acceptedType' ).to( this, 'acceptedType' );
 
+		/**
+		 * Indicates if multiple files can be selected. Defaults to `true`.
+		 *
+		 * @observable
+		 * @member {Boolean} #allowMultipleFiles
+		 */
+		this.set( 'allowMultipleFiles', false );
+		this.fileInputView.bind( 'allowMultipleFiles' ).to( this, 'allowMultipleFiles' );
+
 		/**
 		 * Fired when file dialog is closed with file selected.
 		 *
@@ -99,6 +108,14 @@ class FileInputView extends View {
 		 */
 		this.set( 'acceptedType' );
 
+		/**
+		 * Indicates if multiple files can be selected. Defaults to `false`.
+		 *
+		 * @observable
+		 * @member {Boolean} #allowMultipleFiles
+		 */
+		this.set( 'allowMultipleFiles', false );
+
 		const bind = this.bindTemplate;
 
 		this.template = new Template( {
@@ -110,7 +127,8 @@ class FileInputView extends View {
 				],
 				type: 'file',
 				tabindex: '-1',
-				accept: bind.to( 'acceptedType' )
+				accept: bind.to( 'acceptedType' ),
+				multiple: bind.to( 'allowMultipleFiles' )
 			},
 
 			on: {

+ 28 - 2
packages/ckeditor5-upload/tests/imageuploadcommand.js

@@ -10,6 +10,7 @@ import { createNativeFileMock, AdapterMock } from './_utils/mocks';
 import { setData as setModelData, getData as getModelData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
 import Image from '@ckeditor/ckeditor5-image/src/image/imageengine';
 import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
+import buildModelConverter from '@ckeditor/ckeditor5-engine/src/conversion/buildmodelconverter';
 
 describe( 'ImageUploadCommand', () => {
 	let editor, command, adapterMock, document, fileRepository;
@@ -40,11 +41,36 @@ describe( 'ImageUploadCommand', () => {
 		it( 'should insert image', () => {
 			const file = createNativeFileMock();
 			setModelData( document, '<paragraph>foo[]</paragraph>' );
+
+			command._doExecute( { file } );
+
+			const id = fileRepository.getLoader( file ).id;
+			expect( getModelData( document ) ).to.equal( `[<image uploadId="${ id }"></image>]<paragraph>foo</paragraph>` );
+		} );
+
+		it( 'should insert image after other image', () => {
+			const file = createNativeFileMock();
+			setModelData( document, '[<image src="image.png"></image>]' );
+
 			command._doExecute( { file } );
 
 			const id = fileRepository.getLoader( file ).id;
+			expect( getModelData( document ) ).to.equal( `<image src="image.png"></image>[<image uploadId="${ id }"></image>]` );
+		} );
+
+		it( 'should not insert image when proper insert position cannot be found', () => {
+			const file = createNativeFileMock();
+			document.schema.registerItem( 'other' );
+			document.schema.allow( { name: 'other', inside: '$root' } );
+			buildModelConverter().for( editor.editing.modelToView )
+				.fromElement( 'other' )
+				.toElement( 'span' );
+
+			setModelData( document, '<other>[]</other>' );
+
+			command._doExecute( { file } );
 
-			expect( getModelData( document ) ).to.equal( `<image uploadId="${ id }"></image><paragraph>foo[]</paragraph>` );
+			expect( getModelData( document ) ).to.equal( '<other>[]</other>' );
 		} );
 
 		it( 'should not insert non-image', () => {
@@ -66,7 +92,7 @@ describe( 'ImageUploadCommand', () => {
 			command._doExecute( { batch, file } );
 			const id = fileRepository.getLoader( file ).id;
 
-			expect( getModelData( document ) ).to.equal( `<image uploadId="${ id }"></image><paragraph>foo[]</paragraph>` );
+			expect( getModelData( document ) ).to.equal( `[<image uploadId="${ id }"></image>]<paragraph>foo</paragraph>` );
 			sinon.assert.calledOnce( spy );
 		} );
 	} );

+ 5 - 5
packages/ckeditor5-upload/tests/imageuploadengine.js

@@ -71,7 +71,7 @@ describe( 'ImageUploadEngine', () => {
 
 		const id = fileRepository.getLoader( fileMock ).id;
 		expect( getModelData( document ) ).to.equal(
-			`<image uploadId="${ id }"></image><paragraph>foo bar baz[]</paragraph>`
+			`[<image uploadId="${ id }"></image>]<paragraph>foo bar baz</paragraph>`
 		);
 	} );
 
@@ -120,10 +120,10 @@ describe( 'ImageUploadEngine', () => {
 
 		adapterMock.uploadStartedCallback = () => {
 			expect( getViewData( viewDocument ) ).to.equal(
-				'<figure class="image ck-widget" contenteditable="false">' +
+				'[<figure class="image ck-widget" contenteditable="false">' +
 					`<img src="${ base64Sample }"></img>` +
-				'</figure>' +
-				'<p>{}foo bar</p>' );
+				'</figure>]' +
+				'<p>foo bar</p>' );
 			expect( loader.status ).to.equal( 'uploading' );
 			done();
 		};
@@ -140,7 +140,7 @@ describe( 'ImageUploadEngine', () => {
 		adapterMock.uploadStartedCallback = () => {
 			document.once( 'changesDone', () => {
 				expect( getViewData( viewDocument ) ).to.equal(
-					'<figure class="image ck-widget" contenteditable="false"><img src="image.png"></img></figure><p>{}foo bar</p>'
+					'[<figure class="image ck-widget" contenteditable="false"><img src="image.png"></img></figure>]<p>foo bar</p>'
 				);
 				expect( loader.status ).to.equal( 'idle' );
 

+ 12 - 3
packages/ckeditor5-upload/tests/manual/imageupload.html

@@ -3,6 +3,15 @@
 	<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla finibus consequat placerat. Vestibulum id tellus et mauris sagittis tincidunt quis id mauris. Curabitur consectetur lectus sit amet tellus mattis, non lobortis leo interdum.</p>
 </div>
 
-<div style="margin-top: 10px;">
-	<button id="progress" disabled>Upload progress</button>
-</div>
+<div id="button-container"></div>
+
+<style>
+	#button-container div {
+		margin-top: 10px;
+	}
+
+	#button-container button {
+		margin-right: 10px;
+	}
+</style>
+

+ 34 - 27
packages/ckeditor5-upload/tests/manual/imageupload.js

@@ -3,7 +3,7 @@
  * For licensing, see LICENSE.md.
  */
 
-/* globals document, window, console */
+/* globals document, console */
 
 import ClassicEditor from '@ckeditor/ckeditor5-editor-classic/src/classic';
 import Enter from '@ckeditor/ckeditor5-enter/src/enter';
@@ -22,6 +22,8 @@ import List from '@ckeditor/ckeditor5-list/src/list';
 import ImageUpload from '../../src/imageupload';
 import { AdapterMock } from '../_utils/mocks';
 
+const buttonContainer = document.getElementById( 'button-container' );
+
 ClassicEditor.create( document.querySelector( '#editor' ), {
 	plugins: [
 		Enter, Typing, Paragraph, Heading, Undo, Bold, Italic, Heading, List, Image, ImageToolbar, Clipboard,
@@ -30,38 +32,43 @@ ClassicEditor.create( document.querySelector( '#editor' ), {
 	toolbar: [ 'headings', 'undo', 'redo', 'bold', 'italic', 'bulletedList', 'numberedList', 'insertImage' ]
 } )
 .then( editor => {
-	let adapterMock, progress;
-	const total = 500;
-
-	window.editor = editor;
-
-	const progressButton = document.getElementById( 'progress' );
-
 	// Register fake adapter.
 	editor.plugins.get( 'upload/filerepository' ).createAdapter = loader => {
-		adapterMock = new AdapterMock( loader );
-		progress = 0;
-		loader.on( 'change:uploadedPercent', () => {
-			console.log( `Loader upload progress: ${ loader.uploadedPercent }%` );
-		}  );
-
-		progressButton.removeAttribute( 'disabled' );
+		const adapterMock = new AdapterMock( loader );
+		createProgressButton( loader, adapterMock );
 
 		return adapterMock;
 	};
-
-	progressButton.addEventListener( 'click', () => {
-		if ( adapterMock ) {
-			progress += 100;
-			adapterMock.mockProgress( progress, total );
-
-			if ( progress == total ) {
-				progressButton.setAttribute( 'disabled', 'true' );
-				adapterMock.mockSuccess( { original: './sample.jpg' } );
-			}
-		}
-	} );
 } )
 .catch( err => {
 	console.error( err.stack );
 } );
+
+function createProgressButton( loader, adapterMock ) {
+	const fileName = loader.file.name;
+	const container = document.createElement( 'div' );
+	const progressInfo = document.createElement( 'span' );
+	progressInfo.innerHTML = `File: ${ fileName }. Progress: 0%.`;
+	const button = document.createElement( 'button' );
+	button.innerHTML = 'Upload progress';
+
+	container.appendChild( button );
+	container.appendChild( progressInfo );
+
+	buttonContainer.appendChild( container );
+
+	let progress = 0;
+	const total = 500;
+	button.addEventListener( 'click', () => {
+		progress += 100;
+		adapterMock.mockProgress( progress, total );
+
+		if ( progress == total ) {
+			button.setAttribute( 'disabled', 'true' );
+			adapterMock.mockSuccess( { original: './sample.jpg' } );
+		}
+
+		progressInfo.innerHTML = `File: ${ fileName }. Progress: ${ loader.uploadedPercent }%.`;
+	} );
+}
+

+ 3 - 2
packages/ckeditor5-upload/tests/manual/imageupload.md

@@ -2,8 +2,9 @@
 
 1. Drop an image into editor.
 1. Image should be read and displayed.
-1. Open console.
 1. Press "Upload progress" button couple times to simulate upload process.
 1. After uploading is complete your image should be replaced with sample image from server.
 
-Repeat all the steps but this time use toolbar button to add image.
+Repeat all the steps with:
+* dropping multiple images,
+* using toolbar button to add one and multiple images.

+ 6 - 0
packages/ckeditor5-upload/tests/ui/filedialogbuttonview.js

@@ -46,6 +46,12 @@ describe( 'FileDialogButtonView', () => {
 		expect( view.fileInputView.acceptedType ).to.equal( 'audio/*' );
 	} );
 
+	it( 'should pass allowMultipleFiles to input view', () => {
+		view.set( { allowMultipleFiles: true } );
+
+		expect( view.fileInputView.allowMultipleFiles ).to.be.true;
+	} );
+
 	it( 'should delegate input view done event', ( done ) => {
 		const files = [];