UNPKG

8.65 kBJavaScriptView Raw
1/**
2 * @license Copyright (c) 2003-2024, 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 * @module image/imageupload/imageuploadprogress
7 */
8/* globals setTimeout */
9import { Plugin } from 'ckeditor5/src/core.js';
10import { FileRepository } from 'ckeditor5/src/upload.js';
11import '../../theme/imageuploadprogress.css';
12import '../../theme/imageuploadicon.css';
13import '../../theme/imageuploadloader.css';
14/**
15 * The image upload progress plugin.
16 * It shows a placeholder when the image is read from the disk and a progress bar while the image is uploading.
17 */
18export default class ImageUploadProgress extends Plugin {
19 /**
20 * @inheritDoc
21 */
22 static get pluginName() {
23 return 'ImageUploadProgress';
24 }
25 /**
26 * @inheritDoc
27 */
28 constructor(editor) {
29 super(editor);
30 /**
31 * This method is called each time the image `uploadStatus` attribute is changed.
32 *
33 * @param evt An object containing information about the fired event.
34 * @param data Additional information about the change.
35 */
36 this.uploadStatusChange = (evt, data, conversionApi) => {
37 const editor = this.editor;
38 const modelImage = data.item;
39 const uploadId = modelImage.getAttribute('uploadId');
40 if (!conversionApi.consumable.consume(data.item, evt.name)) {
41 return;
42 }
43 const imageUtils = editor.plugins.get('ImageUtils');
44 const fileRepository = editor.plugins.get(FileRepository);
45 const status = uploadId ? data.attributeNewValue : null;
46 const placeholder = this.placeholder;
47 const viewFigure = editor.editing.mapper.toViewElement(modelImage);
48 const viewWriter = conversionApi.writer;
49 if (status == 'reading') {
50 // Start "appearing" effect and show placeholder with infinite progress bar on the top
51 // while image is read from disk.
52 _startAppearEffect(viewFigure, viewWriter);
53 _showPlaceholder(imageUtils, placeholder, viewFigure, viewWriter);
54 return;
55 }
56 // Show progress bar on the top of the image when image is uploading.
57 if (status == 'uploading') {
58 const loader = fileRepository.loaders.get(uploadId);
59 // Start appear effect if needed - see https://github.com/ckeditor/ckeditor5-image/issues/191.
60 _startAppearEffect(viewFigure, viewWriter);
61 if (!loader) {
62 // There is no loader associated with uploadId - this means that image came from external changes.
63 // In such cases we still want to show the placeholder until image is fully uploaded.
64 // Show placeholder if needed - see https://github.com/ckeditor/ckeditor5-image/issues/191.
65 _showPlaceholder(imageUtils, placeholder, viewFigure, viewWriter);
66 }
67 else {
68 // Hide placeholder and initialize progress bar showing upload progress.
69 _hidePlaceholder(viewFigure, viewWriter);
70 _showProgressBar(viewFigure, viewWriter, loader, editor.editing.view);
71 _displayLocalImage(imageUtils, viewFigure, viewWriter, loader);
72 }
73 return;
74 }
75 if (status == 'complete' && fileRepository.loaders.get(uploadId)) {
76 _showCompleteIcon(viewFigure, viewWriter, editor.editing.view);
77 }
78 // Clean up.
79 _hideProgressBar(viewFigure, viewWriter);
80 _hidePlaceholder(viewFigure, viewWriter);
81 _stopAppearEffect(viewFigure, viewWriter);
82 };
83 this.placeholder = 'data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==';
84 }
85 /**
86 * @inheritDoc
87 */
88 init() {
89 const editor = this.editor;
90 // Upload status change - update image's view according to that status.
91 if (editor.plugins.has('ImageBlockEditing')) {
92 editor.editing.downcastDispatcher.on('attribute:uploadStatus:imageBlock', this.uploadStatusChange);
93 }
94 if (editor.plugins.has('ImageInlineEditing')) {
95 editor.editing.downcastDispatcher.on('attribute:uploadStatus:imageInline', this.uploadStatusChange);
96 }
97 }
98}
99/**
100 * Adds ck-appear class to the image figure if one is not already applied.
101 */
102function _startAppearEffect(viewFigure, writer) {
103 if (!viewFigure.hasClass('ck-appear')) {
104 writer.addClass('ck-appear', viewFigure);
105 }
106}
107/**
108 * Removes ck-appear class to the image figure if one is not already removed.
109 */
110function _stopAppearEffect(viewFigure, writer) {
111 writer.removeClass('ck-appear', viewFigure);
112}
113/**
114 * Shows placeholder together with infinite progress bar on given image figure.
115 */
116function _showPlaceholder(imageUtils, placeholder, viewFigure, writer) {
117 if (!viewFigure.hasClass('ck-image-upload-placeholder')) {
118 writer.addClass('ck-image-upload-placeholder', viewFigure);
119 }
120 const viewImg = imageUtils.findViewImgElement(viewFigure);
121 if (viewImg.getAttribute('src') !== placeholder) {
122 writer.setAttribute('src', placeholder, viewImg);
123 }
124 if (!_getUIElement(viewFigure, 'placeholder')) {
125 writer.insert(writer.createPositionAfter(viewImg), _createPlaceholder(writer));
126 }
127}
128/**
129 * Removes placeholder together with infinite progress bar on given image figure.
130 */
131function _hidePlaceholder(viewFigure, writer) {
132 if (viewFigure.hasClass('ck-image-upload-placeholder')) {
133 writer.removeClass('ck-image-upload-placeholder', viewFigure);
134 }
135 _removeUIElement(viewFigure, writer, 'placeholder');
136}
137/**
138 * Shows progress bar displaying upload progress.
139 * Attaches it to the file loader to update when upload percentace is changed.
140 */
141function _showProgressBar(viewFigure, writer, loader, view) {
142 const progressBar = _createProgressBar(writer);
143 writer.insert(writer.createPositionAt(viewFigure, 'end'), progressBar);
144 // Update progress bar width when uploadedPercent is changed.
145 loader.on('change:uploadedPercent', (evt, name, value) => {
146 view.change(writer => {
147 writer.setStyle('width', value + '%', progressBar);
148 });
149 });
150}
151/**
152 * Hides upload progress bar.
153 */
154function _hideProgressBar(viewFigure, writer) {
155 _removeUIElement(viewFigure, writer, 'progressBar');
156}
157/**
158 * Shows complete icon and hides after a certain amount of time.
159 */
160function _showCompleteIcon(viewFigure, writer, view) {
161 const completeIcon = writer.createUIElement('div', { class: 'ck-image-upload-complete-icon' });
162 writer.insert(writer.createPositionAt(viewFigure, 'end'), completeIcon);
163 setTimeout(() => {
164 view.change(writer => writer.remove(writer.createRangeOn(completeIcon)));
165 }, 3000);
166}
167/**
168 * Create progress bar element using {@link module:engine/view/uielement~UIElement}.
169 */
170function _createProgressBar(writer) {
171 const progressBar = writer.createUIElement('div', { class: 'ck-progress-bar' });
172 writer.setCustomProperty('progressBar', true, progressBar);
173 return progressBar;
174}
175/**
176 * Create placeholder element using {@link module:engine/view/uielement~UIElement}.
177 */
178function _createPlaceholder(writer) {
179 const placeholder = writer.createUIElement('div', { class: 'ck-upload-placeholder-loader' });
180 writer.setCustomProperty('placeholder', true, placeholder);
181 return placeholder;
182}
183/**
184 * Returns {@link module:engine/view/uielement~UIElement} of given unique property from image figure element.
185 * Returns `undefined` if element is not found.
186 */
187function _getUIElement(imageFigure, uniqueProperty) {
188 for (const child of imageFigure.getChildren()) {
189 if (child.getCustomProperty(uniqueProperty)) {
190 return child;
191 }
192 }
193}
194/**
195 * Removes {@link module:engine/view/uielement~UIElement} of given unique property from image figure element.
196 */
197function _removeUIElement(viewFigure, writer, uniqueProperty) {
198 const element = _getUIElement(viewFigure, uniqueProperty);
199 if (element) {
200 writer.remove(writer.createRangeOn(element));
201 }
202}
203/**
204 * Displays local data from file loader.
205 */
206function _displayLocalImage(imageUtils, viewFigure, writer, loader) {
207 if (loader.data) {
208 const viewImg = imageUtils.findViewImgElement(viewFigure);
209 writer.setAttribute('src', loader.data, viewImg);
210 }
211}