UNPKG

6.43 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/utils/listwalker
7 */
8import { first, toArray } from 'ckeditor5/src/utils';
9import { isListItemBlock } from './model';
10/**
11 * Document list blocks iterator.
12 */
13export default class ListWalker {
14 /**
15 * Creates a document list iterator.
16 *
17 * @param startElement The start list item block element.
18 * @param options.direction The iterating direction.
19 * @param options.includeSelf Whether start block should be included in the result (if it's matching other criteria).
20 * @param options.sameAttributes Additional attributes that must be the same for each block.
21 * @param options.sameIndent Whether blocks with the same indent level as the start block should be included
22 * in the result.
23 * @param options.lowerIndent Whether blocks with a lower indent level than the start block should be included
24 * in the result.
25 * @param options.higherIndent Whether blocks with a higher indent level than the start block should be included
26 * in the result.
27 */
28 constructor(startElement, options) {
29 this._startElement = startElement;
30 this._referenceIndent = startElement.getAttribute('listIndent');
31 this._isForward = options.direction == 'forward';
32 this._includeSelf = !!options.includeSelf;
33 this._sameAttributes = toArray(options.sameAttributes || []);
34 this._sameIndent = !!options.sameIndent;
35 this._lowerIndent = !!options.lowerIndent;
36 this._higherIndent = !!options.higherIndent;
37 }
38 /**
39 * Performs only first step of iteration and returns the result.
40 *
41 * @param startElement The start list item block element.
42 * @param options.direction The iterating direction.
43 * @param options.includeSelf Whether start block should be included in the result (if it's matching other criteria).
44 * @param options.sameAttributes Additional attributes that must be the same for each block.
45 * @param options.sameIndent Whether blocks with the same indent level as the start block should be included
46 * in the result.
47 * @param options.lowerIndent Whether blocks with a lower indent level than the start block should be included
48 * in the result.
49 * @param options.higherIndent Whether blocks with a higher indent level than the start block should be included
50 * in the result.
51 */
52 static first(startElement, options) {
53 const walker = new this(startElement, options);
54 const iterator = walker[Symbol.iterator]();
55 return first(iterator);
56 }
57 /**
58 * Iterable interface.
59 */
60 *[Symbol.iterator]() {
61 const nestedItems = [];
62 for (const { node } of iterateSiblingListBlocks(this._getStartNode(), this._isForward ? 'forward' : 'backward')) {
63 const indent = node.getAttribute('listIndent');
64 // Leaving a nested list.
65 if (indent < this._referenceIndent) {
66 // Abort searching blocks.
67 if (!this._lowerIndent) {
68 break;
69 }
70 // While searching for lower indents, update the reference indent to find another parent in the next step.
71 this._referenceIndent = indent;
72 }
73 // Entering a nested list.
74 else if (indent > this._referenceIndent) {
75 // Ignore nested blocks.
76 if (!this._higherIndent) {
77 continue;
78 }
79 // Collect nested blocks to verify if they are really nested, or it's a different item.
80 if (!this._isForward) {
81 nestedItems.push(node);
82 continue;
83 }
84 }
85 // Same indent level block.
86 else {
87 // Ignore same indent block.
88 if (!this._sameIndent) {
89 // While looking for nested blocks, stop iterating while encountering first same indent block.
90 if (this._higherIndent) {
91 // No more nested blocks so yield nested items.
92 if (nestedItems.length) {
93 yield* nestedItems;
94 nestedItems.length = 0;
95 }
96 break;
97 }
98 continue;
99 }
100 // Abort if item has any additionally specified attribute different.
101 if (this._sameAttributes.some(attr => node.getAttribute(attr) !== this._startElement.getAttribute(attr))) {
102 break;
103 }
104 }
105 // There is another block for the same list item so the nested items were in the same list item.
106 if (nestedItems.length) {
107 yield* nestedItems;
108 nestedItems.length = 0;
109 }
110 yield node;
111 }
112 }
113 /**
114 * Returns the model element to start iterating.
115 */
116 _getStartNode() {
117 if (this._includeSelf) {
118 return this._startElement;
119 }
120 return this._isForward ?
121 this._startElement.nextSibling :
122 this._startElement.previousSibling;
123 }
124}
125/**
126 * Iterates sibling list blocks starting from the given node.
127 *
128 * @internal
129 * @param node The model node.
130 * @param direction Iteration direction.
131 * @returns The object with `node` and `previous` {@link module:engine/model/element~Element blocks}.
132 */
133export function* iterateSiblingListBlocks(node, direction = 'forward') {
134 const isForward = direction == 'forward';
135 let previous = null;
136 while (isListItemBlock(node)) {
137 yield { node, previous };
138 previous = node;
139 node = isForward ? node.nextSibling : node.previousSibling;
140 }
141}
142/**
143 * The iterable protocol over the list elements.
144 *
145 * @internal
146 */
147export class ListBlocksIterable {
148 /**
149 * @param listHead The head element of a list.
150 */
151 constructor(listHead) {
152 this._listHead = listHead;
153 }
154 /**
155 * List blocks iterator.
156 *
157 * Iterates over all blocks of a list.
158 */
159 [Symbol.iterator]() {
160 return iterateSiblingListBlocks(this._listHead, 'forward');
161 }
162}