UNPKG

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