imageuploadprogress.js 9.9 KB

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