autoimage.js 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174
  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/autoimage
  7. */
  8. import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
  9. import Clipboard from '@ckeditor/ckeditor5-clipboard/src/clipboard';
  10. import LiveRange from '@ckeditor/ckeditor5-engine/src/model/liverange';
  11. import LivePosition from '@ckeditor/ckeditor5-engine/src/model/liveposition';
  12. import Undo from '@ckeditor/ckeditor5-undo/src/undo';
  13. import global from '@ckeditor/ckeditor5-utils/src/dom/global';
  14. import { insertImage } from './image/utils';
  15. // Implements the pattern: http(s)://(www.)example.com/path/to/resource.ext?query=params&maybe=too.
  16. const IMAGE_URL_REGEXP = new RegExp( String( /^(http(s)?:\/\/)?[\w-]+(\.[\w-]+)+[\w._~:/?#[\]@!$&'()*+,;=%-]+/.source +
  17. /\.(jpg|jpeg|png|gif|ico|JPG|JPEG|PNG|GIF|ICO)\??[\w._~:/#[\]@!$&'()*+,;=%-]*$/.source ) );
  18. /**
  19. * The auto-image plugin. It recognizes image links in the pasted content and embeds
  20. * them shortly after they are injected into the document.
  21. *
  22. * @extends module:core/plugin~Plugin
  23. */
  24. export default class AutoImage extends Plugin {
  25. /**
  26. * @inheritDoc
  27. */
  28. static get requires() {
  29. return [ Clipboard, Undo ];
  30. }
  31. /**
  32. * @inheritDoc
  33. */
  34. static get pluginName() {
  35. return 'AutoImage';
  36. }
  37. /**
  38. * @inheritDoc
  39. */
  40. constructor( editor ) {
  41. super( editor );
  42. /**
  43. * The paste–to–embed `setTimeout` ID. Stored as a property to allow
  44. * cleaning of the timeout.
  45. *
  46. * @private
  47. * @member {Number} #_timeoutId
  48. */
  49. this._timeoutId = null;
  50. /**
  51. * The position where the `<image>` element will be inserted after the timeout,
  52. * determined each time the new content is pasted into the document.
  53. *
  54. * @private
  55. * @member {module:engine/model/liveposition~LivePosition} #_positionToInsert
  56. */
  57. this._positionToInsert = null;
  58. }
  59. /**
  60. * @inheritDoc
  61. */
  62. init() {
  63. const editor = this.editor;
  64. const modelDocument = editor.model.document;
  65. // We need to listen on `Clipboard#inputTransformation` because we need to save positions of selection.
  66. // After pasting, the content between those positions will be checked for a URL that could be transformed
  67. // into image.
  68. this.listenTo( editor.plugins.get( Clipboard ), 'inputTransformation', () => {
  69. const firstRange = modelDocument.selection.getFirstRange();
  70. const leftLivePosition = LivePosition.fromPosition( firstRange.start );
  71. leftLivePosition.stickiness = 'toPrevious';
  72. const rightLivePosition = LivePosition.fromPosition( firstRange.end );
  73. rightLivePosition.stickiness = 'toNext';
  74. modelDocument.once( 'change:data', () => {
  75. this._embedImageBetweenPositions( leftLivePosition, rightLivePosition );
  76. leftLivePosition.detach();
  77. rightLivePosition.detach();
  78. }, { priority: 'high' } );
  79. } );
  80. editor.commands.get( 'undo' ).on( 'execute', () => {
  81. if ( this._timeoutId ) {
  82. global.window.clearTimeout( this._timeoutId );
  83. this._positionToInsert.detach();
  84. this._timeoutId = null;
  85. this._positionToInsert = null;
  86. }
  87. }, { priority: 'high' } );
  88. }
  89. /**
  90. * Analyzes the part of the document between provided positions in search for an URL representing an image.
  91. * When the URL is found, it is automatically converted into an image.
  92. *
  93. * @protected
  94. * @param {module:engine/model/liveposition~LivePosition} leftPosition Left position of the selection.
  95. * @param {module:engine/model/liveposition~LivePosition} rightPosition Right position of the selection.
  96. */
  97. _embedImageBetweenPositions( leftPosition, rightPosition ) {
  98. const editor = this.editor;
  99. // TODO: Use marker instead of LiveRange & LivePositions.
  100. const urlRange = new LiveRange( leftPosition, rightPosition );
  101. const walker = urlRange.getWalker( { ignoreElementEnd: true } );
  102. let src = '';
  103. for ( const node of walker ) {
  104. if ( node.item.is( '$textProxy' ) ) {
  105. src += node.item.data;
  106. }
  107. }
  108. src = src.trim();
  109. // If the URL does not match to image URL regexp, let's skip that.
  110. if ( !src.match( IMAGE_URL_REGEXP ) ) {
  111. urlRange.detach();
  112. return;
  113. }
  114. // Position won't be available in the `setTimeout` function so let's clone it.
  115. this._positionToInsert = LivePosition.fromPosition( leftPosition );
  116. // This action mustn't be executed if undo was called between pasting and auto-embedding.
  117. this._timeoutId = global.window.setTimeout( () => {
  118. // Don't do anything if image element cannot be inserted at the current position.
  119. // See https://github.com/ckeditor/ckeditor5/issues/2763.
  120. // Condition must be checked after timeout - pasting may take place on an element, replacing it. The final position matters.
  121. const imageCommand = editor.commands.get( 'imageInsert' );
  122. if ( !imageCommand.isEnabled ) {
  123. urlRange.detach();
  124. return;
  125. }
  126. editor.model.change( writer => {
  127. this._timeoutId = null;
  128. writer.remove( urlRange );
  129. urlRange.detach();
  130. let insertionPosition;
  131. // Check if position where the element should be inserted is still valid.
  132. // Otherwise leave it as undefined to use the logic of insertImage().
  133. if ( this._positionToInsert.root.rootName !== '$graveyard' ) {
  134. insertionPosition = this._positionToInsert.toPosition();
  135. }
  136. insertImage( editor.model, { src }, insertionPosition );
  137. this._positionToInsert.detach();
  138. this._positionToInsert = null;
  139. } );
  140. }, 100 );
  141. }
  142. }