indentcommand.js 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139
  1. /**
  2. * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
  3. * For licensing, see LICENSE.md.
  4. */
  5. /**
  6. * @module list/indentcommand
  7. */
  8. import Command from '@ckeditor/ckeditor5-core/src/command/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 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 Editor instance.
  20. * @param {'forward'|'backward'} indentDirection 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. * By how much the command will change list item's indent attribute.
  27. *
  28. * @readonly
  29. * @private
  30. * @member {Number}
  31. */
  32. this._indentBy = indentDirection == 'forward' ? 1 : -1;
  33. this.listenTo( editor.document, 'changesDone', () => {
  34. this.refreshState();
  35. } );
  36. }
  37. /**
  38. * @inheritDoc
  39. */
  40. _doExecute() {
  41. const doc = this.editor.document;
  42. const batch = doc.batch();
  43. let itemsToChange = Array.from( doc.selection.getSelectedBlocks() );
  44. doc.enqueueChanges( () => {
  45. const lastItem = itemsToChange[ itemsToChange.length - 1 ];
  46. // Indenting a list item should also indent all the items that are already sub-items of indented item.
  47. let next = lastItem.nextSibling;
  48. // Check all items after last indented item, as long as their indent is bigger than indent of that item.
  49. while ( next && next.name == 'listItem' && next.getAttribute( 'indent' ) > lastItem.getAttribute( 'indent' ) ) {
  50. itemsToChange.push( next );
  51. next = next.nextSibling;
  52. }
  53. // We need to be sure to keep model in correct state after each small change, because converters
  54. // bases on that state and assumes that model is correct.
  55. // Because of that, if the command outdents items, we will outdent them starting from the last item, as
  56. // it is safer.
  57. if ( this._indentBy < 0 ) {
  58. itemsToChange = itemsToChange.reverse();
  59. }
  60. for ( let item of itemsToChange ) {
  61. const indent = item.getAttribute( 'indent' ) + this._indentBy;
  62. // If indent is lower than 0, it means that the item got outdented when it was not indented.
  63. // This means that we need to convert that list item to paragraph.
  64. if ( indent < 0 ) {
  65. // To keep the model as correct as possible, first rename listItem, then remove attributes,
  66. // as listItem without attributes is very incorrect and will cause problems in converters.
  67. // No need to remove attributes, will be removed by post fixer.
  68. batch.rename( item, 'paragraph' );
  69. }
  70. // If indent is >= 0, change the attribute value.
  71. else {
  72. // Check whether list item's type should not be fixed.
  73. // First, find previous sibling with same indent - if it exists.
  74. let prev = item.previousSibling;
  75. while ( prev && prev.is( 'listItem' ) && prev.getAttribute( 'indent' ) > indent ) {
  76. prev = prev.previousSibling;
  77. }
  78. // Then check if that sibling has same type. If not, change type of this item.
  79. if ( prev && prev.getAttribute( 'type' ) != item.getAttribute( 'type' ) ) {
  80. batch.setAttribute( item, 'type', prev.getAttribute( 'type' ) );
  81. }
  82. batch.setAttribute( item, 'indent', indent );
  83. }
  84. }
  85. } );
  86. }
  87. /**
  88. * @inheritDoc
  89. */
  90. _checkEnabled() {
  91. // Check whether any of position's ancestor is a list item.
  92. const listItem = first( this.editor.document.selection.getSelectedBlocks() );
  93. // If selection is not in a list item, the command is disabled.
  94. if ( !listItem || !listItem.is( 'listItem' ) ) {
  95. return false;
  96. }
  97. if ( this._indentBy > 0 ) {
  98. // Cannot indent first item in it's list. Check if before `listItem` is a list item that is in same list.
  99. // To be in the same list, the item has to have same attributes.
  100. const indent = listItem.getAttribute( 'indent' );
  101. const type = listItem.getAttribute( 'type' );
  102. let prev = listItem.previousSibling;
  103. while ( prev && prev.is( 'listItem' ) ) {
  104. if ( prev.getAttribute( 'indent' ) == indent && prev.getAttribute( 'type' ) == type ) {
  105. return true;
  106. }
  107. prev = prev.previousSibling;
  108. }
  109. // Could not find similar list item, this means that `listItem` is first in its list.
  110. return false;
  111. }
  112. // If we are outdenting it is enough to be in list item. Every list item can always be outdented.
  113. return true;
  114. }
  115. }