UNPKG

5.39 kBJavaScriptView Raw
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 */
8import { Command } from 'ckeditor5/src/core';
9import { expandListBlocksToCompleteItems, indentBlocks, isFirstBlockOfListItem, isListItemBlock, isSingleListItem, outdentBlocksWithMerge, sortBlocks, splitListItemBefore } from './utils/model';
10import ListWalker from './utils/listwalker';
11/**
12 * The document list indent command. It is used by the {@link module:list/documentlist~DocumentList list feature}.
13 */
14export 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 */
122function 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}