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 |
|
10 | import { Command } from 'ckeditor5/src/core';
|
11 | import { 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 | */
|
18 | export 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 | }
|