8
0

deletecommand.js 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196
  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 typing/deletecommand
  7. */
  8. import Command from '@ckeditor/ckeditor5-core/src/command';
  9. import count from '@ckeditor/ckeditor5-utils/src/count';
  10. import ChangeBuffer from './utils/changebuffer';
  11. /**
  12. * The delete command. Used by the {@link module:typing/delete~Delete delete feature} to handle the <kbd>Delete</kbd> and
  13. * <kbd>Backspace</kbd> keys.
  14. *
  15. * @extends module:core/command~Command
  16. */
  17. export default class DeleteCommand extends Command {
  18. /**
  19. * Creates an instance of the command.
  20. *
  21. * @param {module:core/editor/editor~Editor} editor
  22. * @param {'forward'|'backward'} direction The directionality of the delete describing in what direction it
  23. * should consume the content when the selection is collapsed.
  24. */
  25. constructor( editor, direction ) {
  26. super( editor );
  27. /**
  28. * The directionality of the delete describing in what direction it should
  29. * consume the content when the selection is collapsed.
  30. *
  31. * @readonly
  32. * @member {'forward'|'backward'} #direction
  33. */
  34. this.direction = direction;
  35. /**
  36. * Delete's change buffer used to group subsequent changes into batches.
  37. *
  38. * @readonly
  39. * @private
  40. * @member {typing.ChangeBuffer} #buffer
  41. */
  42. this._buffer = new ChangeBuffer( editor.model, editor.config.get( 'typing.undoStep' ) );
  43. }
  44. /**
  45. * The current change buffer.
  46. *
  47. * @type {module:typing/utils/changebuffer~ChangeBuffer}
  48. */
  49. get buffer() {
  50. return this._buffer;
  51. }
  52. /**
  53. * Executes the delete command. Depending on whether the selection is collapsed or not, deletes its content
  54. * or a piece of content in the {@link #direction defined direction}.
  55. *
  56. * @fires execute
  57. * @param {Object} [options] The command options.
  58. * @param {'character'} [options.unit='character'] See {@link module:engine/model/utils/modifyselection~modifySelection}'s options.
  59. * @param {Number} [options.sequence=1] A number describing which subsequent delete event it is without the key being released.
  60. * See the {@link module:engine/view/document~Document#event:delete} event data.
  61. * @param {module:engine/model/selection~Selection} [options.selection] Selection to remove. If not set, current model selection
  62. * will be used.
  63. */
  64. execute( options = {} ) {
  65. const model = this.editor.model;
  66. const doc = model.document;
  67. model.enqueueChange( this._buffer.batch, writer => {
  68. this._buffer.lock();
  69. const selection = writer.createSelection( options.selection || doc.selection );
  70. // Do not replace the whole selected content if selection was collapsed.
  71. // This prevents such situation:
  72. //
  73. // <h1></h1><p>[]</p> --> <h1>[</h1><p>]</p> --> <p></p>
  74. // starting content --> after `modifySelection` --> after `deleteContent`.
  75. const doNotResetEntireContent = selection.isCollapsed;
  76. // Try to extend the selection in the specified direction.
  77. if ( selection.isCollapsed ) {
  78. model.modifySelection( selection, { direction: this.direction, unit: options.unit } );
  79. }
  80. // Check if deleting in an empty editor. See #61.
  81. if ( this._shouldEntireContentBeReplacedWithParagraph( options.sequence || 1 ) ) {
  82. this._replaceEntireContentWithParagraph( writer );
  83. return;
  84. }
  85. // If selection is still collapsed, then there's nothing to delete.
  86. if ( selection.isCollapsed ) {
  87. return;
  88. }
  89. let changeCount = 0;
  90. selection.getFirstRange().getMinimalFlatRanges().forEach( range => {
  91. changeCount += count(
  92. range.getWalker( { singleCharacters: true, ignoreElementEnd: true, shallow: true } )
  93. );
  94. } );
  95. model.deleteContent( selection, {
  96. doNotResetEntireContent,
  97. direction: this.direction
  98. } );
  99. this._buffer.input( changeCount );
  100. writer.setSelection( selection );
  101. this._buffer.unlock();
  102. } );
  103. }
  104. /**
  105. * If the user keeps <kbd>Backspace</kbd> or <kbd>Delete</kbd> key pressed, the content of the current
  106. * editable will be cleared. However, this will not yet lead to resetting the remaining block to a paragraph
  107. * (which happens e.g. when the user does <kbd>Ctrl</kbd> + <kbd>A</kbd>, <kbd>Backspace</kbd>).
  108. *
  109. * But, if the user pressed the key in an empty editable for the first time,
  110. * we want to replace the entire content with a paragraph if:
  111. *
  112. * * the current limit element is empty,
  113. * * the paragraph is allowed in the limit element,
  114. * * the limit doesn't already have a paragraph inside.
  115. *
  116. * See https://github.com/ckeditor/ckeditor5-typing/issues/61.
  117. *
  118. * @private
  119. * @param {Number} sequence A number describing which subsequent delete event it is without the key being released.
  120. * @returns {Boolean}
  121. */
  122. _shouldEntireContentBeReplacedWithParagraph( sequence ) {
  123. // Does nothing if user pressed and held the "Backspace" or "Delete" key.
  124. if ( sequence > 1 ) {
  125. return false;
  126. }
  127. const model = this.editor.model;
  128. const doc = model.document;
  129. const selection = doc.selection;
  130. const limitElement = model.schema.getLimitElement( selection );
  131. // If a collapsed selection contains the whole content it means that the content is empty
  132. // (from the user perspective).
  133. const limitElementIsEmpty = selection.isCollapsed && selection.containsEntireContent( limitElement );
  134. if ( !limitElementIsEmpty ) {
  135. return false;
  136. }
  137. if ( !model.schema.checkChild( limitElement, 'paragraph' ) ) {
  138. return false;
  139. }
  140. const limitElementFirstChild = limitElement.getChild( 0 );
  141. // Does nothing if the limit element already contains only a paragraph.
  142. // We ignore the case when paragraph might have some inline elements (<p><inlineWidget>[]</inlineWidget></p>)
  143. // because we don't support such cases yet and it's unclear whether inlineWidget shouldn't be a limit itself.
  144. if ( limitElementFirstChild && limitElementFirstChild.name === 'paragraph' ) {
  145. return false;
  146. }
  147. return true;
  148. }
  149. /**
  150. * The entire content is replaced with the paragraph. Selection is moved inside the paragraph.
  151. *
  152. * @private
  153. */
  154. _replaceEntireContentWithParagraph( writer ) {
  155. const model = this.editor.model;
  156. const doc = model.document;
  157. const selection = doc.selection;
  158. const limitElement = model.schema.getLimitElement( selection );
  159. const paragraph = writer.createElement( 'paragraph' );
  160. writer.remove( writer.createRangeIn( limitElement ) );
  161. writer.insert( paragraph, limitElement );
  162. writer.setSelection( paragraph, 0 );
  163. }
  164. }