UNPKG

6.99 kBJavaScriptView Raw
1/**
2 * @license Copyright (c) 2003-2024, 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 block-quote/blockquotecommand
7 */
8import { Command } from 'ckeditor5/src/core.js';
9import { first } from 'ckeditor5/src/utils.js';
10/**
11 * The block quote command plugin.
12 *
13 * @extends module:core/command~Command
14 */
15export default class BlockQuoteCommand extends Command {
16 /**
17 * @inheritDoc
18 */
19 refresh() {
20 this.value = this._getValue();
21 this.isEnabled = this._checkEnabled();
22 }
23 /**
24 * Executes the command. When the command {@link #value is on}, all top-most block quotes within
25 * the selection will be removed. If it is off, all selected blocks will be wrapped with
26 * a block quote.
27 *
28 * @fires execute
29 * @param options Command options.
30 * @param options.forceValue If set, it will force the command behavior. If `true`, the command will apply a block quote,
31 * otherwise the command will remove the block quote. If not set, the command will act basing on its current value.
32 */
33 execute(options = {}) {
34 const model = this.editor.model;
35 const schema = model.schema;
36 const selection = model.document.selection;
37 const blocks = Array.from(selection.getSelectedBlocks());
38 const value = (options.forceValue === undefined) ? !this.value : options.forceValue;
39 model.change(writer => {
40 if (!value) {
41 this._removeQuote(writer, blocks.filter(findQuote));
42 }
43 else {
44 const blocksToQuote = blocks.filter(block => {
45 // Already quoted blocks needs to be considered while quoting too
46 // in order to reuse their <bQ> elements.
47 return findQuote(block) || checkCanBeQuoted(schema, block);
48 });
49 this._applyQuote(writer, blocksToQuote);
50 }
51 });
52 }
53 /**
54 * Checks the command's {@link #value}.
55 */
56 _getValue() {
57 const selection = this.editor.model.document.selection;
58 const firstBlock = first(selection.getSelectedBlocks());
59 // In the current implementation, the block quote must be an immediate parent of a block element.
60 return !!(firstBlock && findQuote(firstBlock));
61 }
62 /**
63 * Checks whether the command can be enabled in the current context.
64 *
65 * @returns Whether the command should be enabled.
66 */
67 _checkEnabled() {
68 if (this.value) {
69 return true;
70 }
71 const selection = this.editor.model.document.selection;
72 const schema = this.editor.model.schema;
73 const firstBlock = first(selection.getSelectedBlocks());
74 if (!firstBlock) {
75 return false;
76 }
77 return checkCanBeQuoted(schema, firstBlock);
78 }
79 /**
80 * Removes the quote from given blocks.
81 *
82 * If blocks which are supposed to be "unquoted" are in the middle of a quote,
83 * start it or end it, then the quote will be split (if needed) and the blocks
84 * will be moved out of it, so other quoted blocks remained quoted.
85 */
86 _removeQuote(writer, blocks) {
87 // Unquote all groups of block. Iterate in the reverse order to not break following ranges.
88 getRangesOfBlockGroups(writer, blocks).reverse().forEach(groupRange => {
89 if (groupRange.start.isAtStart && groupRange.end.isAtEnd) {
90 writer.unwrap(groupRange.start.parent);
91 return;
92 }
93 // The group of blocks are at the beginning of an <bQ> so let's move them left (out of the <bQ>).
94 if (groupRange.start.isAtStart) {
95 const positionBefore = writer.createPositionBefore(groupRange.start.parent);
96 writer.move(groupRange, positionBefore);
97 return;
98 }
99 // The blocks are in the middle of an <bQ> so we need to split the <bQ> after the last block
100 // so we move the items there.
101 if (!groupRange.end.isAtEnd) {
102 writer.split(groupRange.end);
103 }
104 // Now we are sure that groupRange.end.isAtEnd is true, so let's move the blocks right.
105 const positionAfter = writer.createPositionAfter(groupRange.end.parent);
106 writer.move(groupRange, positionAfter);
107 });
108 }
109 /**
110 * Applies the quote to given blocks.
111 */
112 _applyQuote(writer, blocks) {
113 const quotesToMerge = [];
114 // Quote all groups of block. Iterate in the reverse order to not break following ranges.
115 getRangesOfBlockGroups(writer, blocks).reverse().forEach(groupRange => {
116 let quote = findQuote(groupRange.start);
117 if (!quote) {
118 quote = writer.createElement('blockQuote');
119 writer.wrap(groupRange, quote);
120 }
121 quotesToMerge.push(quote);
122 });
123 // Merge subsequent <bQ> elements. Reverse the order again because this time we want to go through
124 // the <bQ> elements in the source order (due to how merge works – it moves the right element's content
125 // to the first element and removes the right one. Since we may need to merge a couple of subsequent `<bQ>` elements
126 // we want to keep the reference to the first (furthest left) one.
127 quotesToMerge.reverse().reduce((currentQuote, nextQuote) => {
128 if (currentQuote.nextSibling == nextQuote) {
129 writer.merge(writer.createPositionAfter(currentQuote));
130 return currentQuote;
131 }
132 return nextQuote;
133 });
134 }
135}
136function findQuote(elementOrPosition) {
137 return elementOrPosition.parent.name == 'blockQuote' ? elementOrPosition.parent : null;
138}
139/**
140 * Returns a minimal array of ranges containing groups of subsequent blocks.
141 *
142 * content: abcdefgh
143 * blocks: [ a, b, d, f, g, h ]
144 * output ranges: [ab]c[d]e[fgh]
145 */
146function getRangesOfBlockGroups(writer, blocks) {
147 let startPosition;
148 let i = 0;
149 const ranges = [];
150 while (i < blocks.length) {
151 const block = blocks[i];
152 const nextBlock = blocks[i + 1];
153 if (!startPosition) {
154 startPosition = writer.createPositionBefore(block);
155 }
156 if (!nextBlock || block.nextSibling != nextBlock) {
157 ranges.push(writer.createRange(startPosition, writer.createPositionAfter(block)));
158 startPosition = null;
159 }
160 i++;
161 }
162 return ranges;
163}
164/**
165 * Checks whether <bQ> can wrap the block.
166 */
167function checkCanBeQuoted(schema, block) {
168 // TMP will be replaced with schema.checkWrap().
169 const isBQAllowed = schema.checkChild(block.parent, 'blockQuote');
170 const isBlockAllowedInBQ = schema.checkChild(['$root', 'blockQuote'], block);
171 return isBQAllowed && isBlockAllowedInBQ;
172}