imageuploadprogress.js 9.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271
  1. /**
  2. * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
  3. * For licensing, see LICENSE.md.
  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 UIElement from '@ckeditor/ckeditor5-engine/src/view/uielement';
  12. import ViewPosition from '@ckeditor/ckeditor5-engine/src/view/position';
  13. import ViewRange from '@ckeditor/ckeditor5-engine/src/view/range';
  14. import '../../theme/imageuploadprogress.css';
  15. import '../../theme/imageuploadicon.css';
  16. import '../../theme/imageuploadloader.css';
  17. // Data-uri with blank svg file that will be set as a img#src while placeholder is displayed with same width & height proportions.
  18. const blankImage = 'data:image/svg+xml;utf8,' + encodeURIComponent( '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 700 250"><g fill="none" fill-rule="evenodd"><rect width="700" height="250" rx="4"/></g></svg>' );
  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. */
  25. export default class ImageUploadProgress extends Plugin {
  26. /**
  27. * @inheritDoc
  28. */
  29. init() {
  30. const editor = this.editor;
  31. // Upload status change - update image's view according to that status.
  32. editor.editing.downcastDispatcher.on( 'attribute:uploadStatus:image', ( ...args ) => this.uploadStatusChange( ...args ) );
  33. }
  34. /**
  35. * This method is called each time the image `uploadStatus` attribute is changed.
  36. *
  37. * @param {module:utils/eventinfo~EventInfo} evt An object containing information about the fired event.
  38. * @param {Object} data Additional information about the change.
  39. * @param {module:engine/conversion/modelconsumable~ModelConsumable} consumable Values to consume.
  40. * @param {Object} conversionApi
  41. */
  42. uploadStatusChange( evt, data, conversionApi ) {
  43. const editor = this.editor;
  44. const modelImage = data.item;
  45. const uploadId = modelImage.getAttribute( 'uploadId' );
  46. if ( !conversionApi.consumable.consume( data.item, evt.name ) ) {
  47. return;
  48. }
  49. const fileRepository = editor.plugins.get( FileRepository );
  50. const status = uploadId ? data.attributeNewValue : null;
  51. const viewFigure = editor.editing.mapper.toViewElement( modelImage );
  52. const viewWriter = conversionApi.writer;
  53. if ( status == 'reading' ) {
  54. // Start "appearing" effect and show placeholder with infinite progress bar on the top
  55. // while image is read from disk.
  56. _startAppearEffect( viewFigure, viewWriter );
  57. _showPlaceholder( viewFigure, viewWriter );
  58. return;
  59. }
  60. // Show progress bar on the top of the image when image is uploading.
  61. if ( status == 'uploading' ) {
  62. const loader = fileRepository.loaders.get( uploadId );
  63. // Start appear effect if needed - see https://github.com/ckeditor/ckeditor5-image/issues/191.
  64. _startAppearEffect( viewFigure, viewWriter );
  65. if ( !loader ) {
  66. // There is no loader associated with uploadId - this means that image came from external changes.
  67. // In such cases we still want to show the placeholder until image is fully uploaded.
  68. // Show placeholder if needed - see https://github.com/ckeditor/ckeditor5-image/issues/191.
  69. _showPlaceholder( viewFigure, viewWriter );
  70. } else {
  71. // Hide placeholder and initialize progress bar showing upload progress.
  72. _hidePlaceholder( viewFigure, viewWriter );
  73. _showProgressBar( viewFigure, viewWriter, loader, editor.editing.view );
  74. }
  75. return;
  76. }
  77. if ( status == 'complete' && fileRepository.loaders.get( uploadId ) ) {
  78. _showCompleteIcon( viewFigure, viewWriter, editor.editing.view );
  79. }
  80. // Clean up.
  81. _hideProgressBar( viewFigure, viewWriter );
  82. _hidePlaceholder( viewFigure, viewWriter );
  83. _stopAppearEffect( viewFigure, viewWriter );
  84. }
  85. }
  86. // Symbol added to progress bar UIElement to distinguish it from other elements.
  87. const progressBarSymbol = Symbol( 'progress-bar' );
  88. // Symbol added to placeholder UIElement to distinguish it from other elements.
  89. const placeholderSymbol = Symbol( 'placeholder' );
  90. // Adds ck-appear class to the image figure if one is not already applied.
  91. //
  92. // @param {module:engine/view/containerelement~ContainerElement} viewFigure
  93. // @param {module:engine/view/writer~Writer} writer
  94. function _startAppearEffect( viewFigure, writer ) {
  95. if ( !viewFigure.hasClass( 'ck-appear' ) ) {
  96. writer.addClass( 'ck-appear', viewFigure );
  97. }
  98. }
  99. // Removes ck-appear class to the image figure if one is not already removed.
  100. //
  101. // @param {module:engine/view/containerelement~ContainerElement} viewFigure
  102. // @param {module:engine/view/writer~Writer} writer
  103. function _stopAppearEffect( viewFigure, writer ) {
  104. writer.removeClass( 'ck-appear', viewFigure );
  105. }
  106. // Shows placeholder together with infinite progress bar on given image figure.
  107. //
  108. // @param {module:engine/view/containerelement~ContainerElement} viewFigure
  109. // @param {module:engine/view/writer~Writer} writer
  110. function _showPlaceholder( viewFigure, writer ) {
  111. if ( !viewFigure.hasClass( 'ck-image-upload-placeholder' ) ) {
  112. writer.addClass( 'ck-image-upload-placeholder', viewFigure );
  113. }
  114. if ( !viewFigure.hasClass( 'ck-infinite-progress' ) ) {
  115. writer.addClass( 'ck-infinite-progress', viewFigure );
  116. }
  117. const viewImg = viewFigure.getChild( 0 );
  118. if ( viewImg.getAttribute( 'src' ) !== blankImage ) {
  119. writer.setAttribute( 'src', blankImage, viewImg );
  120. }
  121. if ( !_getUIElement( viewFigure, placeholderSymbol ) ) {
  122. writer.insert( ViewPosition.createAfter( viewImg ), _createPlaceholder( writer ) );
  123. }
  124. }
  125. // Removes placeholder together with infinite progress bar on given image figure.
  126. //
  127. // @param {module:engine/view/containerelement~ContainerElement} viewFigure
  128. // @param {module:engine/view/writer~Writer} writer
  129. function _hidePlaceholder( viewFigure, writer ) {
  130. if ( viewFigure.hasClass( 'ck-image-upload-placeholder' ) ) {
  131. writer.removeClass( 'ck-image-upload-placeholder', viewFigure );
  132. }
  133. if ( viewFigure.hasClass( 'ck-infinite-progress' ) ) {
  134. writer.removeClass( 'ck-infinite-progress', viewFigure );
  135. }
  136. _removeUIElement( viewFigure, writer, placeholderSymbol );
  137. }
  138. // Shows progress bar displaying upload progress.
  139. // Attaches it to the file loader to update when upload percentace is changed.
  140. //
  141. // @param {module:engine/view/containerelement~ContainerElement} viewFigure
  142. // @param {module:engine/view/writer~Writer} writer
  143. // @param {module:upload/filerepository~FileLoader} loader
  144. // @param {module:engine/view/view~View} view
  145. function _showProgressBar( viewFigure, writer, loader, view ) {
  146. const progressBar = _createProgressBar( writer );
  147. writer.insert( ViewPosition.createAt( viewFigure, 'end' ), progressBar );
  148. // Update progress bar width when uploadedPercent is changed.
  149. loader.on( 'change:uploadedPercent', ( evt, name, value ) => {
  150. view.change( writer => {
  151. writer.setStyle( 'width', value + '%', progressBar );
  152. } );
  153. } );
  154. }
  155. // Hides upload progress bar.
  156. //
  157. // @param {module:engine/view/containerelement~ContainerElement} viewFigure
  158. // @param {module:engine/view/writer~Writer} writer
  159. function _hideProgressBar( viewFigure, writer ) {
  160. _removeUIElement( viewFigure, writer, progressBarSymbol );
  161. }
  162. // Shows complete icon and hides after a certain amount of time.
  163. //
  164. // @param {module:engine/view/containerelement~ContainerElement} viewFigure
  165. // @param {module:engine/view/writer~Writer} writer
  166. // @param {module:engine/view/view~View} view
  167. function _showCompleteIcon( viewFigure, writer, view ) {
  168. const completeIcon = new UIElement( 'div', { class: 'ck-image-upload-complete-icon' } );
  169. writer.insert( ViewPosition.createAt( viewFigure, 'end' ), completeIcon );
  170. setTimeout( () => {
  171. view.change( writer => writer.remove( ViewRange.createOn( completeIcon ) ) );
  172. }, 3000 );
  173. }
  174. // Create progress bar element using {@link module:engine/view/uielement~UIElement}.
  175. //
  176. // @private
  177. // @param {module:engine/view/writer~Writer} writer
  178. // @returns {module:engine/view/uielement~UIElement}
  179. function _createProgressBar( writer ) {
  180. const progressBar = writer.createUIElement( 'div', { class: 'ck-progress-bar' } );
  181. writer.setCustomProperty( progressBarSymbol, true, progressBar );
  182. return progressBar;
  183. }
  184. // Create placeholder element using {@link module:engine/view/uielement~UIElement}.
  185. //
  186. // @private
  187. // @param {module:engine/view/writer~Writer} writer
  188. // @returns {module:engine/view/uielement~UIElement}
  189. function _createPlaceholder( writer ) {
  190. const placeholder = writer.createUIElement( 'div', { class: 'ck-upload-placeholder' } );
  191. writer.setCustomProperty( placeholderSymbol, true, placeholder );
  192. placeholder.render = function( domDocument ) {
  193. const domElement = this.toDomElement( domDocument );
  194. domElement.innerHTML = '<div class="ck-upload-placeholder_loader"></div>';
  195. return domElement;
  196. };
  197. return placeholder;
  198. }
  199. // Returns {@link module:engine/view/uielement~UIElement} of given unique property from image figure element.
  200. // Returns `undefined` if element is not found.
  201. //
  202. // @private
  203. // @param {module:engine/view/element~Element} imageFigure
  204. // @param {Symbol} uniqueProperty
  205. // @returns {module:engine/view/uielement~UIElement|undefined}
  206. function _getUIElement( imageFigure, uniqueProperty ) {
  207. for ( const child of imageFigure.getChildren() ) {
  208. if ( child.getCustomProperty( uniqueProperty ) ) {
  209. return child;
  210. }
  211. }
  212. }
  213. // Removes {@link module:engine/view/uielement~UIElement} of given unique property from image figure element.
  214. //
  215. // @private
  216. // @param {module:engine/view/element~Element} imageFigure
  217. // @param {module:engine/view/writer~Writer} writer
  218. // @param {Symbol} uniqueProperty
  219. function _removeUIElement( viewFigure, writer, uniqueProperty ) {
  220. const element = _getUIElement( viewFigure, uniqueProperty );
  221. if ( element ) {
  222. writer.remove( ViewRange.createOn( element ) );
  223. }
  224. }