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

Commit 938f0f7

Browse files
authored
Merge pull request #93 from ckeditor/t/ckeditor5/1378
Feature: Introduced the Base64 image upload adapter. Closes ckeditor/ckeditor5#1378.
2 parents d917b64 + 66a7496 commit 938f0f7

File tree

6 files changed

+375
-0
lines changed

6 files changed

+375
-0
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
<div id="snippet-image-base64-upload">
2+
<p>Paste or drop an image directly into the editor. You can also use the "Insert image" button in the toolbar.</p>
3+
</div>
4+
5+
<button type="button" id="log-data">Log editor data</button>
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
/**
2+
* @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
3+
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
4+
*/
5+
6+
/* globals console, window, document */
7+
8+
import ClassicEditor from '@ckeditor/ckeditor5-build-classic/src/ckeditor';
9+
import Base64UploadAdapter from '@ckeditor/ckeditor5-upload/src/base64uploadadapter';
10+
11+
ClassicEditor.builtinPlugins.push( Base64UploadAdapter );
12+
13+
ClassicEditor
14+
.create( document.querySelector( '#snippet-image-base64-upload' ), {
15+
toolbar: {
16+
viewportTopOffset: window.getViewportTopOffsetConfig()
17+
}
18+
} )
19+
.then( editor => {
20+
window.editor = editor;
21+
} )
22+
.catch( err => {
23+
console.error( err.stack );
24+
} );
25+
26+
// The "Log editor data" button logic.
27+
document.querySelector( '#log-data' ).addEventListener( 'click', () => {
28+
console.log( window.editor.getData() );
29+
} );

docs/api/upload.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,12 @@ This package implements various file upload utilities for CKEditor 5.
1212

1313
See the {@link module:upload/filerepository~FileRepository} plugin documentation.
1414

15+
## Upload Adapters
16+
17+
This repository contains the following upload adapters:
18+
19+
* {@link module:upload/base64uploadadapter~Base64UploadAdapter `Base64UploadAdapter`} - plugin that converts images inserted into the editor into [Base64 strings](https://en.wikipedia.org/wiki/Base64) in the {@glink builds/guides/integration/saving-data editor output}.
20+
1521
## Installation
1622

1723
```bash
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
---
2+
category: features-image-upload
3+
menu-title: Base64 upload adapter
4+
order: 40
5+
---
6+
7+
# Base64 upload adapter
8+
9+
The {@link module:upload/base64uploadadapter~Base64UploadAdapter Base64 image upload adapter} plugin converts images inserted into the editor into [Base64 strings](https://en.wikipedia.org/wiki/Base64) in the {@link builds/guides/integration/saving-data editor output}.
10+
11+
This kind of image upload does not require server processing – images are stored with the rest of the text and displayed by the web browser without additional requests. On the downside, this approach can bloat your database with very long data strings which, in theory, could have a negative impact on the performance.
12+
13+
<info-box>
14+
Check out the comprehensive {@link features/image-upload Image upload overview} to learn about other ways to upload images into CKEditor 5.
15+
</info-box>
16+
17+
## Example
18+
19+
Use the editor below to see the adapter in action. Open the web browser console and click the button below to see the base64–encoded image in the editor output data.
20+
21+
{@snippet features/base64-upload}
22+
23+
## Installation
24+
25+
<info-box info>
26+
The [`@ckeditor/ckeditor5-upload`](https://www.npmjs.com/package/@ckeditor/ckeditor5-upload) package is available by default in all builds. The installation instructions are for developers interested in building their own, custom WYSIWYG editor.
27+
</info-box>
28+
29+
To add this feature to your editor, install the [`@ckeditor/ckeditor5-upload`](https://www.npmjs.com/package/@ckeditor/ckeditor5-upload) package:
30+
31+
```bash
32+
npm install --save @ckeditor/ckeditor5-upload
33+
```
34+
35+
Then add the {@link module:upload/base64uploadadapter~Base64UploadAdapter `Base64UploadAdapter`} to your plugin list:
36+
37+
```js
38+
import Base64UploadAdapter from '@ckeditor/ckeditor5-upload/src/base64uploadadapter';
39+
40+
ClassicEditor
41+
.create( document.querySelector( '#editor' ), {
42+
plugins: [ Base64UploadAdapter, ... ],
43+
toolbar: [ ... ]
44+
} )
45+
.then( ... )
46+
.catch( ... );
47+
```
48+
49+
Once enabled in the plugin list, the upload adapter works out–of–the–box without additional configuration.
50+
51+
<info-box info>
52+
Read more about {@link builds/guides/integration/installing-plugins installing plugins}.
53+
</info-box>
54+
55+
## What's next?
56+
57+
Check out the comprehensive {@link features/image-upload Image upload overview} to learn more about different ways of uploading images in CKEditor 5.
58+
59+
See the {@link features/image Image feature} guide to find out more about handling images in CKEditor 5.
60+
61+
## Contribute
62+
63+
The source code of the feature is available on GitHub in https://github.com/ckeditor/ckeditor5-upload.

src/base64uploadadapter.js

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
/**
2+
* @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
3+
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
4+
*/
5+
6+
/**
7+
* @module upload/base64uploadadapter
8+
*/
9+
10+
/* globals window */
11+
12+
import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
13+
import FileRepository from './filerepository';
14+
15+
/**
16+
* A plugin that converts images inserted into the editor into [Base64 strings](https://en.wikipedia.org/wiki/Base64)
17+
* in the {@glink builds/guides/integration/saving-data editor output}.
18+
*
19+
* This kind of image upload does not require server processing – images are stored with the rest of the text and
20+
* displayed by the web browser without additional requests.
21+
*
22+
* Check out the {@glink features/image-upload/image-upload comprehensive "Image upload overview"} to learn about
23+
* other ways to upload images into CKEditor 5.
24+
*
25+
* @extends module:core/plugin~Plugin
26+
*/
27+
export default class Base64UploadAdapter extends Plugin {
28+
/**
29+
* @inheritDoc
30+
*/
31+
static get requires() {
32+
return [ FileRepository ];
33+
}
34+
35+
/**
36+
* @inheritDoc
37+
*/
38+
static get pluginName() {
39+
return 'Base64UploadAdapter';
40+
}
41+
42+
/**
43+
* @inheritDoc
44+
*/
45+
init() {
46+
this.editor.plugins.get( FileRepository ).createUploadAdapter = loader => new Adapter( loader );
47+
}
48+
}
49+
50+
/**
51+
* The upload adapter that converts images inserted into the editor into Base64 strings.
52+
*
53+
* @private
54+
* @implements module:upload/filerepository~UploadAdapter
55+
*/
56+
class Adapter {
57+
/**
58+
* Creates a new adapter instance.
59+
*
60+
* @param {module:upload/filerepository~FileLoader} loader
61+
*/
62+
constructor( loader ) {
63+
/**
64+
* `FileLoader` instance to use during the upload.
65+
*
66+
* @member {module:upload/filerepository~FileLoader} #loader
67+
*/
68+
this.loader = loader;
69+
}
70+
71+
/**
72+
* Starts the upload process.
73+
*
74+
* @see module:upload/filerepository~UploadAdapter#upload
75+
* @returns {Promise}
76+
*/
77+
upload() {
78+
return new Promise( ( resolve, reject ) => {
79+
const reader = this.reader = new window.FileReader();
80+
81+
reader.addEventListener( 'load', () => {
82+
resolve( { default: reader.result } );
83+
} );
84+
85+
reader.addEventListener( 'error', err => {
86+
reject( err );
87+
} );
88+
89+
reader.addEventListener( 'abort', () => {
90+
reject();
91+
} );
92+
93+
this.loader.file.then( file => {
94+
reader.readAsDataURL( file );
95+
} );
96+
} );
97+
}
98+
99+
/**
100+
* Aborts the upload process.
101+
*
102+
* @see module:upload/filerepository~UploadAdapter#abort
103+
* @returns {Promise}
104+
*/
105+
abort() {
106+
this.reader.abort();
107+
}
108+
}

tests/base64uploadadapter.js

Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
/**
2+
* @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
3+
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
4+
*/
5+
6+
/* globals window, setTimeout */
7+
8+
import Base64UploadAdapter from '../src/base64uploadadapter';
9+
import FileRepository from '../src/filerepository';
10+
import ClassicTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/classictesteditor';
11+
import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils';
12+
import { createNativeFileMock } from './_utils/mocks';
13+
14+
describe( 'Base64UploadAdapter', () => {
15+
let div, stubs;
16+
17+
testUtils.createSinonSandbox();
18+
19+
beforeEach( () => {
20+
div = window.document.createElement( 'div' );
21+
window.document.body.appendChild( div );
22+
23+
stubs = {
24+
addEventListener( event, callback ) {
25+
stubs[ `on${ event }` ] = callback;
26+
},
27+
readAsDataURL: testUtils.sinon.spy(),
28+
abort: testUtils.sinon.spy(),
29+
result: 'data:image/png;base64'
30+
};
31+
32+
testUtils.sinon.stub( window, 'FileReader' ).callsFake( function FileReader() {
33+
return stubs;
34+
} );
35+
} );
36+
37+
afterEach( () => {
38+
window.document.body.removeChild( div );
39+
} );
40+
41+
it( 'should require the FileRepository plugin', () => {
42+
expect( Base64UploadAdapter.requires ).to.deep.equal( [ FileRepository ] );
43+
} );
44+
45+
it( 'should be named', () => {
46+
expect( Base64UploadAdapter.pluginName ).to.equal( 'Base64UploadAdapter' );
47+
} );
48+
49+
describe( 'init()', () => {
50+
it( 'should set the loader', () => {
51+
return ClassicTestEditor
52+
.create( div, {
53+
plugins: [ Base64UploadAdapter ],
54+
} )
55+
.then( editor => {
56+
expect( editor.plugins.get( FileRepository ).createUploadAdapter ).is.a( 'function' );
57+
58+
return editor.destroy();
59+
} );
60+
} );
61+
} );
62+
63+
describe( 'Adapter', () => {
64+
let editor, fileRepository, adapter;
65+
66+
beforeEach( () => {
67+
return ClassicTestEditor.create( div, {
68+
plugins: [ Base64UploadAdapter ],
69+
} ).then( _editor => {
70+
editor = _editor;
71+
fileRepository = editor.plugins.get( FileRepository );
72+
adapter = fileRepository.createLoader( createNativeFileMock() );
73+
} );
74+
} );
75+
76+
afterEach( () => {
77+
return editor.destroy();
78+
} );
79+
80+
it( 'crateAdapter method should be registered and have upload() and abort() methods', () => {
81+
expect( adapter ).to.not.be.undefined;
82+
expect( adapter.upload ).to.be.a( 'function' );
83+
expect( adapter.abort ).to.be.a( 'function' );
84+
} );
85+
86+
describe( 'upload()', () => {
87+
it( 'returns a promise that resolves an image as a base64 string', () => {
88+
setTimeout( () => {
89+
// FileReader has loaded the file.
90+
stubs.onload();
91+
} );
92+
93+
return adapter.upload()
94+
.then( response => {
95+
expect( response.default ).to.equal( 'data:image/png;base64' );
96+
expect( stubs.readAsDataURL.calledOnce ).to.equal( true );
97+
} );
98+
} );
99+
100+
it( 'returns a promise that rejects if something went wrong', () => {
101+
const uploadError = new Error( 'Something went wrong.' );
102+
103+
setTimeout( () => {
104+
// An error occurred while FileReader was reading the file.
105+
stubs.onerror( uploadError );
106+
} );
107+
108+
return adapter.upload()
109+
.then(
110+
() => {
111+
return new Error( 'Supposed to be rejected.' );
112+
},
113+
err => {
114+
expect( err ).to.equal( uploadError );
115+
expect( stubs.readAsDataURL.calledOnce ).to.equal( true );
116+
}
117+
);
118+
} );
119+
120+
it( 'returns a promise that rejects if FileReader aborted reading a file', () => {
121+
setTimeout( () => {
122+
// FileReader aborted reading the file.
123+
stubs.onabort();
124+
} );
125+
126+
return adapter.upload()
127+
.then(
128+
() => {
129+
return new Error( 'Supposed to be rejected.' );
130+
},
131+
() => {
132+
expect( stubs.readAsDataURL.calledOnce ).to.equal( true );
133+
}
134+
);
135+
} );
136+
} );
137+
138+
describe( 'abort()', () => {
139+
it( 'should not call abort() on the non-existing FileReader uploader (loader#file not resolved)', () => {
140+
const adapter = fileRepository.createLoader( createNativeFileMock() );
141+
142+
expect( () => {
143+
adapter.upload();
144+
adapter.abort();
145+
} ).to.not.throw();
146+
147+
expect( stubs.abort.called ).to.equal( false );
148+
} );
149+
150+
it( 'should call abort() on the FileReader uploader (loader#file resolved)', done => {
151+
adapter.upload();
152+
153+
// Wait for the `loader.file` promise.
154+
setTimeout( () => {
155+
adapter.abort();
156+
157+
expect( stubs.abort.called ).to.equal( true );
158+
159+
done();
160+
} );
161+
} );
162+
} );
163+
} );
164+
} );

0 commit comments

Comments
 (0)