8
0
Pārlūkot izejas kodu

Merge pull request #67 from ckeditor/t/66

Fix: Destroying `FileDialogButtonView` should not throw an error. Closes #66.

BREAKING CHANGE: The `FileDialogButtonView` is not a `ButtonView` instance anymore but a wrapper instead. The button of the component is available under the `#buttonView` property.
Aleksander Nowodzinski 8 gadi atpakaļ
vecāks
revīzija
02f2e9f5c8

+ 6 - 3
packages/ckeditor5-upload/src/imageuploadbutton.js

@@ -40,13 +40,16 @@ export default class ImageUploadButton extends Plugin {
 			const command = editor.commands.get( 'imageUpload' );
 
 			view.set( {
-				label: t( 'Insert image' ),
-				icon: imageIcon,
-				tooltip: true,
 				acceptedType: 'image/*',
 				allowMultipleFiles: true
 			} );
 
+			view.buttonView.set( {
+				label: t( 'Insert image' ),
+				icon: imageIcon,
+				tooltip: true
+			} );
+
 			view.bind( 'isEnabled' ).to( command );
 
 			view.on( 'done', ( evt, files ) => {

+ 57 - 24
packages/ckeditor5-upload/src/ui/filedialogbuttonview.js

@@ -3,8 +3,6 @@
  * For licensing, see LICENSE.md.
  */
 
-/* globals document */
-
 /**
  * @module upload/ui/filedialogbuttonview
  */
@@ -14,11 +12,33 @@ import View from '@ckeditor/ckeditor5-ui/src/view';
 import Template from '@ckeditor/ckeditor5-ui/src/template';
 
 /**
- * File Dialog button view.
+ * The file dialog button view.
+ *
+ * This component provides a button that opens the native file selection dialog.
+ * It can be used to implement the UI of a file upload feature.
+ *
+ *		const view = new FileDialogButtonView( locale );
+ *
+ *		view.set( {
+ *			acceptedType: 'image/*',
+ *			allowMultipleFiles: true
+ *		} );
+ *
+ *		view.buttonView.set( {
+ *			label: t( 'Insert image' ),
+ *			icon: imageIcon,
+ *			tooltip: true
+ *		} );
  *
- * @extends module:ui/button/buttonview~ButtonView
+ *		view.on( 'done', ( evt, files ) => {
+ *			for ( const file of Array.from( files ) ) {
+ *				console.log( 'Selected file', file );
+ *			}
+ *		} );
+ *
+ * @extends module:ui/view~View
  */
-export default class FileDialogButtonView extends ButtonView {
+export default class FileDialogButtonView extends View {
 	/**
 	 * @inheritDoc
 	 */
@@ -26,12 +46,19 @@ export default class FileDialogButtonView extends ButtonView {
 		super( locale );
 
 		/**
-		 * Hidden input view used to execute file dialog. It will be hidden and added to the end of `document.body`.
+		 * The button view of the component.
+		 *
+		 * @member {module:ui/button/buttonview~ButtonView}
+		 */
+		this.buttonView = new ButtonView( locale );
+
+		/**
+		 * A hidden `<input>` view used to execute file dialog.
 		 *
 		 * @protected
 		 * @member {module:upload/ui/filedialogbuttonview~FileInputView}
 		 */
-		this.fileInputView = new FileInputView( locale );
+		this._fileInputView = new FileInputView( locale );
 
 		/**
 		 * Accepted file types. Can be provided in form of file extensions, media type or one of:
@@ -42,7 +69,7 @@ export default class FileDialogButtonView extends ButtonView {
 		 * @observable
 		 * @member {String} #acceptedType
 		 */
-		this.fileInputView.bind( 'acceptedType' ).to( this, 'acceptedType' );
+		this._fileInputView.bind( 'acceptedType' ).to( this );
 
 		/**
 		 * Indicates if multiple files can be selected. Defaults to `true`.
@@ -50,42 +77,48 @@ export default class FileDialogButtonView extends ButtonView {
 		 * @observable
 		 * @member {Boolean} #allowMultipleFiles
 		 */
-		this.set( 'allowMultipleFiles', false );
-		this.fileInputView.bind( 'allowMultipleFiles' ).to( this, 'allowMultipleFiles' );
+		this._fileInputView.bind( 'allowMultipleFiles' ).to( this );
 
 		/**
 		 * Fired when file dialog is closed with file selected.
 		 *
-		 *	fileDialogButtonView.on( 'done', ( evt, files ) => {
-		 *		for ( const file of files ) {
-		 *			processFile( file );
+		 *		view.on( 'done', ( evt, files ) => {
+		 *			for ( const file of files ) {
+		 *				console.log( 'Selected file', file );
+		 *			}
 		 *		}
-		 *	}
 		 *
 		 * @event done
 		 * @param {Array.<File>} files Array of selected files.
 		 */
-		this.fileInputView.delegate( 'done' ).to( this );
+		this._fileInputView.delegate( 'done' ).to( this );
 
-		this.on( 'execute', () => {
-			this.fileInputView.open();
+		this.template = new Template( {
+			tag: 'span',
+			attributes: {
+				class: 'ck-file-dialog-button',
+			},
+			children: [
+				this.buttonView,
+				this._fileInputView
+			]
 		} );
 
-		document.body.appendChild( this.fileInputView.element );
+		this.buttonView.on( 'execute', () => {
+			this._fileInputView.open();
+		} );
 	}
 
 	/**
-	 * @inheritDoc
+	 * Focuses the {@link #buttonView}.
 	 */
-	destroy() {
-		document.body.removeChild( this.fileInputView.element );
-
-		super.destroy();
+	focus() {
+		this.buttonView.focus();
 	}
 }
 
 /**
- * Hidden file input view class.
+ * The hidden file input view class.
  *
  * @private
  * @extends {module:ui/view~View}

+ 51 - 38
packages/ckeditor5-upload/tests/ui/filedialogbuttonview.js

@@ -3,64 +3,77 @@
  * For licensing, see LICENSE.md.
  */
 
-/* globals document */
-
-import ClassicEditor from '@ckeditor/ckeditor5-editor-classic/src/classiceditor';
 import FileDialogButtonView from '../../src/ui/filedialogbuttonview';
+import ButtonView from '@ckeditor/ckeditor5-ui/src/button/buttonview';
+import View from '@ckeditor/ckeditor5-ui/src/view';
 
 describe( 'FileDialogButtonView', () => {
-	let view, editor;
+	let view, localeMock;
 
 	beforeEach( () => {
-		const editorElement = document.createElement( 'div' );
-		document.body.appendChild( editorElement );
-
-		return ClassicEditor
-			.create( editorElement )
-			.then( newEditor => {
-				editor = newEditor;
+		localeMock = { t: val => val };
+		view = new FileDialogButtonView( localeMock );
 
-				view = new FileDialogButtonView( editor.locale );
-			} );
+		return view.init();
 	} );
 
-	it( 'should append input view to document body', () => {
-		expect( view.fileInputView.element.parentNode ).to.equal( document.body );
+	it( 'should be rendered from a template', () => {
+		expect( view.element.classList.contains( 'ck-file-dialog-button' ) ).to.true;
 	} );
 
-	it( 'should remove input view from body after destroy', () => {
-		view.destroy();
+	describe( 'child views', () => {
+		describe( 'button view', () => {
+			it( 'should be rendered', () => {
+				expect( view.buttonView ).to.instanceof( ButtonView );
+				expect( view.buttonView ).to.equal( view.template.children.get( 0 ) );
+			} );
 
-		expect( view.fileInputView.element.parentNode ).to.be.null;
-	} );
+			it( 'should open file dialog on execute', () => {
+				const spy = sinon.spy( view._fileInputView, 'open' );
+				view.buttonView.fire( 'execute' );
 
-	it( 'should open file dialog on execute', () => {
-		const spy = sinon.spy( view.fileInputView, 'open' );
-		view.fire( 'execute' );
+				sinon.assert.calledOnce( spy );
+			} );
+		} );
 
-		sinon.assert.calledOnce( spy );
-	} );
+		describe( 'file dialog', () => {
+			it( 'should be rendered', () => {
+				expect( view._fileInputView ).to.instanceof( View );
+				expect( view._fileInputView ).to.equal( view.template.children.get( 1 ) );
+			} );
 
-	it( 'should pass acceptedType to input view', () => {
-		view.set( { acceptedType: 'audio/*' } );
+			it( 'should be bound to view#acceptedType', () => {
+				view.set( { acceptedType: 'audio/*' } );
 
-		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 } );
+			it( 'should be bound to view#allowMultipleFiles', () => {
+				view.set( { allowMultipleFiles: true } );
 
-		expect( view.fileInputView.allowMultipleFiles ).to.be.true;
-	} );
+				expect( view._fileInputView.allowMultipleFiles ).to.be.true;
+			} );
 
-	it( 'should delegate input view done event', done => {
-		const files = [];
+			it( 'should delegate done event to view', () => {
+				const spy = sinon.spy();
+				const files = [];
 
-		view.on( 'done', ( evt, data ) => {
-			expect( data ).to.equal( files );
-			done();
+				view.on( 'done', spy );
+				view._fileInputView.fire( 'done', files );
+
+				sinon.assert.calledOnce( spy );
+				expect( spy.lastCall.args[ 1 ] ).to.equal( files );
+			} );
 		} );
+	} );
+
+	describe( 'focus()', () => {
+		it( 'should focus view#buttonView', () => {
+			const spy = sinon.spy( view.buttonView, 'focus' );
 
-		view.fileInputView.fire( 'done', files );
+			view.focus();
+
+			sinon.assert.calledOnce( spy );
+		} );
 	} );
 } );