1 | /**
|
2 | * @license Copyright (c) 2003-2023, 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 | * @module list/documentlist/documentlistindentcommand
|
7 | */
|
8 | import { Command } from 'ckeditor5/src/core';
|
9 | import { expandListBlocksToCompleteItems, indentBlocks, isFirstBlockOfListItem, isListItemBlock, isSingleListItem, outdentBlocksWithMerge, sortBlocks, splitListItemBefore } from './utils/model';
|
10 | import ListWalker from './utils/listwalker';
|
11 | /**
|
12 | * The document list indent command. It is used by the {@link module:list/documentlist~DocumentList list feature}.
|
13 | */
|
14 | export default class DocumentListIndentCommand extends Command {
|
15 | /**
|
16 | * Creates an instance of the command.
|
17 | *
|
18 | * @param editor The editor instance.
|
19 | * @param indentDirection The direction of indent. If it is equal to `backward`, the command
|
20 | * will outdent a list item.
|
21 | */
|
22 | constructor(editor, indentDirection) {
|
23 | super(editor);
|
24 | this._direction = indentDirection;
|
25 | }
|
26 | /**
|
27 | * @inheritDoc
|
28 | */
|
29 | refresh() {
|
30 | this.isEnabled = this._checkEnabled();
|
31 | }
|
32 | /**
|
33 | * Indents or outdents (depending on the {@link #constructor}'s `indentDirection` parameter) selected list items.
|
34 | *
|
35 | * @fires execute
|
36 | * @fires afterExecute
|
37 | */
|
38 | execute() {
|
39 | const model = this.editor.model;
|
40 | const blocks = getSelectedListBlocks(model.document.selection);
|
41 | model.change(writer => {
|
42 | const changedBlocks = [];
|
43 | // Handle selection contained in the single list item and starting in the following blocks.
|
44 | if (isSingleListItem(blocks) && !isFirstBlockOfListItem(blocks[0])) {
|
45 | // Allow increasing indent of following list item blocks.
|
46 | if (this._direction == 'forward') {
|
47 | changedBlocks.push(...indentBlocks(blocks, writer));
|
48 | }
|
49 | // For indent make sure that indented blocks have a new ID.
|
50 | // For outdent just split blocks from the list item (give them a new IDs).
|
51 | changedBlocks.push(...splitListItemBefore(blocks[0], writer));
|
52 | }
|
53 | // More than a single list item is selected, or the first block of list item is selected.
|
54 | else {
|
55 | // Now just update the attributes of blocks.
|
56 | if (this._direction == 'forward') {
|
57 | changedBlocks.push(...indentBlocks(blocks, writer, { expand: true }));
|
58 | }
|
59 | else {
|
60 | changedBlocks.push(...outdentBlocksWithMerge(blocks, writer));
|
61 | }
|
62 | }
|
63 | // Align the list item type to match the previous list item (from the same list).
|
64 | for (const block of changedBlocks) {
|
65 | // This block become a plain block (for example a paragraph).
|
66 | if (!block.hasAttribute('listType')) {
|
67 | continue;
|
68 | }
|
69 | const previousItemBlock = ListWalker.first(block, { sameIndent: true });
|
70 | if (previousItemBlock) {
|
71 | writer.setAttribute('listType', previousItemBlock.getAttribute('listType'), block);
|
72 | }
|
73 | }
|
74 | this._fireAfterExecute(changedBlocks);
|
75 | });
|
76 | }
|
77 | /**
|
78 | * Fires the `afterExecute` event.
|
79 | *
|
80 | * @param changedBlocks The changed list elements.
|
81 | */
|
82 | _fireAfterExecute(changedBlocks) {
|
83 | this.fire('afterExecute', sortBlocks(new Set(changedBlocks)));
|
84 | }
|
85 | /**
|
86 | * Checks whether the command can be enabled in the current context.
|
87 | *
|
88 | * @returns Whether the command should be enabled.
|
89 | */
|
90 | _checkEnabled() {
|
91 | // Check whether any of position's ancestor is a list item.
|
92 | let blocks = getSelectedListBlocks(this.editor.model.document.selection);
|
93 | let firstBlock = blocks[0];
|
94 | // If selection is not in a list item, the command is disabled.
|
95 | if (!firstBlock) {
|
96 | return false;
|
97 | }
|
98 | // If we are outdenting it is enough to be in list item. Every list item can always be outdented.
|
99 | if (this._direction == 'backward') {
|
100 | return true;
|
101 | }
|
102 | // A single block of a list item is selected, so it could be indented as a sublist.
|
103 | if (isSingleListItem(blocks) && !isFirstBlockOfListItem(blocks[0])) {
|
104 | return true;
|
105 | }
|
106 | blocks = expandListBlocksToCompleteItems(blocks);
|
107 | firstBlock = blocks[0];
|
108 | // Check if there is any list item before selected items that could become a parent of selected items.
|
109 | const siblingItem = ListWalker.first(firstBlock, { sameIndent: true });
|
110 | if (!siblingItem) {
|
111 | return false;
|
112 | }
|
113 | if (siblingItem.getAttribute('listType') == firstBlock.getAttribute('listType')) {
|
114 | return true;
|
115 | }
|
116 | return false;
|
117 | }
|
118 | }
|
119 | /**
|
120 | * Returns an array of selected blocks truncated to the first non list block element.
|
121 | */
|
122 | function getSelectedListBlocks(selection) {
|
123 | const blocks = Array.from(selection.getSelectedBlocks());
|
124 | const firstNonListBlockIndex = blocks.findIndex(block => !isListItemBlock(block));
|
125 | if (firstNonListBlockIndex != -1) {
|
126 | blocks.length = firstNonListBlockIndex;
|
127 | }
|
128 | return blocks;
|
129 | }
|