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

Merge pull request #16 from ckeditor/t/6

Feature: Added support for multiple file upload. Closes #6.
Piotr Jasiun 8 лет назад
Родитель
Сommit
28ae11a75c

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

@@ -40,7 +40,8 @@ export default class ImageUpload extends Plugin {
 				label: t( 'Insert image' ),
 				label: t( 'Insert image' ),
 				icon: imageIcon,
 				icon: imageIcon,
 				tooltip: true,
 				tooltip: true,
-				acceptedType: 'image/*'
+				acceptedType: 'image/*',
+				allowMultipleFiles: true
 			} );
 			} );
 
 
 			view.on( 'done', ( evt, files ) => {
 			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 ModelDocumentFragment from '@ckeditor/ckeditor5-engine/src/model/documentfragment';
 import ModelElement from '@ckeditor/ckeditor5-engine/src/model/element';
 import ModelElement from '@ckeditor/ckeditor5-engine/src/model/element';
 import ModelRange from '@ckeditor/ckeditor5-engine/src/model/range';
 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 ModelSelection from '@ckeditor/ckeditor5-engine/src/model/selection';
 import FileRepository from './filerepository';
 import FileRepository from './filerepository';
 import { isImageType } from './utils';
 import { isImageType } from './utils';
@@ -35,6 +36,7 @@ export default class ImageUploadCommand extends Command {
 		const doc = editor.document;
 		const doc = editor.document;
 		const batch = options.batch || doc.batch();
 		const batch = options.batch || doc.batch();
 		const file = options.file;
 		const file = options.file;
+		const selection = doc.selection;
 		const fileRepository = editor.plugins.get( FileRepository );
 		const fileRepository = editor.plugins.get( FileRepository );
 
 
 		if ( !isImageType( file ) ) {
 		if ( !isImageType( file ) ) {
@@ -42,17 +44,36 @@ export default class ImageUploadCommand extends Command {
 		}
 		}
 
 
 		doc.enqueueChanges( () => {
 		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', {
 			const imageElement = new ModelElement( 'image', {
 				uploadId: fileRepository.createLoader( file ).id
 				uploadId: fileRepository.createLoader( file ).id
 			} );
 			} );
 			const documentFragment = new ModelDocumentFragment( [ imageElement ] );
 			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();
 			const insertSelection = new ModelSelection();
 			insertSelection.setRanges( [ range ] );
 			insertSelection.setRanges( [ range ] );
 
 
 			editor.data.insertContent( documentFragment, insertSelection, batch );
 			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' );
 		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.
 		 * Fired when file dialog is closed with file selected.
 		 *
 		 *
@@ -99,6 +108,14 @@ class FileInputView extends View {
 		 */
 		 */
 		this.set( 'acceptedType' );
 		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;
 		const bind = this.bindTemplate;
 
 
 		this.template = new Template( {
 		this.template = new Template( {
@@ -110,7 +127,8 @@ class FileInputView extends View {
 				],
 				],
 				type: 'file',
 				type: 'file',
 				tabindex: '-1',
 				tabindex: '-1',
-				accept: bind.to( 'acceptedType' )
+				accept: bind.to( 'acceptedType' ),
+				multiple: bind.to( 'allowMultipleFiles' )
 			},
 			},
 
 
 			on: {
 			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 { setData as setModelData, getData as getModelData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
 import Image from '@ckeditor/ckeditor5-image/src/image/imageengine';
 import Image from '@ckeditor/ckeditor5-image/src/image/imageengine';
 import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
 import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
+import buildModelConverter from '@ckeditor/ckeditor5-engine/src/conversion/buildmodelconverter';
 
 
 describe( 'ImageUploadCommand', () => {
 describe( 'ImageUploadCommand', () => {
 	let editor, command, adapterMock, document, fileRepository;
 	let editor, command, adapterMock, document, fileRepository;
@@ -40,11 +41,36 @@ describe( 'ImageUploadCommand', () => {
 		it( 'should insert image', () => {
 		it( 'should insert image', () => {
 			const file = createNativeFileMock();
 			const file = createNativeFileMock();
 			setModelData( document, '<paragraph>foo[]</paragraph>' );
 			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 } );
 			command._doExecute( { file } );
 
 
 			const id = fileRepository.getLoader( file ).id;
 			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', () => {
 		it( 'should not insert non-image', () => {
@@ -66,7 +92,7 @@ describe( 'ImageUploadCommand', () => {
 			command._doExecute( { batch, file } );
 			command._doExecute( { batch, file } );
 			const id = fileRepository.getLoader( file ).id;
 			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 );
 			sinon.assert.calledOnce( spy );
 		} );
 		} );
 	} );
 	} );

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

@@ -71,7 +71,7 @@ describe( 'ImageUploadEngine', () => {
 
 
 		const id = fileRepository.getLoader( fileMock ).id;
 		const id = fileRepository.getLoader( fileMock ).id;
 		expect( getModelData( document ) ).to.equal(
 		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 = () => {
 		adapterMock.uploadStartedCallback = () => {
 			expect( getViewData( viewDocument ) ).to.equal(
 			expect( getViewData( viewDocument ) ).to.equal(
-				'<figure class="image ck-widget" contenteditable="false">' +
+				'[<figure class="image ck-widget" contenteditable="false">' +
 					`<img src="${ base64Sample }"></img>` +
 					`<img src="${ base64Sample }"></img>` +
-				'</figure>' +
-				'<p>{}foo bar</p>' );
+				'</figure>]' +
+				'<p>foo bar</p>' );
 			expect( loader.status ).to.equal( 'uploading' );
 			expect( loader.status ).to.equal( 'uploading' );
 			done();
 			done();
 		};
 		};
@@ -140,7 +140,7 @@ describe( 'ImageUploadEngine', () => {
 		adapterMock.uploadStartedCallback = () => {
 		adapterMock.uploadStartedCallback = () => {
 			document.once( 'changesDone', () => {
 			document.once( 'changesDone', () => {
 				expect( getViewData( viewDocument ) ).to.equal(
 				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' );
 				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>
 	<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>
 
 
-<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.
  * For licensing, see LICENSE.md.
  */
  */
 
 
-/* globals document, window, console */
+/* globals document, console */
 
 
 import ClassicEditor from '@ckeditor/ckeditor5-editor-classic/src/classic';
 import ClassicEditor from '@ckeditor/ckeditor5-editor-classic/src/classic';
 import Enter from '@ckeditor/ckeditor5-enter/src/enter';
 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 ImageUpload from '../../src/imageupload';
 import { AdapterMock } from '../_utils/mocks';
 import { AdapterMock } from '../_utils/mocks';
 
 
+const buttonContainer = document.getElementById( 'button-container' );
+
 ClassicEditor.create( document.querySelector( '#editor' ), {
 ClassicEditor.create( document.querySelector( '#editor' ), {
 	plugins: [
 	plugins: [
 		Enter, Typing, Paragraph, Heading, Undo, Bold, Italic, Heading, List, Image, ImageToolbar, Clipboard,
 		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' ]
 	toolbar: [ 'headings', 'undo', 'redo', 'bold', 'italic', 'bulletedList', 'numberedList', 'insertImage' ]
 } )
 } )
 .then( editor => {
 .then( editor => {
-	let adapterMock, progress;
-	const total = 500;
-
-	window.editor = editor;
-
-	const progressButton = document.getElementById( 'progress' );
-
 	// Register fake adapter.
 	// Register fake adapter.
 	editor.plugins.get( 'upload/filerepository' ).createAdapter = loader => {
 	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;
 		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 => {
 .catch( err => {
 	console.error( err.stack );
 	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. Drop an image into editor.
 1. Image should be read and displayed.
 1. Image should be read and displayed.
-1. Open console.
 1. Press "Upload progress" button couple times to simulate upload process.
 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.
 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/*' );
 		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 ) => {
 	it( 'should delegate input view done event', ( done ) => {
 		const files = [];
 		const files = [];