imageuploadengine.js 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210
  1. /**
  2. * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
  3. * For licensing, see LICENSE.md.
  4. */
  5. /**
  6. * @module upload/imageuploadengine
  7. */
  8. import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
  9. import FileRepository from './filerepository';
  10. import ImageUploadCommand from './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 './utils';
  14. /**
  15. * Image upload engine plugin.
  16. *
  17. * @extends module:core/plugin~Plugin
  18. */
  19. export default class ImageUploadEngine 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.document;
  32. const schema = doc.schema;
  33. const fileRepository = editor.plugins.get( FileRepository );
  34. // Setup schema to allow uploadId for images.
  35. schema.allow( { name: 'image', attributes: [ 'uploadId' ], inside: '$root' } );
  36. schema.allow( { name: 'image', attributes: [ 'uploadStatus' ], inside: '$root' } );
  37. schema.requireAttributes( 'image', [ 'uploadId' ] );
  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', ( evt, type, data ) => {
  66. // Listen on document changes and:
  67. // * start upload process when image with `uploadId` attribute is inserted,
  68. // * abort upload process when image `uploadId` attribute is removed.
  69. if ( type === 'insert' || type === 'reinsert' || type === 'remove' ) {
  70. for ( const value of data.range ) {
  71. if ( value.type === 'elementStart' && value.item.name === 'image' ) {
  72. const imageElement = value.item;
  73. const uploadId = imageElement.getAttribute( 'uploadId' );
  74. if ( uploadId ) {
  75. const loader = fileRepository.loaders.get( uploadId );
  76. if ( loader ) {
  77. if ( type === 'insert' && loader.status == 'idle' ) {
  78. this.load( loader, imageElement );
  79. }
  80. if ( type === 'remove' ) {
  81. loader.abort();
  82. }
  83. }
  84. }
  85. }
  86. }
  87. }
  88. } );
  89. }
  90. /**
  91. * Performs image loading. Image is read from the disk and temporary data is displayed, after uploading process
  92. * is complete we replace temporary data with target image from the server.
  93. *
  94. * @protected
  95. * @param {module:upload/filerepository~FileLoader} loader
  96. * @param {module:engine/model/element~Element} imageElement
  97. */
  98. load( loader, imageElement ) {
  99. const editor = this.editor;
  100. const t = editor.locale.t;
  101. const doc = editor.document;
  102. const fileRepository = editor.plugins.get( FileRepository );
  103. const notification = editor.plugins.get( Notification );
  104. doc.enqueueChanges( () => {
  105. doc.batch( 'transparent' ).setAttribute( imageElement, 'uploadStatus', 'reading' );
  106. } );
  107. loader.read()
  108. .then( data => {
  109. const viewFigure = editor.editing.mapper.toViewElement( imageElement );
  110. const viewImg = viewFigure.getChild( 0 );
  111. const promise = loader.upload();
  112. viewImg.setAttribute( 'src', data );
  113. editor.editing.view.render();
  114. doc.enqueueChanges( () => {
  115. doc.batch( 'transparent' ).setAttribute( imageElement, 'uploadStatus', 'uploading' );
  116. } );
  117. return promise;
  118. } )
  119. .then( data => {
  120. doc.enqueueChanges( () => {
  121. doc.batch( 'transparent' ).setAttribute( imageElement, 'uploadStatus', 'complete' );
  122. doc.batch( 'transparent' ).setAttribute( imageElement, 'src', data.default );
  123. // Srcset attribute for responsive images support.
  124. let maxWidth = 0;
  125. const srcsetAttribute = Object.keys( data )
  126. // Filter out keys that are not integers.
  127. .filter( key => {
  128. const width = parseInt( key, 10 );
  129. if ( !isNaN( width ) ) {
  130. maxWidth = Math.max( maxWidth, width );
  131. return true;
  132. }
  133. } )
  134. // Convert each key to srcset entry.
  135. .map( key => `${ data[ key ] } ${ key }w` )
  136. // Join all entries.
  137. .join( ', ' );
  138. if ( srcsetAttribute != '' ) {
  139. doc.batch( 'transparent' ).setAttribute( imageElement, 'srcset', {
  140. data: srcsetAttribute,
  141. width: maxWidth
  142. } );
  143. }
  144. } );
  145. clean();
  146. } )
  147. .catch( msg => {
  148. // Might be 'aborted'.
  149. if ( loader.status == 'error' ) {
  150. notification.showWarning( msg, {
  151. title: t( 'Upload failed' ),
  152. namespace: 'upload'
  153. } );
  154. }
  155. clean();
  156. // Permanently remove image from insertion batch.
  157. doc.enqueueChanges( () => {
  158. doc.batch( 'transparent' ).remove( imageElement );
  159. } );
  160. } );
  161. function clean() {
  162. doc.enqueueChanges( () => {
  163. doc.batch( 'transparent' ).removeAttribute( imageElement, 'uploadId' );
  164. doc.batch( 'transparent' ).removeAttribute( imageElement, 'uploadStatus' );
  165. } );
  166. fileRepository.destroyLoader( loader );
  167. }
  168. }
  169. }
  170. // Returns true if non-empty `text/html` is included in data transfer.
  171. //
  172. // @param {module:clipboard/datatransfer~DataTransfer} dataTransfer
  173. // @returns {Boolean}
  174. export function isHtmlIncluded( dataTransfer ) {
  175. return Array.from( dataTransfer.types ).includes( 'text/html' ) && dataTransfer.getData( 'text/html' ) !== '';
  176. }