outdentcodeblockcommand.js 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187
  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 code-block/outdentcodeblockcommand
  7. */
  8. import Command from '@ckeditor/ckeditor5-core/src/command';
  9. import {
  10. getLeadingWhiteSpaces,
  11. getIndentOutdentPositions,
  12. isModelSelectionInCodeBlock
  13. } from './utils';
  14. /**
  15. * The code block indentation decrease command plugin.
  16. *
  17. * @extends module:core/command~Command
  18. */
  19. export default class OutdentCodeBlockCommand extends Command {
  20. constructor( editor ) {
  21. super( editor );
  22. /**
  23. * A sequence of characters removed from the line when the command is executed.
  24. *
  25. * @readonly
  26. * @private
  27. * @member {String}
  28. */
  29. this._indentSequence = editor.config.get( 'codeBlock.indentSequence' );
  30. }
  31. /**
  32. * @inheritDoc
  33. */
  34. refresh() {
  35. this.isEnabled = this._checkEnabled();
  36. }
  37. /**
  38. * Executes the command. When the command {@link #isEnabled is enabled}, the indentation of the
  39. * code lines in the selection will be decreased.
  40. *
  41. * @fires execute
  42. */
  43. execute() {
  44. const editor = this.editor;
  45. const model = editor.model;
  46. model.change( writer => {
  47. const positions = getIndentOutdentPositions( model );
  48. // Outdent all positions, for instance assuming the indent sequence is 4x space (" "):
  49. //
  50. // <codeBlock>^foo</codeBlock> -> <codeBlock>foo</codeBlock>
  51. //
  52. // <codeBlock> ^bar</codeBlock> -> <codeBlock>bar</codeBlock>
  53. //
  54. // Also, when there is more than one position:
  55. //
  56. // <codeBlock>
  57. // ^foobar
  58. // <softBreak></softBreak>
  59. // ^bazqux
  60. // </codeBlock>
  61. //
  62. // ->
  63. //
  64. // <codeBlock>
  65. // foobar
  66. // <softBreak></softBreak>
  67. // bazqux
  68. // </codeBlock>
  69. for ( const position of positions ) {
  70. const range = getLastOutdentableSequenceRange( this.editor.model, position, this._indentSequence );
  71. if ( range ) {
  72. writer.remove( range );
  73. }
  74. }
  75. } );
  76. }
  77. /**
  78. * Checks whether the command can be enabled in the current context.
  79. *
  80. * @private
  81. * @returns {Boolean} Whether the command should be enabled.
  82. */
  83. _checkEnabled() {
  84. if ( !this._indentSequence ) {
  85. return false;
  86. }
  87. const model = this.editor.model;
  88. if ( !isModelSelectionInCodeBlock( model.document.selection ) ) {
  89. return false;
  90. }
  91. // Outdent command can execute only when there is an indent character sequence
  92. // in some of the lines.
  93. return getIndentOutdentPositions( model ).some( position => {
  94. return getLastOutdentableSequenceRange( model, position, this._indentSequence );
  95. } );
  96. }
  97. }
  98. // For a position coming from `getIndentOutdentPositions()`, it returns the range representing
  99. // the last occurrence of the indent sequence among the leading whitespaces of the code line the
  100. // position represents.
  101. //
  102. // For instance, assuming the indent sequence is 4x space (" "):
  103. //
  104. // <codeBlock>foo^</codeBlock> -> null
  105. // <codeBlock>foo^<softBreak></softBreak>bar</codeBlock> -> null
  106. // <codeBlock> ^foo</codeBlock> -> null
  107. // <codeBlock> ^foo</codeBlock> -> <codeBlock> [ ]foo</codeBlock>
  108. // <codeBlock> ^foo bar</codeBlock> -> <codeBlock>[ ]foo bar</codeBlock>
  109. //
  110. // @param {<module:engine/model/model~Model>} model
  111. // @param {<module:engine/model/position~Position>} position
  112. // @param {String} sequence
  113. // @returns {<module:engine/model/range~Range>|null}
  114. function getLastOutdentableSequenceRange( model, position, sequence ) {
  115. // Positions start before each text node (code line). Get the node corresponding to the position.
  116. const nodeAtPosition = getCodeLineTextNodeAtPosition( position );
  117. if ( !nodeAtPosition ) {
  118. return null;
  119. }
  120. const leadingWhiteSpaces = getLeadingWhiteSpaces( nodeAtPosition );
  121. const lastIndexOfSequence = leadingWhiteSpaces.lastIndexOf( sequence );
  122. // For instance, assuming the indent sequence is 4x space (" "):
  123. //
  124. // <codeBlock> ^foo</codeBlock> -> null
  125. //
  126. if ( lastIndexOfSequence + sequence.length !== leadingWhiteSpaces.length ) {
  127. return null;
  128. }
  129. // For instance, assuming the indent sequence is 4x space (" "):
  130. //
  131. // <codeBlock> ^foo</codeBlock> -> null
  132. //
  133. if ( lastIndexOfSequence === -1 ) {
  134. return null;
  135. }
  136. const { parent, startOffset } = nodeAtPosition;
  137. // Create a range that contains the **last** indent sequence among the leading whitespaces
  138. // of the line.
  139. //
  140. // For instance, assuming the indent sequence is 4x space (" "):
  141. //
  142. // <codeBlock> ^foo</codeBlock> -> <codeBlock> [ ]foo</codeBlock>
  143. //
  144. return model.createRange(
  145. model.createPositionAt( parent, startOffset + lastIndexOfSequence ),
  146. model.createPositionAt( parent, startOffset + lastIndexOfSequence + sequence.length )
  147. );
  148. }
  149. function getCodeLineTextNodeAtPosition( position ) {
  150. // Positions start before each text node (code line). Get the node corresponding to the position.
  151. let nodeAtPosition = position.parent.getChild( position.index );
  152. // <codeBlock>foo^</codeBlock>
  153. // <codeBlock>foo^<softBreak></softBreak>bar</codeBlock>
  154. if ( !nodeAtPosition || nodeAtPosition.is( 'element', 'softBreak' ) ) {
  155. nodeAtPosition = position.nodeBefore;
  156. }
  157. // <codeBlock>^</codeBlock>
  158. // <codeBlock>foo^<softBreak></softBreak>bar</codeBlock>
  159. if ( !nodeAtPosition || nodeAtPosition.is( 'element', 'softBreak' ) ) {
  160. return null;
  161. }
  162. return nodeAtPosition;
  163. }