UNPKG

10.5 kBJavaScriptView Raw
1/**
2 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
3 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
4 */
5
6/**
7 * @module image/imageupload/imageuploadprogress
8 */
9
10/* globals setTimeout */
11
12import { Plugin } from 'ckeditor5/src/core';
13import { FileRepository } from 'ckeditor5/src/upload';
14
15import '../../theme/imageuploadprogress.css';
16import '../../theme/imageuploadicon.css';
17import '../../theme/imageuploadloader.css';
18
19/**
20 * The image upload progress plugin.
21 * It shows a placeholder when the image is read from the disk and a progress bar while the image is uploading.
22 *
23 * @extends module:core/plugin~Plugin
24 */
25export default class ImageUploadProgress extends Plugin {
26 /**
27 * @inheritDoc
28 */
29 static get pluginName() {
30 return 'ImageUploadProgress';
31 }
32
33 /**
34 * @inheritDoc
35 */
36 constructor( editor ) {
37 super( editor );
38
39 /**
40 * The image placeholder that is displayed before real image data can be accessed.
41 *
42 * For the record, this image is a 1x1 px GIF with an aspect ratio set by CSS.
43 *
44 * @protected
45 * @member {String} #placeholder
46 */
47 this.placeholder = 'data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==';
48 }
49
50 /**
51 * @inheritDoc
52 */
53 init() {
54 const editor = this.editor;
55
56 // Upload status change - update image's view according to that status.
57 if ( editor.plugins.has( 'ImageBlockEditing' ) ) {
58 editor.editing.downcastDispatcher.on( 'attribute:uploadStatus:imageBlock', ( ...args ) => this.uploadStatusChange( ...args ) );
59 }
60
61 if ( editor.plugins.has( 'ImageInlineEditing' ) ) {
62 editor.editing.downcastDispatcher.on( 'attribute:uploadStatus:imageInline', ( ...args ) => this.uploadStatusChange( ...args ) );
63 }
64 }
65
66 /**
67 * This method is called each time the image `uploadStatus` attribute is changed.
68 *
69 * @param {module:utils/eventinfo~EventInfo} evt An object containing information about the fired event.
70 * @param {Object} data Additional information about the change.
71 * @param {module:engine/conversion/downcastdispatcher~DowncastConversionApi} conversionApi
72 */
73 uploadStatusChange( evt, data, conversionApi ) {
74 const editor = this.editor;
75 const modelImage = data.item;
76 const uploadId = modelImage.getAttribute( 'uploadId' );
77
78 if ( !conversionApi.consumable.consume( data.item, evt.name ) ) {
79 return;
80 }
81
82 const imageUtils = editor.plugins.get( 'ImageUtils' );
83 const fileRepository = editor.plugins.get( FileRepository );
84 const status = uploadId ? data.attributeNewValue : null;
85 const placeholder = this.placeholder;
86 const viewFigure = editor.editing.mapper.toViewElement( modelImage );
87 const viewWriter = conversionApi.writer;
88
89 if ( status == 'reading' ) {
90 // Start "appearing" effect and show placeholder with infinite progress bar on the top
91 // while image is read from disk.
92 _startAppearEffect( viewFigure, viewWriter );
93 _showPlaceholder( imageUtils, placeholder, viewFigure, viewWriter );
94
95 return;
96 }
97
98 // Show progress bar on the top of the image when image is uploading.
99 if ( status == 'uploading' ) {
100 const loader = fileRepository.loaders.get( uploadId );
101
102 // Start appear effect if needed - see https://github.com/ckeditor/ckeditor5-image/issues/191.
103 _startAppearEffect( viewFigure, viewWriter );
104
105 if ( !loader ) {
106 // There is no loader associated with uploadId - this means that image came from external changes.
107 // In such cases we still want to show the placeholder until image is fully uploaded.
108 // Show placeholder if needed - see https://github.com/ckeditor/ckeditor5-image/issues/191.
109 _showPlaceholder( imageUtils, placeholder, viewFigure, viewWriter );
110 } else {
111 // Hide placeholder and initialize progress bar showing upload progress.
112 _hidePlaceholder( viewFigure, viewWriter );
113 _showProgressBar( viewFigure, viewWriter, loader, editor.editing.view );
114 _displayLocalImage( imageUtils, viewFigure, viewWriter, loader );
115 }
116
117 return;
118 }
119
120 if ( status == 'complete' && fileRepository.loaders.get( uploadId ) ) {
121 _showCompleteIcon( viewFigure, viewWriter, editor.editing.view );
122 }
123
124 // Clean up.
125 _hideProgressBar( viewFigure, viewWriter );
126 _hidePlaceholder( viewFigure, viewWriter );
127 _stopAppearEffect( viewFigure, viewWriter );
128 }
129}
130
131// Adds ck-appear class to the image figure if one is not already applied.
132//
133// @param {module:engine/view/containerelement~ContainerElement} viewFigure
134// @param {module:engine/view/downcastwriter~DowncastWriter} writer
135function _startAppearEffect( viewFigure, writer ) {
136 if ( !viewFigure.hasClass( 'ck-appear' ) ) {
137 writer.addClass( 'ck-appear', viewFigure );
138 }
139}
140
141// Removes ck-appear class to the image figure if one is not already removed.
142//
143// @param {module:engine/view/containerelement~ContainerElement} viewFigure
144// @param {module:engine/view/downcastwriter~DowncastWriter} writer
145function _stopAppearEffect( viewFigure, writer ) {
146 writer.removeClass( 'ck-appear', viewFigure );
147}
148
149// Shows placeholder together with infinite progress bar on given image figure.
150//
151// @param {module:image/imageutils~ImageUtils} imageUtils
152// @param {String} Data-uri with a svg placeholder.
153// @param {module:engine/view/containerelement~ContainerElement} viewFigure
154// @param {module:engine/view/downcastwriter~DowncastWriter} writer
155function _showPlaceholder( imageUtils, placeholder, viewFigure, writer ) {
156 if ( !viewFigure.hasClass( 'ck-image-upload-placeholder' ) ) {
157 writer.addClass( 'ck-image-upload-placeholder', viewFigure );
158 }
159
160 const viewImg = imageUtils.findViewImgElement( viewFigure );
161
162 if ( viewImg.getAttribute( 'src' ) !== placeholder ) {
163 writer.setAttribute( 'src', placeholder, viewImg );
164 }
165
166 if ( !_getUIElement( viewFigure, 'placeholder' ) ) {
167 writer.insert( writer.createPositionAfter( viewImg ), _createPlaceholder( writer ) );
168 }
169}
170
171// Removes placeholder together with infinite progress bar on given image figure.
172//
173// @param {module:engine/view/containerelement~ContainerElement} viewFigure
174// @param {module:engine/view/downcastwriter~DowncastWriter} writer
175function _hidePlaceholder( viewFigure, writer ) {
176 if ( viewFigure.hasClass( 'ck-image-upload-placeholder' ) ) {
177 writer.removeClass( 'ck-image-upload-placeholder', viewFigure );
178 }
179
180 _removeUIElement( viewFigure, writer, 'placeholder' );
181}
182
183// Shows progress bar displaying upload progress.
184// Attaches it to the file loader to update when upload percentace is changed.
185//
186// @param {module:engine/view/containerelement~ContainerElement} viewFigure
187// @param {module:engine/view/downcastwriter~DowncastWriter} writer
188// @param {module:upload/filerepository~FileLoader} loader
189// @param {module:engine/view/view~View} view
190function _showProgressBar( viewFigure, writer, loader, view ) {
191 const progressBar = _createProgressBar( writer );
192 writer.insert( writer.createPositionAt( viewFigure, 'end' ), progressBar );
193
194 // Update progress bar width when uploadedPercent is changed.
195 loader.on( 'change:uploadedPercent', ( evt, name, value ) => {
196 view.change( writer => {
197 writer.setStyle( 'width', value + '%', progressBar );
198 } );
199 } );
200}
201
202// Hides upload progress bar.
203//
204// @param {module:engine/view/containerelement~ContainerElement} viewFigure
205// @param {module:engine/view/downcastwriter~DowncastWriter} writer
206function _hideProgressBar( viewFigure, writer ) {
207 _removeUIElement( viewFigure, writer, 'progressBar' );
208}
209
210// Shows complete icon and hides after a certain amount of time.
211//
212// @param {module:engine/view/containerelement~ContainerElement} viewFigure
213// @param {module:engine/view/downcastwriter~DowncastWriter} writer
214// @param {module:engine/view/view~View} view
215function _showCompleteIcon( viewFigure, writer, view ) {
216 const completeIcon = writer.createUIElement( 'div', { class: 'ck-image-upload-complete-icon' } );
217
218 writer.insert( writer.createPositionAt( viewFigure, 'end' ), completeIcon );
219
220 setTimeout( () => {
221 view.change( writer => writer.remove( writer.createRangeOn( completeIcon ) ) );
222 }, 3000 );
223}
224
225// Create progress bar element using {@link module:engine/view/uielement~UIElement}.
226//
227// @private
228// @param {module:engine/view/downcastwriter~DowncastWriter} writer
229// @returns {module:engine/view/uielement~UIElement}
230function _createProgressBar( writer ) {
231 const progressBar = writer.createUIElement( 'div', { class: 'ck-progress-bar' } );
232
233 writer.setCustomProperty( 'progressBar', true, progressBar );
234
235 return progressBar;
236}
237
238// Create placeholder element using {@link module:engine/view/uielement~UIElement}.
239//
240// @private
241// @param {module:engine/view/downcastwriter~DowncastWriter} writer
242// @returns {module:engine/view/uielement~UIElement}
243function _createPlaceholder( writer ) {
244 const placeholder = writer.createUIElement( 'div', { class: 'ck-upload-placeholder-loader' } );
245
246 writer.setCustomProperty( 'placeholder', true, placeholder );
247
248 return placeholder;
249}
250
251// Returns {@link module:engine/view/uielement~UIElement} of given unique property from image figure element.
252// Returns `undefined` if element is not found.
253//
254// @private
255// @param {module:engine/view/element~Element} imageFigure
256// @param {String} uniqueProperty
257// @returns {module:engine/view/uielement~UIElement|undefined}
258function _getUIElement( imageFigure, uniqueProperty ) {
259 for ( const child of imageFigure.getChildren() ) {
260 if ( child.getCustomProperty( uniqueProperty ) ) {
261 return child;
262 }
263 }
264}
265
266// Removes {@link module:engine/view/uielement~UIElement} of given unique property from image figure element.
267//
268// @private
269// @param {module:engine/view/element~Element} imageFigure
270// @param {module:engine/view/downcastwriter~DowncastWriter} writer
271// @param {String} uniqueProperty
272function _removeUIElement( viewFigure, writer, uniqueProperty ) {
273 const element = _getUIElement( viewFigure, uniqueProperty );
274
275 if ( element ) {
276 writer.remove( writer.createRangeOn( element ) );
277 }
278}
279
280// Displays local data from file loader.
281//
282// @param {module:image/imageutils~ImageUtils} imageUtils
283// @param {module:engine/view/element~Element} imageFigure
284// @param {module:engine/view/downcastwriter~DowncastWriter} writer
285// @param {module:upload/filerepository~FileLoader} loader
286function _displayLocalImage( imageUtils, viewFigure, writer, loader ) {
287 if ( loader.data ) {
288 const viewImg = imageUtils.findViewImgElement( viewFigure );
289
290 writer.setAttribute( 'src', loader.data, viewImg );
291 }
292}