imageuploadediting.js 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214
  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/imageuploadediting
  7. */
  8. import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
  9. import FileRepository from '@ckeditor/ckeditor5-upload/src/filerepository';
  10. import ImageUploadCommand from '../../src/imageupload/imageuploadcommand';
  11. import Notification from '@ckeditor/ckeditor5-ui/src/notification/notification';
  12. import ModelSelection from '@ckeditor/ckeditor5-engine/src/model/selection';
  13. import { isImageType, findOptimalInsertionPosition } from '../../src/imageupload/utils';
  14. /**
  15. * Image upload editing plugin.
  16. *
  17. * @extends module:core/plugin~Plugin
  18. */
  19. export default class ImageUploadEditing extends Plugin {
  20. /**
  21. * @inheritDoc
  22. */
  23. static get requires() {
  24. return [ FileRepository, Notification ];
  25. }
  26. /**
  27. * @inheritDoc
  28. */
  29. init() {
  30. const editor = this.editor;
  31. const doc = editor.model.document;
  32. const schema = editor.model.schema;
  33. const fileRepository = editor.plugins.get( FileRepository );
  34. // Setup schema to allow uploadId and uploadStatus for images.
  35. schema.extend( 'image', {
  36. allowAttributes: [ 'uploadId', 'uploadStatus' ]
  37. } );
  38. // Register imageUpload command.
  39. editor.commands.add( 'imageUpload', new ImageUploadCommand( editor ) );
  40. // Execute imageUpload command when image is dropped or pasted.
  41. editor.editing.view.on( 'clipboardInput', ( evt, data ) => {
  42. // Skip if non empty HTML data is included.
  43. // https://github.com/ckeditor/ckeditor5-upload/issues/68
  44. if ( isHtmlIncluded( data.dataTransfer ) ) {
  45. return;
  46. }
  47. let targetModelSelection = new ModelSelection(
  48. data.targetRanges.map( viewRange => editor.editing.mapper.toModelRange( viewRange ) )
  49. );
  50. for ( const file of data.dataTransfer.files ) {
  51. const insertAt = findOptimalInsertionPosition( targetModelSelection );
  52. if ( isImageType( file ) ) {
  53. editor.execute( 'imageUpload', { file, insertAt } );
  54. evt.stop();
  55. }
  56. // Use target ranges only for the first image. Then, use that image position
  57. // so we keep adding the next ones after the previous one.
  58. targetModelSelection = doc.selection;
  59. }
  60. } );
  61. // Prevents from browser redirecting to the dropped image.
  62. editor.editing.view.on( 'dragover', ( evt, data ) => {
  63. data.preventDefault();
  64. } );
  65. doc.on( 'change', () => {
  66. const changes = doc.differ.getChanges( { includeChangesInGraveyard: true } );
  67. for ( const entry of changes ) {
  68. if ( entry.type == 'insert' && entry.name == 'image' ) {
  69. const item = entry.position.nodeAfter;
  70. const isInGraveyard = entry.position.root.rootName == '$graveyard';
  71. // Check if the image element still has upload id.
  72. const uploadId = item.getAttribute( 'uploadId' );
  73. if ( !uploadId ) {
  74. continue;
  75. }
  76. // Check if the image is loaded on this client.
  77. const loader = fileRepository.loaders.get( uploadId );
  78. if ( !loader ) {
  79. continue;
  80. }
  81. if ( isInGraveyard ) {
  82. // If the image was inserted to the graveyard - abort the loading process.
  83. loader.abort();
  84. } else if ( loader.status == 'idle' ) {
  85. // If the image was inserted into content and has not been loaded, start loading it.
  86. this._load( loader, item );
  87. }
  88. }
  89. }
  90. } );
  91. }
  92. /**
  93. * Performs image loading. Image is read from the disk and temporary data is displayed, after uploading process
  94. * is complete we replace temporary data with target image from the server.
  95. *
  96. * @private
  97. * @param {module:upload/filerepository~FileLoader} loader
  98. * @param {module:engine/model/element~Element} imageElement
  99. */
  100. _load( loader, imageElement ) {
  101. const editor = this.editor;
  102. const model = editor.model;
  103. const t = editor.locale.t;
  104. const fileRepository = editor.plugins.get( FileRepository );
  105. const notification = editor.plugins.get( Notification );
  106. model.enqueueChange( 'transparent', writer => {
  107. writer.setAttribute( 'uploadStatus', 'reading', imageElement );
  108. } );
  109. loader.read()
  110. .then( data => {
  111. const viewFigure = editor.editing.mapper.toViewElement( imageElement );
  112. const viewImg = viewFigure.getChild( 0 );
  113. const promise = loader.upload();
  114. viewImg.setAttribute( 'src', data );
  115. editor.editing.view.render();
  116. model.enqueueChange( 'transparent', writer => {
  117. writer.setAttribute( 'uploadStatus', 'uploading', imageElement );
  118. } );
  119. return promise;
  120. } )
  121. .then( data => {
  122. model.enqueueChange( 'transparent', writer => {
  123. writer.setAttributes( { uploadStatus: 'complete', src: data.default }, imageElement );
  124. // Srcset attribute for responsive images support.
  125. let maxWidth = 0;
  126. const srcsetAttribute = Object.keys( data )
  127. // Filter out keys that are not integers.
  128. .filter( key => {
  129. const width = parseInt( key, 10 );
  130. if ( !isNaN( width ) ) {
  131. maxWidth = Math.max( maxWidth, width );
  132. return true;
  133. }
  134. } )
  135. // Convert each key to srcset entry.
  136. .map( key => `${ data[ key ] } ${ key }w` )
  137. // Join all entries.
  138. .join( ', ' );
  139. if ( srcsetAttribute != '' ) {
  140. writer.setAttribute( 'srcset', {
  141. data: srcsetAttribute,
  142. width: maxWidth
  143. }, imageElement );
  144. }
  145. } );
  146. clean();
  147. } )
  148. .catch( msg => {
  149. // Might be 'aborted'.
  150. if ( loader.status == 'error' ) {
  151. notification.showWarning( msg, {
  152. title: t( 'Upload failed' ),
  153. namespace: 'upload'
  154. } );
  155. }
  156. clean();
  157. // Permanently remove image from insertion batch.
  158. model.enqueueChange( 'transparent', writer => {
  159. writer.remove( imageElement );
  160. } );
  161. } );
  162. function clean() {
  163. model.enqueueChange( 'transparent', writer => {
  164. writer.removeAttribute( 'uploadId', imageElement );
  165. writer.removeAttribute( 'uploadStatus', imageElement );
  166. } );
  167. fileRepository.destroyLoader( loader );
  168. }
  169. }
  170. }
  171. // Returns true if non-empty `text/html` is included in data transfer.
  172. //
  173. // @param {module:clipboard/datatransfer~DataTransfer} dataTransfer
  174. // @returns {Boolean}
  175. export function isHtmlIncluded( dataTransfer ) {
  176. return Array.from( dataTransfer.types ).includes( 'text/html' ) && dataTransfer.getData( 'text/html' ) !== '';
  177. }