@ckeditor/ckeditor5-utils 40.0.0 → 40.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +26 -26
- package/LICENSE.md +3 -3
- package/package.json +1 -1
- package/src/abortabledebounce.d.ts +17 -0
- package/src/abortabledebounce.js +22 -0
- package/src/areconnectedthroughproperties.d.ts +11 -11
- package/src/areconnectedthroughproperties.js +73 -73
- package/src/ckeditorerror.d.ts +123 -123
- package/src/ckeditorerror.js +176 -176
- package/src/collection.d.ts +433 -427
- package/src/collection.js +583 -575
- package/src/comparearrays.d.ts +30 -30
- package/src/comparearrays.js +47 -47
- package/src/config.d.ts +163 -163
- package/src/config.js +163 -162
- package/src/count.d.ts +18 -18
- package/src/count.js +24 -24
- package/src/delay.d.ts +19 -19
- package/src/delay.js +26 -26
- package/src/diff.d.ts +31 -31
- package/src/diff.js +115 -115
- package/src/difftochanges.d.ts +59 -59
- package/src/difftochanges.js +79 -79
- package/src/dom/createelement.d.ts +57 -57
- package/src/dom/createelement.js +40 -40
- package/src/dom/emittermixin.d.ts +142 -142
- package/src/dom/emittermixin.js +239 -239
- package/src/dom/findclosestscrollableancestor.d.ts +11 -11
- package/src/dom/findclosestscrollableancestor.js +31 -31
- package/src/dom/getancestors.d.ts +17 -17
- package/src/dom/getancestors.js +27 -27
- package/src/dom/getborderwidths.d.ts +24 -24
- package/src/dom/getborderwidths.js +24 -24
- package/src/dom/getcommonancestor.d.ts +12 -12
- package/src/dom/getcommonancestor.js +25 -25
- package/src/dom/getdatafromelement.d.ts +14 -14
- package/src/dom/getdatafromelement.js +20 -20
- package/src/dom/getpositionedancestor.d.ts +10 -10
- package/src/dom/getpositionedancestor.js +22 -22
- package/src/dom/global.d.ts +32 -32
- package/src/dom/global.js +35 -35
- package/src/dom/indexof.d.ts +14 -14
- package/src/dom/indexof.js +21 -21
- package/src/dom/insertat.d.ts +15 -15
- package/src/dom/insertat.js +17 -17
- package/src/dom/iscomment.d.ts +11 -11
- package/src/dom/iscomment.js +14 -14
- package/src/dom/isnode.d.ts +11 -11
- package/src/dom/isnode.js +21 -21
- package/src/dom/isrange.d.ts +11 -11
- package/src/dom/isrange.js +13 -13
- package/src/dom/istext.d.ts +11 -11
- package/src/dom/istext.js +13 -13
- package/src/dom/isvalidattributename.d.ts +10 -10
- package/src/dom/isvalidattributename.js +22 -22
- package/src/dom/isvisible.d.ts +18 -18
- package/src/dom/isvisible.js +20 -20
- package/src/dom/iswindow.d.ts +11 -11
- package/src/dom/iswindow.js +22 -22
- package/src/dom/position.d.ts +211 -211
- package/src/dom/position.js +313 -313
- package/src/dom/rect.d.ts +195 -195
- package/src/dom/rect.js +474 -474
- package/src/dom/remove.d.ts +13 -13
- package/src/dom/remove.js +18 -18
- package/src/dom/resizeobserver.d.ts +74 -74
- package/src/dom/resizeobserver.js +126 -126
- package/src/dom/scroll.d.ts +73 -73
- package/src/dom/scroll.js +383 -383
- package/src/dom/setdatainelement.d.ts +14 -14
- package/src/dom/setdatainelement.js +20 -20
- package/src/dom/tounit.d.ts +22 -22
- package/src/dom/tounit.js +16 -16
- package/src/elementreplacer.d.ts +31 -31
- package/src/elementreplacer.js +43 -43
- package/src/emittermixin.d.ts +312 -312
- package/src/emittermixin.js +453 -453
- package/src/env.d.ts +117 -117
- package/src/env.js +122 -122
- package/src/eventinfo.d.ts +58 -58
- package/src/eventinfo.js +26 -26
- package/src/fastdiff.d.ts +112 -112
- package/src/fastdiff.js +248 -248
- package/src/first.d.ts +11 -11
- package/src/first.js +17 -17
- package/src/focustracker.d.ts +75 -75
- package/src/focustracker.js +95 -95
- package/src/index.d.ts +64 -61
- package/src/index.js +63 -60
- package/src/inserttopriorityarray.d.ts +30 -30
- package/src/inserttopriorityarray.js +21 -21
- package/src/isiterable.d.ts +14 -14
- package/src/isiterable.js +16 -16
- package/src/keyboard.d.ts +126 -126
- package/src/keyboard.js +221 -221
- package/src/keystrokehandler.d.ts +87 -87
- package/src/keystrokehandler.js +122 -122
- package/src/language.d.ts +17 -17
- package/src/language.js +19 -19
- package/src/locale.d.ts +120 -120
- package/src/locale.js +76 -76
- package/src/mapsequal.d.ts +15 -15
- package/src/mapsequal.js +27 -27
- package/src/mix.d.ts +85 -85
- package/src/mix.js +50 -50
- package/src/nth.d.ts +16 -16
- package/src/nth.js +24 -24
- package/src/objecttomap.d.ts +23 -23
- package/src/objecttomap.js +27 -27
- package/src/observablemixin.d.ts +560 -560
- package/src/observablemixin.js +580 -580
- package/src/priorities.d.ts +33 -33
- package/src/priorities.js +23 -23
- package/src/retry.d.ts +33 -0
- package/src/retry.js +47 -0
- package/src/splicearray.d.ts +26 -26
- package/src/splicearray.js +40 -40
- package/src/spy.d.ts +21 -21
- package/src/spy.js +22 -22
- package/src/toarray.d.ts +25 -25
- package/src/toarray.js +7 -7
- package/src/tomap.d.ts +19 -19
- package/src/tomap.js +29 -29
- package/src/translation-service.d.ts +168 -168
- package/src/translation-service.js +198 -198
- package/src/uid.d.ts +15 -15
- package/src/uid.js +57 -57
- package/src/unicode.d.ts +54 -54
- package/src/unicode.js +85 -85
- package/src/verifylicense.d.ts +15 -15
- package/src/verifylicense.js +87 -87
- package/src/version.d.ts +10 -10
- package/src/version.js +153 -153
- package/src/wait.d.ts +16 -0
- package/src/wait.js +29 -0
package/src/collection.js
CHANGED
|
@@ -1,575 +1,583 @@
|
|
|
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 utils/collection
|
|
7
|
-
*/
|
|
8
|
-
import EmitterMixin from './emittermixin';
|
|
9
|
-
import CKEditorError from './ckeditorerror';
|
|
10
|
-
import uid from './uid';
|
|
11
|
-
import isIterable from './isiterable';
|
|
12
|
-
/**
|
|
13
|
-
* Collections are ordered sets of objects. Items in the collection can be retrieved by their indexes
|
|
14
|
-
* in the collection (like in an array) or by their ids.
|
|
15
|
-
*
|
|
16
|
-
* If an object without an `id` property is being added to the collection, the `id` property will be generated
|
|
17
|
-
* automatically. Note that the automatically generated id is unique only within this single collection instance.
|
|
18
|
-
*
|
|
19
|
-
* By default an item in the collection is identified by its `id` property. The name of the identifier can be
|
|
20
|
-
* configured through the constructor of the collection.
|
|
21
|
-
*
|
|
22
|
-
* @typeParam T The type of the collection element.
|
|
23
|
-
*/
|
|
24
|
-
export default class Collection extends EmitterMixin() {
|
|
25
|
-
constructor(initialItemsOrOptions = {}, options = {}) {
|
|
26
|
-
super();
|
|
27
|
-
const hasInitialItems = isIterable(initialItemsOrOptions);
|
|
28
|
-
if (!hasInitialItems) {
|
|
29
|
-
options = initialItemsOrOptions;
|
|
30
|
-
}
|
|
31
|
-
this._items = [];
|
|
32
|
-
this._itemMap = new Map();
|
|
33
|
-
this._idProperty = options.idProperty || 'id';
|
|
34
|
-
this._bindToExternalToInternalMap = new WeakMap();
|
|
35
|
-
this._bindToInternalToExternalMap = new WeakMap();
|
|
36
|
-
this._skippedIndexesFromExternal = [];
|
|
37
|
-
// Set the initial content of the collection (if provided in the constructor).
|
|
38
|
-
if (hasInitialItems) {
|
|
39
|
-
for (const item of initialItemsOrOptions) {
|
|
40
|
-
this._items.push(item);
|
|
41
|
-
this._itemMap.set(this._getItemIdBeforeAdding(item), item);
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
/**
|
|
46
|
-
* The number of items available in the collection.
|
|
47
|
-
*/
|
|
48
|
-
get length() {
|
|
49
|
-
return this._items.length;
|
|
50
|
-
}
|
|
51
|
-
/**
|
|
52
|
-
* Returns the first item from the collection or null when collection is empty.
|
|
53
|
-
*/
|
|
54
|
-
get first() {
|
|
55
|
-
return this._items[0] || null;
|
|
56
|
-
}
|
|
57
|
-
/**
|
|
58
|
-
* Returns the last item from the collection or null when collection is empty.
|
|
59
|
-
*/
|
|
60
|
-
get last() {
|
|
61
|
-
return this._items[this.length - 1] || null;
|
|
62
|
-
}
|
|
63
|
-
/**
|
|
64
|
-
* Adds an item into the collection.
|
|
65
|
-
*
|
|
66
|
-
* If the item does not have an id, then it will be automatically generated and set on the item.
|
|
67
|
-
*
|
|
68
|
-
* @param item
|
|
69
|
-
* @param index The position of the item in the collection. The item
|
|
70
|
-
* is pushed to the collection when `index` not specified.
|
|
71
|
-
* @fires add
|
|
72
|
-
* @fires change
|
|
73
|
-
*/
|
|
74
|
-
add(item, index) {
|
|
75
|
-
return this.addMany([item], index);
|
|
76
|
-
}
|
|
77
|
-
/**
|
|
78
|
-
* Adds multiple items into the collection.
|
|
79
|
-
*
|
|
80
|
-
* Any item not containing an id will get an automatically generated one.
|
|
81
|
-
*
|
|
82
|
-
* @param items
|
|
83
|
-
* @param index The position of the insertion. Items will be appended if no `index` is specified.
|
|
84
|
-
* @fires add
|
|
85
|
-
* @fires change
|
|
86
|
-
*/
|
|
87
|
-
addMany(items, index) {
|
|
88
|
-
if (index === undefined) {
|
|
89
|
-
index = this._items.length;
|
|
90
|
-
}
|
|
91
|
-
else if (index > this._items.length || index < 0) {
|
|
92
|
-
/**
|
|
93
|
-
* The `index` passed to {@link module:utils/collection~Collection#addMany `Collection#addMany()`}
|
|
94
|
-
* is invalid. It must be a number between 0 and the collection's length.
|
|
95
|
-
*
|
|
96
|
-
* @error collection-add-item-invalid-index
|
|
97
|
-
*/
|
|
98
|
-
throw new CKEditorError('collection-add-item-invalid-index', this);
|
|
99
|
-
}
|
|
100
|
-
let offset = 0;
|
|
101
|
-
for (const item of items) {
|
|
102
|
-
const itemId = this._getItemIdBeforeAdding(item);
|
|
103
|
-
const currentItemIndex = index + offset;
|
|
104
|
-
this._items.splice(currentItemIndex, 0, item);
|
|
105
|
-
this._itemMap.set(itemId, item);
|
|
106
|
-
this.fire('add', item, currentItemIndex);
|
|
107
|
-
offset++;
|
|
108
|
-
}
|
|
109
|
-
this.fire('change', {
|
|
110
|
-
added: items,
|
|
111
|
-
removed: [],
|
|
112
|
-
index
|
|
113
|
-
});
|
|
114
|
-
return this;
|
|
115
|
-
}
|
|
116
|
-
/**
|
|
117
|
-
* Gets an item by its ID or index.
|
|
118
|
-
*
|
|
119
|
-
* @param idOrIndex The item ID or index in the collection.
|
|
120
|
-
* @returns The requested item or `null` if such item does not exist.
|
|
121
|
-
*/
|
|
122
|
-
get(idOrIndex) {
|
|
123
|
-
let item;
|
|
124
|
-
if (typeof idOrIndex == 'string') {
|
|
125
|
-
item = this._itemMap.get(idOrIndex);
|
|
126
|
-
}
|
|
127
|
-
else if (typeof idOrIndex == 'number') {
|
|
128
|
-
item = this._items[idOrIndex];
|
|
129
|
-
}
|
|
130
|
-
else {
|
|
131
|
-
/**
|
|
132
|
-
* An index or ID must be given.
|
|
133
|
-
*
|
|
134
|
-
* @error collection-get-invalid-arg
|
|
135
|
-
*/
|
|
136
|
-
throw new CKEditorError('collection-get-invalid-arg', this);
|
|
137
|
-
}
|
|
138
|
-
return item || null;
|
|
139
|
-
}
|
|
140
|
-
/**
|
|
141
|
-
* Returns a Boolean indicating whether the collection contains an item.
|
|
142
|
-
*
|
|
143
|
-
* @param itemOrId The item or its ID in the collection.
|
|
144
|
-
* @returns `true` if the collection contains the item, `false` otherwise.
|
|
145
|
-
*/
|
|
146
|
-
has(itemOrId) {
|
|
147
|
-
if (typeof itemOrId == 'string') {
|
|
148
|
-
return this._itemMap.has(itemOrId);
|
|
149
|
-
}
|
|
150
|
-
else { // Object
|
|
151
|
-
const idProperty = this._idProperty;
|
|
152
|
-
const id = itemOrId[idProperty];
|
|
153
|
-
return id && this._itemMap.has(id);
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
/**
|
|
157
|
-
* Gets an index of an item in the collection.
|
|
158
|
-
* When an item is not defined in the collection, the index will equal -1.
|
|
159
|
-
*
|
|
160
|
-
* @param itemOrId The item or its ID in the collection.
|
|
161
|
-
* @returns The index of a given item.
|
|
162
|
-
*/
|
|
163
|
-
getIndex(itemOrId) {
|
|
164
|
-
let item;
|
|
165
|
-
if (typeof itemOrId == 'string') {
|
|
166
|
-
item = this._itemMap.get(itemOrId);
|
|
167
|
-
}
|
|
168
|
-
else {
|
|
169
|
-
item = itemOrId;
|
|
170
|
-
}
|
|
171
|
-
return item ? this._items.indexOf(item) : -1;
|
|
172
|
-
}
|
|
173
|
-
/**
|
|
174
|
-
* Removes an item from the collection.
|
|
175
|
-
*
|
|
176
|
-
* @param subject The item to remove, its ID or index in the collection.
|
|
177
|
-
* @returns The removed item.
|
|
178
|
-
* @fires remove
|
|
179
|
-
* @fires change
|
|
180
|
-
*/
|
|
181
|
-
remove(subject) {
|
|
182
|
-
const [item, index] = this._remove(subject);
|
|
183
|
-
this.fire('change', {
|
|
184
|
-
added: [],
|
|
185
|
-
removed: [item],
|
|
186
|
-
index
|
|
187
|
-
});
|
|
188
|
-
return item;
|
|
189
|
-
}
|
|
190
|
-
/**
|
|
191
|
-
* Executes the callback for each item in the collection and composes an array or values returned by this callback.
|
|
192
|
-
*
|
|
193
|
-
* @typeParam U The result type of the callback.
|
|
194
|
-
* @param callback
|
|
195
|
-
* @param ctx Context in which the `callback` will be called.
|
|
196
|
-
* @returns The result of mapping.
|
|
197
|
-
*/
|
|
198
|
-
map(callback, ctx) {
|
|
199
|
-
return this._items.map(callback, ctx);
|
|
200
|
-
}
|
|
201
|
-
/**
|
|
202
|
-
*
|
|
203
|
-
*
|
|
204
|
-
* @param callback
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
*
|
|
213
|
-
*
|
|
214
|
-
* @
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
*
|
|
223
|
-
*
|
|
224
|
-
*
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
this.
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
*
|
|
253
|
-
*
|
|
254
|
-
*
|
|
255
|
-
*
|
|
256
|
-
*
|
|
257
|
-
*
|
|
258
|
-
*
|
|
259
|
-
*
|
|
260
|
-
*
|
|
261
|
-
*
|
|
262
|
-
*
|
|
263
|
-
*
|
|
264
|
-
*
|
|
265
|
-
*
|
|
266
|
-
*
|
|
267
|
-
*
|
|
268
|
-
* source.
|
|
269
|
-
*
|
|
270
|
-
*
|
|
271
|
-
*
|
|
272
|
-
*
|
|
273
|
-
*
|
|
274
|
-
*
|
|
275
|
-
*
|
|
276
|
-
*
|
|
277
|
-
*
|
|
278
|
-
*
|
|
279
|
-
*
|
|
280
|
-
*
|
|
281
|
-
*
|
|
282
|
-
*
|
|
283
|
-
*
|
|
284
|
-
* class
|
|
285
|
-
* public label: string;
|
|
286
|
-
*
|
|
287
|
-
* constructor( data: { label: string } ) {
|
|
288
|
-
* this.label = data.label;
|
|
289
|
-
* }
|
|
290
|
-
* }
|
|
291
|
-
*
|
|
292
|
-
*
|
|
293
|
-
*
|
|
294
|
-
*
|
|
295
|
-
*
|
|
296
|
-
*
|
|
297
|
-
*
|
|
298
|
-
*
|
|
299
|
-
*
|
|
300
|
-
*
|
|
301
|
-
*
|
|
302
|
-
*
|
|
303
|
-
* source.
|
|
304
|
-
*
|
|
305
|
-
*
|
|
306
|
-
*
|
|
307
|
-
*
|
|
308
|
-
*
|
|
309
|
-
*
|
|
310
|
-
*
|
|
311
|
-
*
|
|
312
|
-
*
|
|
313
|
-
*
|
|
314
|
-
*
|
|
315
|
-
*
|
|
316
|
-
*
|
|
317
|
-
*
|
|
318
|
-
*
|
|
319
|
-
*
|
|
320
|
-
*
|
|
321
|
-
*
|
|
322
|
-
*
|
|
323
|
-
*
|
|
324
|
-
*
|
|
325
|
-
*
|
|
326
|
-
*
|
|
327
|
-
*
|
|
328
|
-
*
|
|
329
|
-
*
|
|
330
|
-
*
|
|
331
|
-
*
|
|
332
|
-
*
|
|
333
|
-
*
|
|
334
|
-
*
|
|
335
|
-
*
|
|
336
|
-
*
|
|
337
|
-
*
|
|
338
|
-
*
|
|
339
|
-
* }
|
|
340
|
-
*
|
|
341
|
-
*
|
|
342
|
-
*
|
|
343
|
-
*
|
|
344
|
-
*
|
|
345
|
-
*
|
|
346
|
-
*
|
|
347
|
-
*
|
|
348
|
-
*
|
|
349
|
-
*
|
|
350
|
-
*
|
|
351
|
-
*
|
|
352
|
-
*
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
this._bindToCollection
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
// When
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
//
|
|
413
|
-
//
|
|
414
|
-
|
|
415
|
-
// external
|
|
416
|
-
//
|
|
417
|
-
//
|
|
418
|
-
//
|
|
419
|
-
//
|
|
420
|
-
// internal -> [
|
|
421
|
-
//
|
|
422
|
-
//
|
|
423
|
-
//
|
|
424
|
-
for
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
//
|
|
430
|
-
//
|
|
431
|
-
//
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
// external
|
|
438
|
-
//
|
|
439
|
-
//
|
|
440
|
-
//
|
|
441
|
-
// internal -> [ 'A', 'B - skipped for external', 'C - skipped for external'
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
if (
|
|
505
|
-
/**
|
|
506
|
-
* This item
|
|
507
|
-
*
|
|
508
|
-
* @error collection-add-
|
|
509
|
-
*/
|
|
510
|
-
throw new CKEditorError('collection-add-
|
|
511
|
-
}
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
item = this.
|
|
542
|
-
itemDoesNotExist = !item;
|
|
543
|
-
if (item) {
|
|
544
|
-
|
|
545
|
-
}
|
|
546
|
-
}
|
|
547
|
-
else {
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
}
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
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 utils/collection
|
|
7
|
+
*/
|
|
8
|
+
import EmitterMixin from './emittermixin';
|
|
9
|
+
import CKEditorError from './ckeditorerror';
|
|
10
|
+
import uid from './uid';
|
|
11
|
+
import isIterable from './isiterable';
|
|
12
|
+
/**
|
|
13
|
+
* Collections are ordered sets of objects. Items in the collection can be retrieved by their indexes
|
|
14
|
+
* in the collection (like in an array) or by their ids.
|
|
15
|
+
*
|
|
16
|
+
* If an object without an `id` property is being added to the collection, the `id` property will be generated
|
|
17
|
+
* automatically. Note that the automatically generated id is unique only within this single collection instance.
|
|
18
|
+
*
|
|
19
|
+
* By default an item in the collection is identified by its `id` property. The name of the identifier can be
|
|
20
|
+
* configured through the constructor of the collection.
|
|
21
|
+
*
|
|
22
|
+
* @typeParam T The type of the collection element.
|
|
23
|
+
*/
|
|
24
|
+
export default class Collection extends EmitterMixin() {
|
|
25
|
+
constructor(initialItemsOrOptions = {}, options = {}) {
|
|
26
|
+
super();
|
|
27
|
+
const hasInitialItems = isIterable(initialItemsOrOptions);
|
|
28
|
+
if (!hasInitialItems) {
|
|
29
|
+
options = initialItemsOrOptions;
|
|
30
|
+
}
|
|
31
|
+
this._items = [];
|
|
32
|
+
this._itemMap = new Map();
|
|
33
|
+
this._idProperty = options.idProperty || 'id';
|
|
34
|
+
this._bindToExternalToInternalMap = new WeakMap();
|
|
35
|
+
this._bindToInternalToExternalMap = new WeakMap();
|
|
36
|
+
this._skippedIndexesFromExternal = [];
|
|
37
|
+
// Set the initial content of the collection (if provided in the constructor).
|
|
38
|
+
if (hasInitialItems) {
|
|
39
|
+
for (const item of initialItemsOrOptions) {
|
|
40
|
+
this._items.push(item);
|
|
41
|
+
this._itemMap.set(this._getItemIdBeforeAdding(item), item);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* The number of items available in the collection.
|
|
47
|
+
*/
|
|
48
|
+
get length() {
|
|
49
|
+
return this._items.length;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Returns the first item from the collection or null when collection is empty.
|
|
53
|
+
*/
|
|
54
|
+
get first() {
|
|
55
|
+
return this._items[0] || null;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Returns the last item from the collection or null when collection is empty.
|
|
59
|
+
*/
|
|
60
|
+
get last() {
|
|
61
|
+
return this._items[this.length - 1] || null;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Adds an item into the collection.
|
|
65
|
+
*
|
|
66
|
+
* If the item does not have an id, then it will be automatically generated and set on the item.
|
|
67
|
+
*
|
|
68
|
+
* @param item
|
|
69
|
+
* @param index The position of the item in the collection. The item
|
|
70
|
+
* is pushed to the collection when `index` not specified.
|
|
71
|
+
* @fires add
|
|
72
|
+
* @fires change
|
|
73
|
+
*/
|
|
74
|
+
add(item, index) {
|
|
75
|
+
return this.addMany([item], index);
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Adds multiple items into the collection.
|
|
79
|
+
*
|
|
80
|
+
* Any item not containing an id will get an automatically generated one.
|
|
81
|
+
*
|
|
82
|
+
* @param items
|
|
83
|
+
* @param index The position of the insertion. Items will be appended if no `index` is specified.
|
|
84
|
+
* @fires add
|
|
85
|
+
* @fires change
|
|
86
|
+
*/
|
|
87
|
+
addMany(items, index) {
|
|
88
|
+
if (index === undefined) {
|
|
89
|
+
index = this._items.length;
|
|
90
|
+
}
|
|
91
|
+
else if (index > this._items.length || index < 0) {
|
|
92
|
+
/**
|
|
93
|
+
* The `index` passed to {@link module:utils/collection~Collection#addMany `Collection#addMany()`}
|
|
94
|
+
* is invalid. It must be a number between 0 and the collection's length.
|
|
95
|
+
*
|
|
96
|
+
* @error collection-add-item-invalid-index
|
|
97
|
+
*/
|
|
98
|
+
throw new CKEditorError('collection-add-item-invalid-index', this);
|
|
99
|
+
}
|
|
100
|
+
let offset = 0;
|
|
101
|
+
for (const item of items) {
|
|
102
|
+
const itemId = this._getItemIdBeforeAdding(item);
|
|
103
|
+
const currentItemIndex = index + offset;
|
|
104
|
+
this._items.splice(currentItemIndex, 0, item);
|
|
105
|
+
this._itemMap.set(itemId, item);
|
|
106
|
+
this.fire('add', item, currentItemIndex);
|
|
107
|
+
offset++;
|
|
108
|
+
}
|
|
109
|
+
this.fire('change', {
|
|
110
|
+
added: items,
|
|
111
|
+
removed: [],
|
|
112
|
+
index
|
|
113
|
+
});
|
|
114
|
+
return this;
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Gets an item by its ID or index.
|
|
118
|
+
*
|
|
119
|
+
* @param idOrIndex The item ID or index in the collection.
|
|
120
|
+
* @returns The requested item or `null` if such item does not exist.
|
|
121
|
+
*/
|
|
122
|
+
get(idOrIndex) {
|
|
123
|
+
let item;
|
|
124
|
+
if (typeof idOrIndex == 'string') {
|
|
125
|
+
item = this._itemMap.get(idOrIndex);
|
|
126
|
+
}
|
|
127
|
+
else if (typeof idOrIndex == 'number') {
|
|
128
|
+
item = this._items[idOrIndex];
|
|
129
|
+
}
|
|
130
|
+
else {
|
|
131
|
+
/**
|
|
132
|
+
* An index or ID must be given.
|
|
133
|
+
*
|
|
134
|
+
* @error collection-get-invalid-arg
|
|
135
|
+
*/
|
|
136
|
+
throw new CKEditorError('collection-get-invalid-arg', this);
|
|
137
|
+
}
|
|
138
|
+
return item || null;
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Returns a Boolean indicating whether the collection contains an item.
|
|
142
|
+
*
|
|
143
|
+
* @param itemOrId The item or its ID in the collection.
|
|
144
|
+
* @returns `true` if the collection contains the item, `false` otherwise.
|
|
145
|
+
*/
|
|
146
|
+
has(itemOrId) {
|
|
147
|
+
if (typeof itemOrId == 'string') {
|
|
148
|
+
return this._itemMap.has(itemOrId);
|
|
149
|
+
}
|
|
150
|
+
else { // Object
|
|
151
|
+
const idProperty = this._idProperty;
|
|
152
|
+
const id = itemOrId[idProperty];
|
|
153
|
+
return id && this._itemMap.has(id);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Gets an index of an item in the collection.
|
|
158
|
+
* When an item is not defined in the collection, the index will equal -1.
|
|
159
|
+
*
|
|
160
|
+
* @param itemOrId The item or its ID in the collection.
|
|
161
|
+
* @returns The index of a given item.
|
|
162
|
+
*/
|
|
163
|
+
getIndex(itemOrId) {
|
|
164
|
+
let item;
|
|
165
|
+
if (typeof itemOrId == 'string') {
|
|
166
|
+
item = this._itemMap.get(itemOrId);
|
|
167
|
+
}
|
|
168
|
+
else {
|
|
169
|
+
item = itemOrId;
|
|
170
|
+
}
|
|
171
|
+
return item ? this._items.indexOf(item) : -1;
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Removes an item from the collection.
|
|
175
|
+
*
|
|
176
|
+
* @param subject The item to remove, its ID or index in the collection.
|
|
177
|
+
* @returns The removed item.
|
|
178
|
+
* @fires remove
|
|
179
|
+
* @fires change
|
|
180
|
+
*/
|
|
181
|
+
remove(subject) {
|
|
182
|
+
const [item, index] = this._remove(subject);
|
|
183
|
+
this.fire('change', {
|
|
184
|
+
added: [],
|
|
185
|
+
removed: [item],
|
|
186
|
+
index
|
|
187
|
+
});
|
|
188
|
+
return item;
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* Executes the callback for each item in the collection and composes an array or values returned by this callback.
|
|
192
|
+
*
|
|
193
|
+
* @typeParam U The result type of the callback.
|
|
194
|
+
* @param callback
|
|
195
|
+
* @param ctx Context in which the `callback` will be called.
|
|
196
|
+
* @returns The result of mapping.
|
|
197
|
+
*/
|
|
198
|
+
map(callback, ctx) {
|
|
199
|
+
return this._items.map(callback, ctx);
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* Performs the specified action for each item in the collection.
|
|
203
|
+
*
|
|
204
|
+
* @param ctx Context in which the `callback` will be called.
|
|
205
|
+
*/
|
|
206
|
+
forEach(callback, ctx) {
|
|
207
|
+
this._items.forEach(callback, ctx);
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* Finds the first item in the collection for which the `callback` returns a true value.
|
|
211
|
+
*
|
|
212
|
+
* @param callback
|
|
213
|
+
* @param ctx Context in which the `callback` will be called.
|
|
214
|
+
* @returns The item for which `callback` returned a true value.
|
|
215
|
+
*/
|
|
216
|
+
find(callback, ctx) {
|
|
217
|
+
return this._items.find(callback, ctx);
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* Returns an array with items for which the `callback` returned a true value.
|
|
221
|
+
*
|
|
222
|
+
* @param callback
|
|
223
|
+
* @param ctx Context in which the `callback` will be called.
|
|
224
|
+
* @returns The array with matching items.
|
|
225
|
+
*/
|
|
226
|
+
filter(callback, ctx) {
|
|
227
|
+
return this._items.filter(callback, ctx);
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* Removes all items from the collection and destroys the binding created using
|
|
231
|
+
* {@link #bindTo}.
|
|
232
|
+
*
|
|
233
|
+
* @fires remove
|
|
234
|
+
* @fires change
|
|
235
|
+
*/
|
|
236
|
+
clear() {
|
|
237
|
+
if (this._bindToCollection) {
|
|
238
|
+
this.stopListening(this._bindToCollection);
|
|
239
|
+
this._bindToCollection = null;
|
|
240
|
+
}
|
|
241
|
+
const removedItems = Array.from(this._items);
|
|
242
|
+
while (this.length) {
|
|
243
|
+
this._remove(0);
|
|
244
|
+
}
|
|
245
|
+
this.fire('change', {
|
|
246
|
+
added: [],
|
|
247
|
+
removed: removedItems,
|
|
248
|
+
index: 0
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
/**
|
|
252
|
+
* Binds and synchronizes the collection with another one.
|
|
253
|
+
*
|
|
254
|
+
* The binding can be a simple factory:
|
|
255
|
+
*
|
|
256
|
+
* ```ts
|
|
257
|
+
* class FactoryClass {
|
|
258
|
+
* public label: string;
|
|
259
|
+
*
|
|
260
|
+
* constructor( data: { label: string } ) {
|
|
261
|
+
* this.label = data.label;
|
|
262
|
+
* }
|
|
263
|
+
* }
|
|
264
|
+
*
|
|
265
|
+
* const source = new Collection<{ label: string }>( { idProperty: 'label' } );
|
|
266
|
+
* const target = new Collection<FactoryClass>();
|
|
267
|
+
*
|
|
268
|
+
* target.bindTo( source ).as( FactoryClass );
|
|
269
|
+
*
|
|
270
|
+
* source.add( { label: 'foo' } );
|
|
271
|
+
* source.add( { label: 'bar' } );
|
|
272
|
+
*
|
|
273
|
+
* console.log( target.length ); // 2
|
|
274
|
+
* console.log( target.get( 1 ).label ); // 'bar'
|
|
275
|
+
*
|
|
276
|
+
* source.remove( 0 );
|
|
277
|
+
* console.log( target.length ); // 1
|
|
278
|
+
* console.log( target.get( 0 ).label ); // 'bar'
|
|
279
|
+
* ```
|
|
280
|
+
*
|
|
281
|
+
* or the factory driven by a custom callback:
|
|
282
|
+
*
|
|
283
|
+
* ```ts
|
|
284
|
+
* class FooClass {
|
|
285
|
+
* public label: string;
|
|
286
|
+
*
|
|
287
|
+
* constructor( data: { label: string } ) {
|
|
288
|
+
* this.label = data.label;
|
|
289
|
+
* }
|
|
290
|
+
* }
|
|
291
|
+
*
|
|
292
|
+
* class BarClass {
|
|
293
|
+
* public label: string;
|
|
294
|
+
*
|
|
295
|
+
* constructor( data: { label: string } ) {
|
|
296
|
+
* this.label = data.label;
|
|
297
|
+
* }
|
|
298
|
+
* }
|
|
299
|
+
*
|
|
300
|
+
* const source = new Collection<{ label: string }>( { idProperty: 'label' } );
|
|
301
|
+
* const target = new Collection<FooClass | BarClass>();
|
|
302
|
+
*
|
|
303
|
+
* target.bindTo( source ).using( ( item ) => {
|
|
304
|
+
* if ( item.label == 'foo' ) {
|
|
305
|
+
* return new FooClass( item );
|
|
306
|
+
* } else {
|
|
307
|
+
* return new BarClass( item );
|
|
308
|
+
* }
|
|
309
|
+
* } );
|
|
310
|
+
*
|
|
311
|
+
* source.add( { label: 'foo' } );
|
|
312
|
+
* source.add( { label: 'bar' } );
|
|
313
|
+
*
|
|
314
|
+
* console.log( target.length ); // 2
|
|
315
|
+
* console.log( target.get( 0 ) instanceof FooClass ); // true
|
|
316
|
+
* console.log( target.get( 1 ) instanceof BarClass ); // true
|
|
317
|
+
* ```
|
|
318
|
+
*
|
|
319
|
+
* or the factory out of property name:
|
|
320
|
+
*
|
|
321
|
+
* ```ts
|
|
322
|
+
* const source = new Collection<{ nested: { value: string } }>();
|
|
323
|
+
* const target = new Collection<{ value: string }>();
|
|
324
|
+
*
|
|
325
|
+
* target.bindTo( source ).using( 'nested' );
|
|
326
|
+
*
|
|
327
|
+
* source.add( { nested: { value: 'foo' } } );
|
|
328
|
+
* source.add( { nested: { value: 'bar' } } );
|
|
329
|
+
*
|
|
330
|
+
* console.log( target.length ); // 2
|
|
331
|
+
* console.log( target.get( 0 ).value ); // 'foo'
|
|
332
|
+
* console.log( target.get( 1 ).value ); // 'bar'
|
|
333
|
+
* ```
|
|
334
|
+
*
|
|
335
|
+
* It's possible to skip specified items by returning null value:
|
|
336
|
+
*
|
|
337
|
+
* ```ts
|
|
338
|
+
* const source = new Collection<{ hidden: boolean }>();
|
|
339
|
+
* const target = new Collection<{ hidden: boolean }>();
|
|
340
|
+
*
|
|
341
|
+
* target.bindTo( source ).using( item => {
|
|
342
|
+
* if ( item.hidden ) {
|
|
343
|
+
* return null;
|
|
344
|
+
* }
|
|
345
|
+
*
|
|
346
|
+
* return item;
|
|
347
|
+
* } );
|
|
348
|
+
*
|
|
349
|
+
* source.add( { hidden: true } );
|
|
350
|
+
* source.add( { hidden: false } );
|
|
351
|
+
*
|
|
352
|
+
* console.log( source.length ); // 2
|
|
353
|
+
* console.log( target.length ); // 1
|
|
354
|
+
* ```
|
|
355
|
+
*
|
|
356
|
+
* **Note**: {@link #clear} can be used to break the binding.
|
|
357
|
+
*
|
|
358
|
+
* @typeParam S The type of `externalCollection` element.
|
|
359
|
+
* @param externalCollection A collection to be bound.
|
|
360
|
+
* @returns The binding chain object.
|
|
361
|
+
*/
|
|
362
|
+
bindTo(externalCollection) {
|
|
363
|
+
if (this._bindToCollection) {
|
|
364
|
+
/**
|
|
365
|
+
* The collection cannot be bound more than once.
|
|
366
|
+
*
|
|
367
|
+
* @error collection-bind-to-rebind
|
|
368
|
+
*/
|
|
369
|
+
throw new CKEditorError('collection-bind-to-rebind', this);
|
|
370
|
+
}
|
|
371
|
+
this._bindToCollection = externalCollection;
|
|
372
|
+
return {
|
|
373
|
+
as: Class => {
|
|
374
|
+
this._setUpBindToBinding(item => new Class(item));
|
|
375
|
+
},
|
|
376
|
+
using: callbackOrProperty => {
|
|
377
|
+
if (typeof callbackOrProperty == 'function') {
|
|
378
|
+
this._setUpBindToBinding(callbackOrProperty);
|
|
379
|
+
}
|
|
380
|
+
else {
|
|
381
|
+
this._setUpBindToBinding(item => item[callbackOrProperty]);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
/**
|
|
387
|
+
* Finalizes and activates a binding initiated by {@link #bindTo}.
|
|
388
|
+
*
|
|
389
|
+
* @param factory A function which produces collection items.
|
|
390
|
+
*/
|
|
391
|
+
_setUpBindToBinding(factory) {
|
|
392
|
+
const externalCollection = this._bindToCollection;
|
|
393
|
+
// Adds the item to the collection once a change has been done to the external collection.
|
|
394
|
+
const addItem = (evt, externalItem, index) => {
|
|
395
|
+
const isExternalBoundToThis = externalCollection._bindToCollection == this;
|
|
396
|
+
const externalItemBound = externalCollection._bindToInternalToExternalMap.get(externalItem);
|
|
397
|
+
// If an external collection is bound to this collection, which makes it a 2–way binding,
|
|
398
|
+
// and the particular external collection item is already bound, don't add it here.
|
|
399
|
+
// The external item has been created **out of this collection's item** and (re)adding it will
|
|
400
|
+
// cause a loop.
|
|
401
|
+
if (isExternalBoundToThis && externalItemBound) {
|
|
402
|
+
this._bindToExternalToInternalMap.set(externalItem, externalItemBound);
|
|
403
|
+
this._bindToInternalToExternalMap.set(externalItemBound, externalItem);
|
|
404
|
+
}
|
|
405
|
+
else {
|
|
406
|
+
const item = factory(externalItem);
|
|
407
|
+
// When there is no item we need to remember skipped index first and then we can skip this item.
|
|
408
|
+
if (!item) {
|
|
409
|
+
this._skippedIndexesFromExternal.push(index);
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
412
|
+
// Lets try to put item at the same index as index in external collection
|
|
413
|
+
// but when there are a skipped items in one or both collections we need to recalculate this index.
|
|
414
|
+
let finalIndex = index;
|
|
415
|
+
// When we try to insert item after some skipped items from external collection we need
|
|
416
|
+
// to include this skipped items and decrease index.
|
|
417
|
+
//
|
|
418
|
+
// For the following example:
|
|
419
|
+
// external -> [ 'A', 'B - skipped for internal', 'C - skipped for internal' ]
|
|
420
|
+
// internal -> [ A ]
|
|
421
|
+
//
|
|
422
|
+
// Another item is been added at the end of external collection:
|
|
423
|
+
// external.add( 'D' )
|
|
424
|
+
// external -> [ 'A', 'B - skipped for internal', 'C - skipped for internal', 'D' ]
|
|
425
|
+
//
|
|
426
|
+
// We can't just add 'D' to internal at the same index as index in external because
|
|
427
|
+
// this will produce empty indexes what is invalid:
|
|
428
|
+
// internal -> [ 'A', empty, empty, 'D' ]
|
|
429
|
+
//
|
|
430
|
+
// So we need to include skipped items and decrease index
|
|
431
|
+
// internal -> [ 'A', 'D' ]
|
|
432
|
+
for (const skipped of this._skippedIndexesFromExternal) {
|
|
433
|
+
if (index > skipped) {
|
|
434
|
+
finalIndex--;
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
// We need to take into consideration that external collection could skip some items from
|
|
438
|
+
// internal collection.
|
|
439
|
+
//
|
|
440
|
+
// For the following example:
|
|
441
|
+
// internal -> [ 'A', 'B - skipped for external', 'C - skipped for external' ]
|
|
442
|
+
// external -> [ A ]
|
|
443
|
+
//
|
|
444
|
+
// Another item is been added at the end of external collection:
|
|
445
|
+
// external.add( 'D' )
|
|
446
|
+
// external -> [ 'A', 'D' ]
|
|
447
|
+
//
|
|
448
|
+
// We need to include skipped items and place new item after them:
|
|
449
|
+
// internal -> [ 'A', 'B - skipped for external', 'C - skipped for external', 'D' ]
|
|
450
|
+
for (const skipped of externalCollection._skippedIndexesFromExternal) {
|
|
451
|
+
if (finalIndex >= skipped) {
|
|
452
|
+
finalIndex++;
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
this._bindToExternalToInternalMap.set(externalItem, item);
|
|
456
|
+
this._bindToInternalToExternalMap.set(item, externalItem);
|
|
457
|
+
this.add(item, finalIndex);
|
|
458
|
+
// After adding new element to internal collection we need update indexes
|
|
459
|
+
// of skipped items in external collection.
|
|
460
|
+
for (let i = 0; i < externalCollection._skippedIndexesFromExternal.length; i++) {
|
|
461
|
+
if (finalIndex <= externalCollection._skippedIndexesFromExternal[i]) {
|
|
462
|
+
externalCollection._skippedIndexesFromExternal[i]++;
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
};
|
|
467
|
+
// Load the initial content of the collection.
|
|
468
|
+
for (const externalItem of externalCollection) {
|
|
469
|
+
addItem(null, externalItem, externalCollection.getIndex(externalItem));
|
|
470
|
+
}
|
|
471
|
+
// Synchronize the with collection as new items are added.
|
|
472
|
+
this.listenTo(externalCollection, 'add', addItem);
|
|
473
|
+
// Synchronize the with collection as new items are removed.
|
|
474
|
+
this.listenTo(externalCollection, 'remove', (evt, externalItem, index) => {
|
|
475
|
+
const item = this._bindToExternalToInternalMap.get(externalItem);
|
|
476
|
+
if (item) {
|
|
477
|
+
this.remove(item);
|
|
478
|
+
}
|
|
479
|
+
// After removing element from external collection we need update/remove indexes
|
|
480
|
+
// of skipped items in internal collection.
|
|
481
|
+
this._skippedIndexesFromExternal = this._skippedIndexesFromExternal.reduce((result, skipped) => {
|
|
482
|
+
if (index < skipped) {
|
|
483
|
+
result.push(skipped - 1);
|
|
484
|
+
}
|
|
485
|
+
if (index > skipped) {
|
|
486
|
+
result.push(skipped);
|
|
487
|
+
}
|
|
488
|
+
return result;
|
|
489
|
+
}, []);
|
|
490
|
+
});
|
|
491
|
+
}
|
|
492
|
+
/**
|
|
493
|
+
* Returns an unique id property for a given `item`.
|
|
494
|
+
*
|
|
495
|
+
* The method will generate new id and assign it to the `item` if it doesn't have any.
|
|
496
|
+
*
|
|
497
|
+
* @param item Item to be added.
|
|
498
|
+
*/
|
|
499
|
+
_getItemIdBeforeAdding(item) {
|
|
500
|
+
const idProperty = this._idProperty;
|
|
501
|
+
let itemId;
|
|
502
|
+
if ((idProperty in item)) {
|
|
503
|
+
itemId = item[idProperty];
|
|
504
|
+
if (typeof itemId != 'string') {
|
|
505
|
+
/**
|
|
506
|
+
* This item's ID should be a string.
|
|
507
|
+
*
|
|
508
|
+
* @error collection-add-invalid-id
|
|
509
|
+
*/
|
|
510
|
+
throw new CKEditorError('collection-add-invalid-id', this);
|
|
511
|
+
}
|
|
512
|
+
if (this.get(itemId)) {
|
|
513
|
+
/**
|
|
514
|
+
* This item already exists in the collection.
|
|
515
|
+
*
|
|
516
|
+
* @error collection-add-item-already-exists
|
|
517
|
+
*/
|
|
518
|
+
throw new CKEditorError('collection-add-item-already-exists', this);
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
else {
|
|
522
|
+
item[idProperty] = itemId = uid();
|
|
523
|
+
}
|
|
524
|
+
return itemId;
|
|
525
|
+
}
|
|
526
|
+
/**
|
|
527
|
+
* Core {@link #remove} method implementation shared in other functions.
|
|
528
|
+
*
|
|
529
|
+
* In contrast this method **does not** fire the {@link #event:change} event.
|
|
530
|
+
*
|
|
531
|
+
* @param subject The item to remove, its id or index in the collection.
|
|
532
|
+
* @returns Returns an array with the removed item and its index.
|
|
533
|
+
* @fires remove
|
|
534
|
+
*/
|
|
535
|
+
_remove(subject) {
|
|
536
|
+
let index, id, item;
|
|
537
|
+
let itemDoesNotExist = false;
|
|
538
|
+
const idProperty = this._idProperty;
|
|
539
|
+
if (typeof subject == 'string') {
|
|
540
|
+
id = subject;
|
|
541
|
+
item = this._itemMap.get(id);
|
|
542
|
+
itemDoesNotExist = !item;
|
|
543
|
+
if (item) {
|
|
544
|
+
index = this._items.indexOf(item);
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
else if (typeof subject == 'number') {
|
|
548
|
+
index = subject;
|
|
549
|
+
item = this._items[index];
|
|
550
|
+
itemDoesNotExist = !item;
|
|
551
|
+
if (item) {
|
|
552
|
+
id = item[idProperty];
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
else {
|
|
556
|
+
item = subject;
|
|
557
|
+
id = item[idProperty];
|
|
558
|
+
index = this._items.indexOf(item);
|
|
559
|
+
itemDoesNotExist = (index == -1 || !this._itemMap.get(id));
|
|
560
|
+
}
|
|
561
|
+
if (itemDoesNotExist) {
|
|
562
|
+
/**
|
|
563
|
+
* Item not found.
|
|
564
|
+
*
|
|
565
|
+
* @error collection-remove-404
|
|
566
|
+
*/
|
|
567
|
+
throw new CKEditorError('collection-remove-404', this);
|
|
568
|
+
}
|
|
569
|
+
this._items.splice(index, 1);
|
|
570
|
+
this._itemMap.delete(id);
|
|
571
|
+
const externalItem = this._bindToInternalToExternalMap.get(item);
|
|
572
|
+
this._bindToInternalToExternalMap.delete(item);
|
|
573
|
+
this._bindToExternalToInternalMap.delete(externalItem);
|
|
574
|
+
this.fire('remove', item, index);
|
|
575
|
+
return [item, index];
|
|
576
|
+
}
|
|
577
|
+
/**
|
|
578
|
+
* Iterable interface.
|
|
579
|
+
*/
|
|
580
|
+
[Symbol.iterator]() {
|
|
581
|
+
return this._items[Symbol.iterator]();
|
|
582
|
+
}
|
|
583
|
+
}
|