indentcommand.js 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150
  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 list/indentcommand
  7. */
  8. import Command from '@ckeditor/ckeditor5-core/src/command';
  9. import first from '@ckeditor/ckeditor5-utils/src/first';
  10. /**
  11. * The list indent command. It is used by the {@link module:list/list~List list feature}.
  12. *
  13. * @extends module:core/command~Command
  14. */
  15. export default class IndentCommand extends Command {
  16. /**
  17. * Creates an instance of the command.
  18. *
  19. * @param {module:core/editor/editor~Editor} editor The editor instance.
  20. * @param {'forward'|'backward'} indentDirection The direction of indent. If it is equal to `backward`, the command
  21. * will outdent a list item.
  22. */
  23. constructor( editor, indentDirection ) {
  24. super( editor );
  25. /**
  26. * Determines by how much the command will change the list item's indent attribute.
  27. *
  28. * @readonly
  29. * @private
  30. * @member {Number}
  31. */
  32. this._indentBy = indentDirection == 'forward' ? 1 : -1;
  33. }
  34. /**
  35. * @inheritDoc
  36. */
  37. refresh() {
  38. this.isEnabled = this._checkEnabled();
  39. }
  40. /**
  41. * Indents or outdents (depending on the {@link #constructor}'s `indentDirection` parameter) selected list items.
  42. *
  43. * @fires execute
  44. * @fires _executeCleanup
  45. */
  46. execute() {
  47. const model = this.editor.model;
  48. const doc = model.document;
  49. let itemsToChange = Array.from( doc.selection.getSelectedBlocks() );
  50. model.change( writer => {
  51. const lastItem = itemsToChange[ itemsToChange.length - 1 ];
  52. // Indenting a list item should also indent all the items that are already sub-items of indented item.
  53. let next = lastItem.nextSibling;
  54. // Check all items after last indented item, as long as their indent is bigger than indent of that item.
  55. while ( next && next.name == 'listItem' && next.getAttribute( 'listIndent' ) > lastItem.getAttribute( 'listIndent' ) ) {
  56. itemsToChange.push( next );
  57. next = next.nextSibling;
  58. }
  59. // We need to be sure to keep model in correct state after each small change, because converters
  60. // bases on that state and assumes that model is correct.
  61. // Because of that, if the command outdents items, we will outdent them starting from the last item, as
  62. // it is safer.
  63. if ( this._indentBy < 0 ) {
  64. itemsToChange = itemsToChange.reverse();
  65. }
  66. for ( const item of itemsToChange ) {
  67. const indent = item.getAttribute( 'listIndent' ) + this._indentBy;
  68. // If indent is lower than 0, it means that the item got outdented when it was not indented.
  69. // This means that we need to convert that list item to paragraph.
  70. if ( indent < 0 ) {
  71. // To keep the model as correct as possible, first rename listItem, then remove attributes,
  72. // as listItem without attributes is very incorrect and will cause problems in converters.
  73. // No need to remove attributes, will be removed by post fixer.
  74. writer.rename( item, 'paragraph' );
  75. }
  76. // If indent is >= 0, change the attribute value.
  77. else {
  78. writer.setAttribute( 'listIndent', indent, item );
  79. }
  80. }
  81. /**
  82. * Event fired by the {@link #execute} method.
  83. *
  84. * It allows to execute an action after executing the {@link ~IndentCommand#execute} method, for example adjusting
  85. * attributes of changed list items.
  86. *
  87. * @protected
  88. * @event _executeCleanup
  89. */
  90. this.fire( '_executeCleanup', itemsToChange );
  91. } );
  92. }
  93. /**
  94. * Checks whether the command can be enabled in the current context.
  95. *
  96. * @private
  97. * @returns {Boolean} Whether the command should be enabled.
  98. */
  99. _checkEnabled() {
  100. // Check whether any of position's ancestor is a list item.
  101. const listItem = first( this.editor.model.document.selection.getSelectedBlocks() );
  102. // If selection is not in a list item, the command is disabled.
  103. if ( !listItem || !listItem.is( 'element', 'listItem' ) ) {
  104. return false;
  105. }
  106. if ( this._indentBy > 0 ) {
  107. // Cannot indent first item in it's list. Check if before `listItem` is a list item that is in same list.
  108. // To be in the same list, the item has to have same attributes and cannot be "split" by an item with lower indent.
  109. const indent = listItem.getAttribute( 'listIndent' );
  110. const type = listItem.getAttribute( 'listType' );
  111. let prev = listItem.previousSibling;
  112. while ( prev && prev.is( 'element', 'listItem' ) && prev.getAttribute( 'listIndent' ) >= indent ) {
  113. if ( prev.getAttribute( 'listIndent' ) == indent ) {
  114. // The item is on the same level.
  115. // If it has same type, it means that we found a preceding sibling from the same list.
  116. // If it does not have same type, it means that `listItem` is on different list (this can happen only
  117. // on top level lists, though).
  118. return prev.getAttribute( 'listType' ) == type;
  119. }
  120. prev = prev.previousSibling;
  121. }
  122. // Could not find similar list item, this means that `listItem` is first in its list.
  123. return false;
  124. }
  125. // If we are outdenting it is enough to be in list item. Every list item can always be outdented.
  126. return true;
  127. }
  128. }