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 | import { Command } from 'ckeditor5/src/core';
|
6 | import { splitListItemBefore, expandListBlocksToCompleteItems, getListItemBlocks, getListItems, removeListAttributes, outdentFollowingItems, ListItemUid, sortBlocks, getSelectedBlockObject, isListItemBlock } from './utils/model';
|
7 | /**
|
8 | * The list command. It is used by the {@link module:list/documentlist~DocumentList document list feature}.
|
9 | */
|
10 | export default class DocumentListCommand extends Command {
|
11 | /**
|
12 | * Creates an instance of the command.
|
13 | *
|
14 | * @param editor The editor instance.
|
15 | * @param type List type that will be handled by this command.
|
16 | */
|
17 | constructor(editor, type) {
|
18 | super(editor);
|
19 | this.type = type;
|
20 | }
|
21 | /**
|
22 | * @inheritDoc
|
23 | */
|
24 | refresh() {
|
25 | this.value = this._getValue();
|
26 | this.isEnabled = this._checkEnabled();
|
27 | }
|
28 | /**
|
29 | * Executes the list command.
|
30 | *
|
31 | * @fires execute
|
32 | * @fires afterExecute
|
33 | * @param options Command options.
|
34 | * @param options.forceValue If set, it will force the command behavior. If `true`, the command will try to convert the
|
35 | * selected items and potentially the neighbor elements to the proper list items. If set to `false` it will convert selected elements
|
36 | * to paragraphs. If not set, the command will toggle selected elements to list items or paragraphs, depending on the selection.
|
37 | */
|
38 | execute(options = {}) {
|
39 | const model = this.editor.model;
|
40 | const document = model.document;
|
41 | const selectedBlockObject = getSelectedBlockObject(model);
|
42 | const blocks = Array.from(document.selection.getSelectedBlocks())
|
43 | .filter(block => model.schema.checkAttribute(block, 'listType'));
|
44 | // Whether we are turning off some items.
|
45 | const turnOff = options.forceValue !== undefined ? !options.forceValue : this.value;
|
46 | model.change(writer => {
|
47 | if (turnOff) {
|
48 | const lastBlock = blocks[blocks.length - 1];
|
49 | // Split the first block from the list item.
|
50 | const itemBlocks = getListItemBlocks(lastBlock, { direction: 'forward' });
|
51 | const changedBlocks = [];
|
52 | if (itemBlocks.length > 1) {
|
53 | changedBlocks.push(...splitListItemBefore(itemBlocks[1], writer));
|
54 | }
|
55 | // Convert list blocks to plain blocks.
|
56 | changedBlocks.push(...removeListAttributes(blocks, writer));
|
57 | // Outdent items following the selected list item.
|
58 | changedBlocks.push(...outdentFollowingItems(lastBlock, writer));
|
59 | this._fireAfterExecute(changedBlocks);
|
60 | }
|
61 | // Turning on the list items for a collapsed selection inside a list item.
|
62 | else if ((selectedBlockObject || document.selection.isCollapsed) && isListItemBlock(blocks[0])) {
|
63 | const changedBlocks = getListItems(selectedBlockObject || blocks[0]);
|
64 | for (const block of changedBlocks) {
|
65 | writer.setAttribute('listType', this.type, block);
|
66 | }
|
67 | this._fireAfterExecute(changedBlocks);
|
68 | }
|
69 | // Turning on the list items for a non-collapsed selection.
|
70 | else {
|
71 | const changedBlocks = [];
|
72 | for (const block of blocks) {
|
73 | // Promote the given block to the list item.
|
74 | if (!block.hasAttribute('listType')) {
|
75 | writer.setAttributes({
|
76 | listIndent: 0,
|
77 | listItemId: ListItemUid.next(),
|
78 | listType: this.type
|
79 | }, block);
|
80 | changedBlocks.push(block);
|
81 | }
|
82 | // Change the type of list item.
|
83 | else {
|
84 | for (const node of expandListBlocksToCompleteItems(block, { withNested: false })) {
|
85 | if (node.getAttribute('listType') != this.type) {
|
86 | writer.setAttribute('listType', this.type, node);
|
87 | changedBlocks.push(node);
|
88 | }
|
89 | }
|
90 | }
|
91 | }
|
92 | this._fireAfterExecute(changedBlocks);
|
93 | }
|
94 | });
|
95 | }
|
96 | /**
|
97 | * Fires the `afterExecute` event.
|
98 | *
|
99 | * @param changedBlocks The changed list elements.
|
100 | */
|
101 | _fireAfterExecute(changedBlocks) {
|
102 | this.fire('afterExecute', sortBlocks(new Set(changedBlocks)));
|
103 | }
|
104 | /**
|
105 | * Checks the command's {@link #value}.
|
106 | *
|
107 | * @returns The current value.
|
108 | */
|
109 | _getValue() {
|
110 | const selection = this.editor.model.document.selection;
|
111 | const blocks = Array.from(selection.getSelectedBlocks());
|
112 | if (!blocks.length) {
|
113 | return false;
|
114 | }
|
115 | for (const block of blocks) {
|
116 | if (block.getAttribute('listType') != this.type) {
|
117 | return false;
|
118 | }
|
119 | }
|
120 | return true;
|
121 | }
|
122 | /**
|
123 | * Checks whether the command can be enabled in the current context.
|
124 | *
|
125 | * @returns Whether the command should be enabled.
|
126 | */
|
127 | _checkEnabled() {
|
128 | const selection = this.editor.model.document.selection;
|
129 | const schema = this.editor.model.schema;
|
130 | const blocks = Array.from(selection.getSelectedBlocks());
|
131 | if (!blocks.length) {
|
132 | return false;
|
133 | }
|
134 | // If command value is true it means that we are in list item, so the command should be enabled.
|
135 | if (this.value) {
|
136 | return true;
|
137 | }
|
138 | for (const block of blocks) {
|
139 | if (schema.checkAttribute(block, 'listType')) {
|
140 | return true;
|
141 | }
|
142 | }
|
143 | return false;
|
144 | }
|
145 | }
|