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

Commit 6908ec6

Browse files
authored
Merge pull request #209 from ckeditor/t/208
Fix: Made image upload by drag&drop work when the `ImageUploadCommand` is disabled. Closes #208.
2 parents 937809c + 282bec0 commit 6908ec6

File tree

3 files changed

+130
-29
lines changed

3 files changed

+130
-29
lines changed

src/imageupload/imageuploadcommand.js

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

6-
import ModelElement from '@ckeditor/ckeditor5-engine/src/model/element';
76
import ModelRange from '@ckeditor/ckeditor5-engine/src/model/range';
87
import ModelSelection from '@ckeditor/ckeditor5-engine/src/model/selection';
98
import FileRepository from '@ckeditor/ckeditor5-upload/src/filerepository';
@@ -44,7 +43,7 @@ export default class ImageUploadCommand extends Command {
4443
return;
4544
}
4645

47-
const imageElement = new ModelElement( 'image', {
46+
const imageElement = writer.createElement( 'image', {
4847
uploadId: loader.id
4948
} );
5049

@@ -60,7 +59,7 @@ export default class ImageUploadCommand extends Command {
6059

6160
// Inserting an image might've failed due to schema regulations.
6261
if ( imageElement.parent ) {
63-
writer.setSelection( ModelRange.createOn( imageElement ) );
62+
writer.setSelection( imageElement, 'on' );
6463
}
6564
} );
6665
}

src/imageupload/imageuploadediting.js

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import FileRepository from '@ckeditor/ckeditor5-upload/src/filerepository';
1212
import ImageUploadCommand from '../../src/imageupload/imageuploadcommand';
1313
import Notification from '@ckeditor/ckeditor5-ui/src/notification/notification';
1414
import ModelSelection from '@ckeditor/ckeditor5-engine/src/model/selection';
15+
import ModelRange from '@ckeditor/ckeditor5-engine/src/model/range';
1516
import { isImageType, findOptimalInsertionPosition } from '../../src/imageupload/utils';
1617

1718
/**
@@ -45,7 +46,7 @@ export default class ImageUploadEditing extends Plugin {
4546
editor.commands.add( 'imageUpload', new ImageUploadCommand( editor ) );
4647

4748
// Execute imageUpload command when image is dropped or pasted.
48-
editor.editing.view.document.on( 'clipboardInput', ( evt, data ) => {
49+
this.listenTo( editor.editing.view.document, 'clipboardInput', ( evt, data ) => {
4950
// Skip if non empty HTML data is included.
5051
// https://github.com/ckeditor/ckeditor5-upload/issues/68
5152
if ( isHtmlIncluded( data.dataTransfer ) ) {
@@ -57,10 +58,28 @@ export default class ImageUploadEditing extends Plugin {
5758
);
5859

5960
for ( const file of data.dataTransfer.files ) {
60-
const insertAt = findOptimalInsertionPosition( targetModelSelection );
61-
6261
if ( isImageType( file ) ) {
63-
editor.execute( 'imageUpload', { file, insertAt } );
62+
const insertAt = findOptimalInsertionPosition( targetModelSelection );
63+
64+
editor.model.change( writer => {
65+
const loader = fileRepository.createLoader( file );
66+
67+
// Do not throw when upload adapter is not set. FileRepository will log an error anyway.
68+
if ( !loader ) {
69+
return;
70+
}
71+
72+
const imageElement = writer.createElement( 'image', { uploadId: loader.id } );
73+
const targetSelection = new ModelSelection( [ new ModelRange( insertAt ) ] );
74+
75+
editor.model.insertContent( imageElement, targetSelection );
76+
77+
// Inserting an image might've failed due to schema regulations.
78+
if ( imageElement.parent ) {
79+
writer.setSelection( imageElement, 'on' );
80+
}
81+
} );
82+
6483
evt.stop();
6584
}
6685

tests/imageupload/imageuploadediting.js

Lines changed: 105 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import VirtualTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/virtualtesteditor';
99

1010
import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
11+
import Clipboard from '@ckeditor/ckeditor5-clipboard/src/clipboard';
1112
import ImageEditing from '../../src/image/imageediting';
1213
import ImageUploadEditing from '../../src/imageupload/imageuploadediting';
1314
import ImageUploadCommand from '../../src/imageupload/imageuploadcommand';
@@ -23,6 +24,7 @@ import { getData as getViewData } from '@ckeditor/ckeditor5-engine/src/dev-utils
2324
import Range from '@ckeditor/ckeditor5-engine/src/model/range';
2425
import Position from '@ckeditor/ckeditor5-engine/src/model/position';
2526

27+
import log from '@ckeditor/ckeditor5-utils/src/log';
2628
import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils';
2729
import Notification from '@ckeditor/ckeditor5-ui/src/notification/notification';
2830

@@ -65,6 +67,10 @@ describe( 'ImageUploadEditing', () => {
6567
} );
6668
} );
6769

70+
afterEach( () => {
71+
return editor.destroy();
72+
} );
73+
6874
it( 'should register proper schema rules', () => {
6975
expect( model.schema.checkAttribute( [ '$root', 'image' ], 'uploadId' ) ).to.be.true;
7076
} );
@@ -73,8 +79,7 @@ describe( 'ImageUploadEditing', () => {
7379
expect( editor.commands.get( 'imageUpload' ) ).to.be.instanceOf( ImageUploadCommand );
7480
} );
7581

76-
it( 'should execute imageUpload command when image is pasted', () => {
77-
const spy = sinon.spy( editor, 'execute' );
82+
it( 'should insert image when is pasted', () => {
7883
const fileMock = createNativeFileMock();
7984
const dataTransfer = new DataTransfer( { files: [ fileMock ], types: [ 'Files' ] } );
8085
setModelData( model, '<paragraph>[]foo</paragraph>' );
@@ -84,17 +89,13 @@ describe( 'ImageUploadEditing', () => {
8489

8590
viewDocument.fire( 'clipboardInput', { dataTransfer, targetRanges: [ targetViewRange ] } );
8691

87-
sinon.assert.calledOnce( spy );
88-
sinon.assert.calledWith( spy, 'imageUpload' );
89-
9092
const id = fileRepository.getLoader( fileMock ).id;
9193
expect( getModelData( model ) ).to.equal(
9294
`<paragraph>foo</paragraph>[<image uploadId="${ id }" uploadStatus="reading"></image>]`
9395
);
9496
} );
9597

96-
it( 'should execute imageUpload command with an optimized position when image is pasted', () => {
97-
const spy = sinon.spy( editor, 'execute' );
98+
it( 'should insert image at optimized position when is pasted', () => {
9899
const fileMock = createNativeFileMock();
99100
const dataTransfer = new DataTransfer( { files: [ fileMock ], types: [ 'Files' ] } );
100101
setModelData( model, '<paragraph>[]foo</paragraph>' );
@@ -105,17 +106,13 @@ describe( 'ImageUploadEditing', () => {
105106

106107
viewDocument.fire( 'clipboardInput', { dataTransfer, targetRanges: [ targetViewRange ] } );
107108

108-
sinon.assert.calledOnce( spy );
109-
sinon.assert.calledWith( spy, 'imageUpload' );
110-
111109
const id = fileRepository.getLoader( fileMock ).id;
112110
expect( getModelData( model ) ).to.equal(
113111
`[<image uploadId="${ id }" uploadStatus="reading"></image>]<paragraph>foo</paragraph>`
114112
);
115113
} );
116114

117-
it( 'should execute imageUpload command when multiple files image are pasted', () => {
118-
const spy = sinon.spy( editor, 'execute' );
115+
it( 'should insert multiple image files when are pasted', () => {
119116
const files = [ createNativeFileMock(), createNativeFileMock() ];
120117
const dataTransfer = new DataTransfer( { files, types: [ 'Files' ] } );
121118
setModelData( model, '<paragraph>[]foo</paragraph>' );
@@ -125,9 +122,6 @@ describe( 'ImageUploadEditing', () => {
125122

126123
viewDocument.fire( 'clipboardInput', { dataTransfer, targetRanges: [ targetViewRange ] } );
127124

128-
sinon.assert.calledTwice( spy );
129-
sinon.assert.calledWith( spy, 'imageUpload' );
130-
131125
const id1 = fileRepository.getLoader( files[ 0 ] ).id;
132126
const id2 = fileRepository.getLoader( files[ 1 ] ).id;
133127

@@ -138,8 +132,56 @@ describe( 'ImageUploadEditing', () => {
138132
);
139133
} );
140134

141-
it( 'should not execute imageUpload command when file is not an image', () => {
142-
const spy = sinon.spy( editor, 'execute' );
135+
it( 'should insert image when is pasted on allowed position when ImageUploadCommand is disabled', () => {
136+
const fileMock = createNativeFileMock();
137+
const dataTransfer = new DataTransfer( { files: [ fileMock ], types: [ 'Files' ] } );
138+
setModelData( model, '<paragraph>[]foo</paragraph>' );
139+
140+
const command = editor.commands.get( 'imageUpload' );
141+
142+
command.on( 'beforeChange:isEnabled', evt => {
143+
evt.return = false;
144+
evt.stop();
145+
}, { priority: 'highest' } );
146+
147+
command.isEnabled = false;
148+
149+
const targetRange = Range.createFromParentsAndOffsets( doc.getRoot(), 1, doc.getRoot(), 1 );
150+
const targetViewRange = editor.editing.mapper.toViewRange( targetRange );
151+
152+
viewDocument.fire( 'clipboardInput', { dataTransfer, targetRanges: [ targetViewRange ] } );
153+
154+
const id = fileRepository.getLoader( fileMock ).id;
155+
expect( getModelData( model ) ).to.equal(
156+
`<paragraph>foo</paragraph>[<image uploadId="${ id }" uploadStatus="reading"></image>]`
157+
);
158+
} );
159+
160+
it( 'should not insert image when editor is in read-only mode', () => {
161+
// Clipboard plugin is required for this test.
162+
return VirtualTestEditor
163+
.create( {
164+
plugins: [ ImageEditing, ImageUploadEditing, Paragraph, UploadAdapterPluginMock, Clipboard ]
165+
} )
166+
.then( editor => {
167+
const fileMock = createNativeFileMock();
168+
const dataTransfer = new DataTransfer( { files: [ fileMock ], types: [ 'Files' ] } );
169+
setModelData( editor.model, '<paragraph>[]foo</paragraph>' );
170+
171+
const targetRange = editor.model.document.selection.getFirstRange();
172+
const targetViewRange = editor.editing.mapper.toViewRange( targetRange );
173+
174+
editor.isReadOnly = true;
175+
176+
editor.editing.view.document.fire( 'clipboardInput', { dataTransfer, targetRanges: [ targetViewRange ] } );
177+
178+
expect( getModelData( editor.model ) ).to.equal( '<paragraph>[]foo</paragraph>' );
179+
180+
return editor.destroy();
181+
} );
182+
} );
183+
184+
it( 'should not insert image when file is not an image', () => {
143185
const viewDocument = editor.editing.view.document;
144186
const fileMock = {
145187
type: 'media/mp3',
@@ -149,16 +191,15 @@ describe( 'ImageUploadEditing', () => {
149191

150192
setModelData( model, '<paragraph>foo[]</paragraph>' );
151193

152-
const targetRange = Range.createFromParentsAndOffsets( doc.getRoot(), 1, doc.getRoot(), 1 );
194+
const targetRange = doc.selection.getFirstRange();
153195
const targetViewRange = editor.editing.mapper.toViewRange( targetRange );
154196

155197
viewDocument.fire( 'clipboardInput', { dataTransfer, targetRanges: [ targetViewRange ] } );
156198

157-
sinon.assert.notCalled( spy );
199+
expect( getModelData( model ) ).to.equal( '<paragraph>foo[]</paragraph>' );
158200
} );
159201

160-
it( 'should not execute imageUpload command when there is non-empty HTML content pasted', () => {
161-
const spy = sinon.spy( editor, 'execute' );
202+
it( 'should not insert image when there is non-empty HTML content pasted', () => {
162203
const fileMock = createNativeFileMock();
163204
const dataTransfer = new DataTransfer( {
164205
files: [ fileMock ],
@@ -172,7 +213,49 @@ describe( 'ImageUploadEditing', () => {
172213

173214
viewDocument.fire( 'clipboardInput', { dataTransfer, targetRanges: [ targetViewRange ] } );
174215

175-
sinon.assert.notCalled( spy );
216+
expect( getModelData( model ) ).to.equal( '<paragraph>[]foo</paragraph>' );
217+
} );
218+
219+
it( 'should not insert image nor crash when pasted image could not be inserted', () => {
220+
model.schema.register( 'other', {
221+
allowIn: '$root',
222+
isLimit: true
223+
} );
224+
model.schema.extend( '$text', { allowIn: 'other' } );
225+
226+
editor.conversion.elementToElement( { model: 'other', view: 'p' } );
227+
228+
setModelData( model, '<other>[]</other>' );
229+
230+
const fileMock = createNativeFileMock();
231+
const dataTransfer = new DataTransfer( { files: [ fileMock ], types: [ 'Files' ] } );
232+
233+
const targetRange = doc.selection.getFirstRange();
234+
const targetViewRange = editor.editing.mapper.toViewRange( targetRange );
235+
236+
viewDocument.fire( 'clipboardInput', { dataTransfer, targetRanges: [ targetViewRange ] } );
237+
238+
expect( getModelData( model ) ).to.equal( '<other>[]</other>' );
239+
} );
240+
241+
it( 'should not throw when upload adapter is not set (FileRepository will log an error anyway) when image is pasted', () => {
242+
const fileMock = createNativeFileMock();
243+
const dataTransfer = new DataTransfer( { files: [ fileMock ], types: [ 'Files' ] } );
244+
const logStub = testUtils.sinon.stub( log, 'error' );
245+
246+
setModelData( model, '<paragraph>[]foo</paragraph>' );
247+
248+
fileRepository.createUploadAdapter = undefined;
249+
250+
const targetRange = Range.createFromParentsAndOffsets( doc.getRoot(), 1, doc.getRoot(), 1 );
251+
const targetViewRange = editor.editing.mapper.toViewRange( targetRange );
252+
253+
expect( () => {
254+
viewDocument.fire( 'clipboardInput', { dataTransfer, targetRanges: [ targetViewRange ] } );
255+
} ).to.not.throw();
256+
257+
expect( getModelData( model ) ).to.equal( '<paragraph>[]foo</paragraph>' );
258+
sinon.assert.calledOnce( logStub );
176259
} );
177260

178261
// https://github.com/ckeditor/ckeditor5-upload/issues/70

0 commit comments

Comments
 (0)