Skip to content
This repository was archived by the owner on Jun 26, 2020. It is now read-only.

Commit 2d4ba62

Browse files
authored
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.
2 parents 811d32f + 4134934 commit 2d4ba62

File tree

3 files changed

+114
-65
lines changed

3 files changed

+114
-65
lines changed

src/imageuploadbutton.js

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,13 +40,16 @@ export default class ImageUploadButton extends Plugin {
4040
const command = editor.commands.get( 'imageUpload' );
4141

4242
view.set( {
43-
label: t( 'Insert image' ),
44-
icon: imageIcon,
45-
tooltip: true,
4643
acceptedType: 'image/*',
4744
allowMultipleFiles: true
4845
} );
4946

47+
view.buttonView.set( {
48+
label: t( 'Insert image' ),
49+
icon: imageIcon,
50+
tooltip: true
51+
} );
52+
5053
view.bind( 'isEnabled' ).to( command );
5154

5255
view.on( 'done', ( evt, files ) => {

src/ui/filedialogbuttonview.js

Lines changed: 57 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,6 @@
33
* For licensing, see LICENSE.md.
44
*/
55

6-
/* globals document */
7-
86
/**
97
* @module upload/ui/filedialogbuttonview
108
*/
@@ -14,24 +12,53 @@ import View from '@ckeditor/ckeditor5-ui/src/view';
1412
import Template from '@ckeditor/ckeditor5-ui/src/template';
1513

1614
/**
17-
* File Dialog button view.
15+
* The file dialog button view.
16+
*
17+
* This component provides a button that opens the native file selection dialog.
18+
* It can be used to implement the UI of a file upload feature.
19+
*
20+
* const view = new FileDialogButtonView( locale );
21+
*
22+
* view.set( {
23+
* acceptedType: 'image/*',
24+
* allowMultipleFiles: true
25+
* } );
26+
*
27+
* view.buttonView.set( {
28+
* label: t( 'Insert image' ),
29+
* icon: imageIcon,
30+
* tooltip: true
31+
* } );
1832
*
19-
* @extends module:ui/button/buttonview~ButtonView
33+
* view.on( 'done', ( evt, files ) => {
34+
* for ( const file of Array.from( files ) ) {
35+
* console.log( 'Selected file', file );
36+
* }
37+
* } );
38+
*
39+
* @extends module:ui/view~View
2040
*/
21-
export default class FileDialogButtonView extends ButtonView {
41+
export default class FileDialogButtonView extends View {
2242
/**
2343
* @inheritDoc
2444
*/
2545
constructor( locale ) {
2646
super( locale );
2747

2848
/**
29-
* Hidden input view used to execute file dialog. It will be hidden and added to the end of `document.body`.
49+
* The button view of the component.
50+
*
51+
* @member {module:ui/button/buttonview~ButtonView}
52+
*/
53+
this.buttonView = new ButtonView( locale );
54+
55+
/**
56+
* A hidden `<input>` view used to execute file dialog.
3057
*
3158
* @protected
3259
* @member {module:upload/ui/filedialogbuttonview~FileInputView}
3360
*/
34-
this.fileInputView = new FileInputView( locale );
61+
this._fileInputView = new FileInputView( locale );
3562

3663
/**
3764
* Accepted file types. Can be provided in form of file extensions, media type or one of:
@@ -42,50 +69,56 @@ export default class FileDialogButtonView extends ButtonView {
4269
* @observable
4370
* @member {String} #acceptedType
4471
*/
45-
this.fileInputView.bind( 'acceptedType' ).to( this, 'acceptedType' );
72+
this._fileInputView.bind( 'acceptedType' ).to( this );
4673

4774
/**
4875
* Indicates if multiple files can be selected. Defaults to `true`.
4976
*
5077
* @observable
5178
* @member {Boolean} #allowMultipleFiles
5279
*/
53-
this.set( 'allowMultipleFiles', false );
54-
this.fileInputView.bind( 'allowMultipleFiles' ).to( this, 'allowMultipleFiles' );
80+
this._fileInputView.bind( 'allowMultipleFiles' ).to( this );
5581

5682
/**
5783
* Fired when file dialog is closed with file selected.
5884
*
59-
* fileDialogButtonView.on( 'done', ( evt, files ) => {
60-
* for ( const file of files ) {
61-
* processFile( file );
85+
* view.on( 'done', ( evt, files ) => {
86+
* for ( const file of files ) {
87+
* console.log( 'Selected file', file );
88+
* }
6289
* }
63-
* }
6490
*
6591
* @event done
6692
* @param {Array.<File>} files Array of selected files.
6793
*/
68-
this.fileInputView.delegate( 'done' ).to( this );
94+
this._fileInputView.delegate( 'done' ).to( this );
6995

70-
this.on( 'execute', () => {
71-
this.fileInputView.open();
96+
this.template = new Template( {
97+
tag: 'span',
98+
attributes: {
99+
class: 'ck-file-dialog-button',
100+
},
101+
children: [
102+
this.buttonView,
103+
this._fileInputView
104+
]
72105
} );
73106

74-
document.body.appendChild( this.fileInputView.element );
107+
this.buttonView.on( 'execute', () => {
108+
this._fileInputView.open();
109+
} );
75110
}
76111

77112
/**
78-
* @inheritDoc
113+
* Focuses the {@link #buttonView}.
79114
*/
80-
destroy() {
81-
document.body.removeChild( this.fileInputView.element );
82-
83-
super.destroy();
115+
focus() {
116+
this.buttonView.focus();
84117
}
85118
}
86119

87120
/**
88-
* Hidden file input view class.
121+
* The hidden file input view class.
89122
*
90123
* @private
91124
* @extends {module:ui/view~View}

tests/ui/filedialogbuttonview.js

Lines changed: 51 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -3,64 +3,77 @@
33
* For licensing, see LICENSE.md.
44
*/
55

6-
/* globals document */
7-
8-
import ClassicEditor from '@ckeditor/ckeditor5-editor-classic/src/classiceditor';
96
import FileDialogButtonView from '../../src/ui/filedialogbuttonview';
7+
import ButtonView from '@ckeditor/ckeditor5-ui/src/button/buttonview';
8+
import View from '@ckeditor/ckeditor5-ui/src/view';
109

1110
describe( 'FileDialogButtonView', () => {
12-
let view, editor;
11+
let view, localeMock;
1312

1413
beforeEach( () => {
15-
const editorElement = document.createElement( 'div' );
16-
document.body.appendChild( editorElement );
17-
18-
return ClassicEditor
19-
.create( editorElement )
20-
.then( newEditor => {
21-
editor = newEditor;
14+
localeMock = { t: val => val };
15+
view = new FileDialogButtonView( localeMock );
2216

23-
view = new FileDialogButtonView( editor.locale );
24-
} );
17+
return view.init();
2518
} );
2619

27-
it( 'should append input view to document body', () => {
28-
expect( view.fileInputView.element.parentNode ).to.equal( document.body );
20+
it( 'should be rendered from a template', () => {
21+
expect( view.element.classList.contains( 'ck-file-dialog-button' ) ).to.true;
2922
} );
3023

31-
it( 'should remove input view from body after destroy', () => {
32-
view.destroy();
24+
describe( 'child views', () => {
25+
describe( 'button view', () => {
26+
it( 'should be rendered', () => {
27+
expect( view.buttonView ).to.instanceof( ButtonView );
28+
expect( view.buttonView ).to.equal( view.template.children.get( 0 ) );
29+
} );
3330

34-
expect( view.fileInputView.element.parentNode ).to.be.null;
35-
} );
31+
it( 'should open file dialog on execute', () => {
32+
const spy = sinon.spy( view._fileInputView, 'open' );
33+
view.buttonView.fire( 'execute' );
3634

37-
it( 'should open file dialog on execute', () => {
38-
const spy = sinon.spy( view.fileInputView, 'open' );
39-
view.fire( 'execute' );
35+
sinon.assert.calledOnce( spy );
36+
} );
37+
} );
4038

41-
sinon.assert.calledOnce( spy );
42-
} );
39+
describe( 'file dialog', () => {
40+
it( 'should be rendered', () => {
41+
expect( view._fileInputView ).to.instanceof( View );
42+
expect( view._fileInputView ).to.equal( view.template.children.get( 1 ) );
43+
} );
4344

44-
it( 'should pass acceptedType to input view', () => {
45-
view.set( { acceptedType: 'audio/*' } );
45+
it( 'should be bound to view#acceptedType', () => {
46+
view.set( { acceptedType: 'audio/*' } );
4647

47-
expect( view.fileInputView.acceptedType ).to.equal( 'audio/*' );
48-
} );
48+
expect( view._fileInputView.acceptedType ).to.equal( 'audio/*' );
49+
} );
4950

50-
it( 'should pass allowMultipleFiles to input view', () => {
51-
view.set( { allowMultipleFiles: true } );
51+
it( 'should be bound to view#allowMultipleFiles', () => {
52+
view.set( { allowMultipleFiles: true } );
5253

53-
expect( view.fileInputView.allowMultipleFiles ).to.be.true;
54-
} );
54+
expect( view._fileInputView.allowMultipleFiles ).to.be.true;
55+
} );
5556

56-
it( 'should delegate input view done event', done => {
57-
const files = [];
57+
it( 'should delegate done event to view', () => {
58+
const spy = sinon.spy();
59+
const files = [];
5860

59-
view.on( 'done', ( evt, data ) => {
60-
expect( data ).to.equal( files );
61-
done();
61+
view.on( 'done', spy );
62+
view._fileInputView.fire( 'done', files );
63+
64+
sinon.assert.calledOnce( spy );
65+
expect( spy.lastCall.args[ 1 ] ).to.equal( files );
66+
} );
6267
} );
68+
} );
69+
70+
describe( 'focus()', () => {
71+
it( 'should focus view#buttonView', () => {
72+
const spy = sinon.spy( view.buttonView, 'focus' );
6373

64-
view.fileInputView.fire( 'done', files );
74+
view.focus();
75+
76+
sinon.assert.calledOnce( spy );
77+
} );
6578
} );
6679
} );

0 commit comments

Comments
 (0)