diff --git a/src/imageuploadbutton.js b/src/imageuploadbutton.js index 1c39d40..4488ceb 100644 --- a/src/imageuploadbutton.js +++ b/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 ) => { diff --git a/src/ui/filedialogbuttonview.js b/src/ui/filedialogbuttonview.js index b5b8e43..d125e73 100644 --- a/src/ui/filedialogbuttonview.js +++ b/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 `` 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.} 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} diff --git a/tests/ui/filedialogbuttonview.js b/tests/ui/filedialogbuttonview.js index 36e4c34..b232806 100644 --- a/tests/ui/filedialogbuttonview.js +++ b/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 ); + } ); } ); } );