UNPKG

@ckeditor/ckeditor5-image

Version:

Image feature for CKEditor 5.

212 lines (211 loc) • 8.64 kB
/** * @license Copyright (c) 2003-2023, CKSource Holding sp. z o.o. All rights reserved. * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license */ /** * @module image/imageupload/imageuploadprogress */ /* globals setTimeout */ import { Plugin } from 'ckeditor5/src/core'; import { FileRepository } from 'ckeditor5/src/upload'; import '../../theme/imageuploadprogress.css'; import '../../theme/imageuploadicon.css'; import '../../theme/imageuploadloader.css'; /** * The image upload progress plugin. * It shows a placeholder when the image is read from the disk and a progress bar while the image is uploading. */ export default class ImageUploadProgress extends Plugin { /** * @inheritDoc */ static get pluginName() { return 'ImageUploadProgress'; } /** * @inheritDoc */ constructor(editor) { super(editor); /** * This method is called each time the image `uploadStatus` attribute is changed. * * @param evt An object containing information about the fired event. * @param data Additional information about the change. */ this.uploadStatusChange = (evt, data, conversionApi) => { const editor = this.editor; const modelImage = data.item; const uploadId = modelImage.getAttribute('uploadId'); if (!conversionApi.consumable.consume(data.item, evt.name)) { return; } const imageUtils = editor.plugins.get('ImageUtils'); const fileRepository = editor.plugins.get(FileRepository); const status = uploadId ? data.attributeNewValue : null; const placeholder = this.placeholder; const viewFigure = editor.editing.mapper.toViewElement(modelImage); const viewWriter = conversionApi.writer; if (status == 'reading') { // Start "appearing" effect and show placeholder with infinite progress bar on the top // while image is read from disk. _startAppearEffect(viewFigure, viewWriter); _showPlaceholder(imageUtils, placeholder, viewFigure, viewWriter); return; } // Show progress bar on the top of the image when image is uploading. if (status == 'uploading') { const loader = fileRepository.loaders.get(uploadId); // Start appear effect if needed - see https://github.com/ckeditor/ckeditor5-image/issues/191. _startAppearEffect(viewFigure, viewWriter); if (!loader) { // There is no loader associated with uploadId - this means that image came from external changes. // In such cases we still want to show the placeholder until image is fully uploaded. // Show placeholder if needed - see https://github.com/ckeditor/ckeditor5-image/issues/191. _showPlaceholder(imageUtils, placeholder, viewFigure, viewWriter); } else { // Hide placeholder and initialize progress bar showing upload progress. _hidePlaceholder(viewFigure, viewWriter); _showProgressBar(viewFigure, viewWriter, loader, editor.editing.view); _displayLocalImage(imageUtils, viewFigure, viewWriter, loader); } return; } if (status == 'complete' && fileRepository.loaders.get(uploadId)) { _showCompleteIcon(viewFigure, viewWriter, editor.editing.view); } // Clean up. _hideProgressBar(viewFigure, viewWriter); _hidePlaceholder(viewFigure, viewWriter); _stopAppearEffect(viewFigure, viewWriter); }; this.placeholder = 'data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw=='; } /** * @inheritDoc */ init() { const editor = this.editor; // Upload status change - update image's view according to that status. if (editor.plugins.has('ImageBlockEditing')) { editor.editing.downcastDispatcher.on('attribute:uploadStatus:imageBlock', this.uploadStatusChange); } if (editor.plugins.has('ImageInlineEditing')) { editor.editing.downcastDispatcher.on('attribute:uploadStatus:imageInline', this.uploadStatusChange); } } } /** * Adds ck-appear class to the image figure if one is not already applied. */ function _startAppearEffect(viewFigure, writer) { if (!viewFigure.hasClass('ck-appear')) { writer.addClass('ck-appear', viewFigure); } } /** * Removes ck-appear class to the image figure if one is not already removed. */ function _stopAppearEffect(viewFigure, writer) { writer.removeClass('ck-appear', viewFigure); } /** * Shows placeholder together with infinite progress bar on given image figure. */ function _showPlaceholder(imageUtils, placeholder, viewFigure, writer) { if (!viewFigure.hasClass('ck-image-upload-placeholder')) { writer.addClass('ck-image-upload-placeholder', viewFigure); } const viewImg = imageUtils.findViewImgElement(viewFigure); if (viewImg.getAttribute('src') !== placeholder) { writer.setAttribute('src', placeholder, viewImg); } if (!_getUIElement(viewFigure, 'placeholder')) { writer.insert(writer.createPositionAfter(viewImg), _createPlaceholder(writer)); } } /** * Removes placeholder together with infinite progress bar on given image figure. */ function _hidePlaceholder(viewFigure, writer) { if (viewFigure.hasClass('ck-image-upload-placeholder')) { writer.removeClass('ck-image-upload-placeholder', viewFigure); } _removeUIElement(viewFigure, writer, 'placeholder'); } /** * Shows progress bar displaying upload progress. * Attaches it to the file loader to update when upload percentace is changed. */ function _showProgressBar(viewFigure, writer, loader, view) { const progressBar = _createProgressBar(writer); writer.insert(writer.createPositionAt(viewFigure, 'end'), progressBar); // Update progress bar width when uploadedPercent is changed. loader.on('change:uploadedPercent', (evt, name, value) => { view.change(writer => { writer.setStyle('width', value + '%', progressBar); }); }); } /** * Hides upload progress bar. */ function _hideProgressBar(viewFigure, writer) { _removeUIElement(viewFigure, writer, 'progressBar'); } /** * Shows complete icon and hides after a certain amount of time. */ function _showCompleteIcon(viewFigure, writer, view) { const completeIcon = writer.createUIElement('div', { class: 'ck-image-upload-complete-icon' }); writer.insert(writer.createPositionAt(viewFigure, 'end'), completeIcon); setTimeout(() => { view.change(writer => writer.remove(writer.createRangeOn(completeIcon))); }, 3000); } /** * Create progress bar element using {@link module:engine/view/uielement~UIElement}. */ function _createProgressBar(writer) { const progressBar = writer.createUIElement('div', { class: 'ck-progress-bar' }); writer.setCustomProperty('progressBar', true, progressBar); return progressBar; } /** * Create placeholder element using {@link module:engine/view/uielement~UIElement}. */ function _createPlaceholder(writer) { const placeholder = writer.createUIElement('div', { class: 'ck-upload-placeholder-loader' }); writer.setCustomProperty('placeholder', true, placeholder); return placeholder; } /** * Returns {@link module:engine/view/uielement~UIElement} of given unique property from image figure element. * Returns `undefined` if element is not found. */ function _getUIElement(imageFigure, uniqueProperty) { for (const child of imageFigure.getChildren()) { if (child.getCustomProperty(uniqueProperty)) { return child; } } } /** * Removes {@link module:engine/view/uielement~UIElement} of given unique property from image figure element. */ function _removeUIElement(viewFigure, writer, uniqueProperty) { const element = _getUIElement(viewFigure, uniqueProperty); if (element) { writer.remove(writer.createRangeOn(element)); } } /** * Displays local data from file loader. */ function _displayLocalImage(imageUtils, viewFigure, writer, loader) { if (loader.data) { const viewImg = imageUtils.findViewImgElement(viewFigure); writer.setAttribute('src', loader.data, viewImg); } }