@converse/skeletor 0.0.8 → 1.0.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/README.md +21 -92
- package/dist/skeletor.d.ts +764 -0
- package/dist/skeletor.js +10206 -6249
- package/dist/skeletor.min.js +72 -6
- package/package.json +59 -33
- package/src/collection.ts +846 -0
- package/src/drivers/sessionStorage.ts +297 -0
- package/src/eventemitter.ts +172 -0
- package/src/helpers.ts +179 -0
- package/src/index.ts +84 -0
- package/src/listening.ts +86 -0
- package/src/model.ts +520 -0
- package/src/storage.ts +293 -0
- package/src/types.ts +189 -0
- package/src/utils/events.ts +224 -0
- package/dist/skeletor.js.map +0 -1
- package/dist/skeletor.min.js.map +0 -1
- package/src/collection.js +0 -740
- package/src/drivers/sessionStorage.js +0 -306
- package/src/drivers/sessionStorage.js~ +0 -307
- package/src/element.js +0 -188
- package/src/events.js +0 -328
- package/src/helpers.js +0 -167
- package/src/history.js +0 -301
- package/src/main.js +0 -43
- package/src/model.js +0 -437
- package/src/overview.js +0 -191
- package/src/router.js +0 -119
- package/src/storage.js +0 -258
- package/src/view.js +0 -267
|
@@ -0,0 +1,846 @@
|
|
|
1
|
+
import clone from 'lodash-es/clone';
|
|
2
|
+
import countBy from 'lodash-es/countBy';
|
|
3
|
+
import groupBy from 'lodash-es/groupBy';
|
|
4
|
+
import isFunction from 'lodash-es/isFunction';
|
|
5
|
+
import isString from 'lodash-es/isString';
|
|
6
|
+
import keyBy from 'lodash-es/keyBy';
|
|
7
|
+
import sortBy from 'lodash-es/sortBy';
|
|
8
|
+
import {EventEmitterObject} from './eventemitter';
|
|
9
|
+
import type Storage from './storage';
|
|
10
|
+
import {getResolveablePromise, getSyncMethod, wrapError} from './helpers';
|
|
11
|
+
import {Model} from './model';
|
|
12
|
+
import {
|
|
13
|
+
CollectionOptions,
|
|
14
|
+
Comparator,
|
|
15
|
+
ModelAttributes,
|
|
16
|
+
ModelOptions,
|
|
17
|
+
ObjectWithId,
|
|
18
|
+
Options,
|
|
19
|
+
SyncOperation,
|
|
20
|
+
} from './types';
|
|
21
|
+
|
|
22
|
+
// Default options for `Collection#set`.
|
|
23
|
+
const setOptions = {add: true, remove: true, merge: true};
|
|
24
|
+
const addOptions = {add: true, remove: false};
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* @public
|
|
28
|
+
* If models tend to represent a single row of data, a Collection is
|
|
29
|
+
* more analogous to a table full of data ... or a small slice or page of that
|
|
30
|
+
* table, or a collection of rows that belong together for a particular reason
|
|
31
|
+
* -- all of the messages in this particular folder, all of the documents
|
|
32
|
+
* belonging to this particular author, and so on. Collections maintain
|
|
33
|
+
* indexes of their models, both in order, and for lookup by `id`.
|
|
34
|
+
*/
|
|
35
|
+
export class Collection<T extends Model = Model> extends EventEmitterObject {
|
|
36
|
+
[key: symbol]: () => CollectionIterator<T>;
|
|
37
|
+
_browserStorage?: Storage;
|
|
38
|
+
_comparator?: Comparator<T>;
|
|
39
|
+
_url: string = '';
|
|
40
|
+
models: T[];
|
|
41
|
+
protected _byId: Record<string, T>;
|
|
42
|
+
protected _model?: new (attributes?: Partial<ModelAttributes>, options?: ModelOptions) => T;
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Create a new **Collection**, perhaps to contain a specific type of `model`.
|
|
46
|
+
* If a `comparator` is specified, the Collection will maintain
|
|
47
|
+
* its models in sort order, as they're added and removed.
|
|
48
|
+
*/
|
|
49
|
+
constructor(models?: T[] | ModelAttributes[] | T | ModelAttributes, options?: CollectionOptions<T>) {
|
|
50
|
+
super();
|
|
51
|
+
options = options || {};
|
|
52
|
+
this.preinitialize.apply(this, arguments as any);
|
|
53
|
+
if (options.model) this._model = options.model;
|
|
54
|
+
if (options.comparator !== undefined) this.comparator = options.comparator;
|
|
55
|
+
this._reset();
|
|
56
|
+
this.initialize.apply(this, arguments as any);
|
|
57
|
+
if (models) this.reset(models, Object.assign({silent: true}, options));
|
|
58
|
+
|
|
59
|
+
this[Symbol.iterator] = this.values;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
get comparator(): Comparator<T> {
|
|
63
|
+
return this._comparator;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
set comparator(c: Comparator<T>) {
|
|
67
|
+
this._comparator = c;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
set browserStorage(storage: Storage) {
|
|
71
|
+
this._browserStorage = storage;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
get browserStorage(): Storage | undefined {
|
|
75
|
+
return this._browserStorage;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* The default model for a collection is just a **Model**.
|
|
80
|
+
* This should be overridden in most cases.
|
|
81
|
+
*/
|
|
82
|
+
get model(): new (attributes?: Partial<ModelAttributes>, options?: ModelOptions) => T | Model {
|
|
83
|
+
return this._model ?? Model;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
set model(model: new (attributes?: Partial<ModelAttributes>, options?: ModelOptions) => T) {
|
|
87
|
+
this._model = model;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
get length(): number {
|
|
91
|
+
return this.models.length;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
get url(): string {
|
|
95
|
+
return this._url;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
set url(url: string) {
|
|
99
|
+
this._url = url;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* preinitialize is an empty function by default. You can override it with a function
|
|
104
|
+
* or object. preinitialize will run before any instantiation logic is run in the Collection.
|
|
105
|
+
*/
|
|
106
|
+
preinitialize(..._args: any[]): void {}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Initialize is an empty function by default. Override it with your own
|
|
110
|
+
* initialization logic.
|
|
111
|
+
*/
|
|
112
|
+
initialize(..._args: any[]): void {}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* The JSON representation of a Collection is an array of the
|
|
116
|
+
* models' attributes.
|
|
117
|
+
*/
|
|
118
|
+
toJSON(): any[] {
|
|
119
|
+
return this.map(function (model) {
|
|
120
|
+
return model.toJSON();
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
sync(method: SyncOperation, model: Model | Collection<any>, options?: Options): any {
|
|
125
|
+
return getSyncMethod(this)(method, model, options);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Add a model, or list of models to the set. `models` may be
|
|
130
|
+
* Models or raw JavaScript objects to be converted to Models, or any
|
|
131
|
+
* combination of the two.
|
|
132
|
+
*/
|
|
133
|
+
add(models: T[] | T | ModelAttributes | ModelAttributes[], options?: Options): T | T[] {
|
|
134
|
+
return this.set(models, Object.assign({merge: false}, options, addOptions));
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Remove a model, or a list of models from the set.
|
|
139
|
+
*/
|
|
140
|
+
remove(models: T | ObjectWithId | (T | ObjectWithId)[], options?: Options): T | T[] {
|
|
141
|
+
options = Object.assign({}, options);
|
|
142
|
+
const singular = !Array.isArray(models);
|
|
143
|
+
const modelsArray = singular ? [models] : (models as T[]).slice();
|
|
144
|
+
const removed = this._removeModels(modelsArray, options);
|
|
145
|
+
if (!options.silent && removed.length) {
|
|
146
|
+
options.changes = {added: [], merged: [], removed: removed};
|
|
147
|
+
this.trigger('update', this, options);
|
|
148
|
+
}
|
|
149
|
+
return singular ? removed[0] : removed;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Update a collection by `set`-ing a new list of models, adding new ones,
|
|
154
|
+
* removing models that are no longer present, and merging models that
|
|
155
|
+
* already exist in the collection, as necessary. Similar to **Model#set**,
|
|
156
|
+
* the core operation for updating the data contained by the collection.
|
|
157
|
+
*/
|
|
158
|
+
set(models: T[] | T | ModelAttributes | ModelAttributes[], options?: Options): T | T[] {
|
|
159
|
+
if (models == null) return;
|
|
160
|
+
|
|
161
|
+
options = Object.assign({}, setOptions, options);
|
|
162
|
+
if (options.parse && !this._isModel(models)) {
|
|
163
|
+
models = this.parse(models, options) || [];
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const singular = !Array.isArray(models);
|
|
167
|
+
models = singular ? [models] : (models as T[] | ModelAttributes[]).slice();
|
|
168
|
+
|
|
169
|
+
let at = options.at;
|
|
170
|
+
if (at != null) at = +at;
|
|
171
|
+
if (at > this.length) at = this.length;
|
|
172
|
+
if (at < 0) at += this.length + 1;
|
|
173
|
+
|
|
174
|
+
const set = [];
|
|
175
|
+
const toAdd = [];
|
|
176
|
+
const toMerge = [];
|
|
177
|
+
const toRemove = [];
|
|
178
|
+
const modelMap = {};
|
|
179
|
+
|
|
180
|
+
const add = options.add;
|
|
181
|
+
const merge = options.merge;
|
|
182
|
+
const remove = options.remove;
|
|
183
|
+
|
|
184
|
+
let sort = false;
|
|
185
|
+
const sortable = this.comparator && at == null && options.sort !== false;
|
|
186
|
+
const sortAttr = isString(this.comparator) ? this.comparator : null;
|
|
187
|
+
|
|
188
|
+
// Turn bare objects into model references, and prevent invalid models
|
|
189
|
+
// from being added.
|
|
190
|
+
let model: T, i: number;
|
|
191
|
+
for (i = 0; i < models.length; i++) {
|
|
192
|
+
model = models[i];
|
|
193
|
+
|
|
194
|
+
// If a duplicate is found, prevent it from being added and
|
|
195
|
+
// optionally merge it into the existing model.
|
|
196
|
+
const existing = this.get(model);
|
|
197
|
+
if (existing) {
|
|
198
|
+
if (merge && model !== existing) {
|
|
199
|
+
let attrs = this._isModel(model) ? model.attributes : model;
|
|
200
|
+
if (options.parse) attrs = existing.parse(attrs, options) as Partial<ModelAttributes>;
|
|
201
|
+
existing.set(attrs, options);
|
|
202
|
+
toMerge.push(existing);
|
|
203
|
+
|
|
204
|
+
if (sortable && !sort) sort = existing.hasChanged(sortAttr as string);
|
|
205
|
+
}
|
|
206
|
+
if (!modelMap[existing.cid]) {
|
|
207
|
+
modelMap[existing.cid] = true;
|
|
208
|
+
set.push(existing);
|
|
209
|
+
}
|
|
210
|
+
models[i] = existing;
|
|
211
|
+
|
|
212
|
+
// If this is a new, valid model, push it to the `toAdd` list.
|
|
213
|
+
} else if (add) {
|
|
214
|
+
model = models[i] = this._prepareModel(model, options);
|
|
215
|
+
if (model) {
|
|
216
|
+
toAdd.push(model);
|
|
217
|
+
this._addReference(model, options);
|
|
218
|
+
modelMap[model.cid] = true;
|
|
219
|
+
set.push(model);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Remove stale models.
|
|
225
|
+
if (remove) {
|
|
226
|
+
for (i = 0; i < this.length; i++) {
|
|
227
|
+
model = this.models[i];
|
|
228
|
+
if (!modelMap[model.cid]) toRemove.push(model);
|
|
229
|
+
}
|
|
230
|
+
if (toRemove.length) this._removeModels(toRemove, options);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// See if sorting is needed, update `length` and splice in new models.
|
|
234
|
+
let orderChanged = false;
|
|
235
|
+
const replace = !sortable && add && remove;
|
|
236
|
+
|
|
237
|
+
if (set.length && replace) {
|
|
238
|
+
orderChanged = this.length !== set.length || this.models.some((m, idx) => m !== set[idx]);
|
|
239
|
+
this.models.length = 0;
|
|
240
|
+
this.models.splice(0, 0, ...set);
|
|
241
|
+
} else if (toAdd.length) {
|
|
242
|
+
if (sortable) sort = true;
|
|
243
|
+
let idx = at == null ? this.length : at;
|
|
244
|
+
idx = Math.min(Math.max(idx, 0), this.models.length);
|
|
245
|
+
this.models.splice(idx, 0, ...toAdd);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Silently sort the collection if appropriate.
|
|
249
|
+
if (sort) this.sort({silent: true});
|
|
250
|
+
|
|
251
|
+
// Unless silenced, it's time to fire all appropriate add/sort/update events.
|
|
252
|
+
if (!options.silent) {
|
|
253
|
+
for (i = 0; i < toAdd.length; i++) {
|
|
254
|
+
if (at != null) options.index = at + i;
|
|
255
|
+
model = toAdd[i];
|
|
256
|
+
model.trigger('add', model, this, options);
|
|
257
|
+
}
|
|
258
|
+
if (sort || orderChanged) this.trigger('sort', this, options);
|
|
259
|
+
if (toAdd.length || toRemove.length || toMerge.length) {
|
|
260
|
+
options.changes = {
|
|
261
|
+
added: toAdd,
|
|
262
|
+
removed: toRemove,
|
|
263
|
+
merged: toMerge,
|
|
264
|
+
};
|
|
265
|
+
this.trigger('update', this, options);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// Return the added (or merged) model (or models).
|
|
270
|
+
return singular ? models[0] : (models as T);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
async clearStore(options: Options = {}, filter: (model: T) => boolean = () => true): Promise<void> {
|
|
274
|
+
await Promise.all(
|
|
275
|
+
this.models.filter(filter).map((m) => {
|
|
276
|
+
return new Promise<void>((resolve) => {
|
|
277
|
+
m.destroy(
|
|
278
|
+
Object.assign(options, {
|
|
279
|
+
'success': resolve,
|
|
280
|
+
'error': (_m: T, e: any) => {
|
|
281
|
+
console.error(e);
|
|
282
|
+
resolve();
|
|
283
|
+
},
|
|
284
|
+
})
|
|
285
|
+
);
|
|
286
|
+
});
|
|
287
|
+
})
|
|
288
|
+
);
|
|
289
|
+
await this.browserStorage?.clear();
|
|
290
|
+
this.reset();
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* When you have more items than you want to add or remove individually,
|
|
295
|
+
* you can reset the entire set with a new list of models, without firing
|
|
296
|
+
* any granular `add` or `remove` events. Fires `reset` when finished.
|
|
297
|
+
* Useful for bulk operations and optimizations.
|
|
298
|
+
*/
|
|
299
|
+
reset(models?: T[] | T | ModelAttributes | ModelAttributes[], options?: Options): T | T[] {
|
|
300
|
+
options = options ? clone(options) : {};
|
|
301
|
+
for (let i = 0; i < this.models.length; i++) {
|
|
302
|
+
this._removeReference(this.models[i], options);
|
|
303
|
+
}
|
|
304
|
+
options.previousModels = this.models;
|
|
305
|
+
this._reset();
|
|
306
|
+
models = this.add(models, Object.assign({silent: true}, options));
|
|
307
|
+
if (!options.silent) this.trigger('reset', this, options);
|
|
308
|
+
return models as T | T[];
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Add a model to the end of the collection.
|
|
313
|
+
*/
|
|
314
|
+
push(model: T | ModelAttributes, options?: Options): T {
|
|
315
|
+
return this.add(model, Object.assign({at: this.length}, options)) as T;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Remove a model from the end of the collection.
|
|
320
|
+
*/
|
|
321
|
+
pop(options?: Options): T | undefined {
|
|
322
|
+
const model = this.at(this.length - 1);
|
|
323
|
+
return this.remove(model, options) as T | undefined;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Add a model to the beginning of the collection.
|
|
328
|
+
*/
|
|
329
|
+
unshift(model: T | ModelAttributes, options?: Options): T {
|
|
330
|
+
return this.add(model, Object.assign({at: 0}, options)) as T;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* Remove a model from the beginning of the collection.
|
|
335
|
+
*/
|
|
336
|
+
shift(options?: Options): T | undefined {
|
|
337
|
+
const model = this.at(0);
|
|
338
|
+
return this.remove(model, options) as T | undefined;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/** Slice out a sub-array of models from the collection. */
|
|
342
|
+
slice(start?: number, end?: number): T[] {
|
|
343
|
+
return this.models.slice(start, end);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
filter(callback: ((model: T) => boolean) | string | Partial<ModelAttributes>, thisArg?: any): T[] {
|
|
347
|
+
return this.models.filter(
|
|
348
|
+
isFunction(callback)
|
|
349
|
+
? (callback as (model: T) => boolean)
|
|
350
|
+
: (m) => m.matches(callback as Partial<ModelAttributes>),
|
|
351
|
+
thisArg
|
|
352
|
+
);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
every(pred: ((attrs: ModelAttributes) => boolean) | Options): boolean {
|
|
356
|
+
if (isFunction(pred)) {
|
|
357
|
+
return this.models.map((m) => m.attributes).every(pred as (attrs: ModelAttributes) => boolean);
|
|
358
|
+
} else {
|
|
359
|
+
return this.models.every((m) => m.matches(pred));
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
difference(values: T[]): T[] {
|
|
364
|
+
return this.models.filter((m) => !values.includes(m));
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
max(): number {
|
|
368
|
+
return Math.max.apply(Math, this.models as any);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
min(): number {
|
|
372
|
+
return Math.min.apply(Math, this.models as any);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
drop(n: number = 1): T[] {
|
|
376
|
+
return this.models.slice(n);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
some(pred: ((attrs: ModelAttributes) => boolean) | Options): boolean {
|
|
380
|
+
if (isFunction(pred)) {
|
|
381
|
+
return this.models.map((m) => m.attributes).some(pred as (attrs: ModelAttributes) => boolean);
|
|
382
|
+
} else {
|
|
383
|
+
return this.models.some((m) => m.matches(pred));
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
sortBy(iteratee: string | ((model: T) => any)): T[] {
|
|
388
|
+
return sortBy(
|
|
389
|
+
this.models,
|
|
390
|
+
isFunction(iteratee)
|
|
391
|
+
? iteratee
|
|
392
|
+
: (m: T) => (isString(iteratee) ? m.get(iteratee as string) : m.matches(iteratee as Partial<ModelAttributes>))
|
|
393
|
+
);
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
isEmpty(): boolean {
|
|
397
|
+
return !this.models.length;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
keyBy(iteratee: string | ((model: T) => string)): Record<string, T> {
|
|
401
|
+
return keyBy(this.models, iteratee);
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
each(callback: (model: T, index: number, array: T[]) => void, thisArg?: any): void {
|
|
405
|
+
return this.forEach(callback, thisArg);
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
forEach(callback: (model: T, index: number, array: T[]) => void, thisArg?: any): void {
|
|
409
|
+
return this.models.forEach(callback, thisArg);
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
includes(item: T): boolean {
|
|
413
|
+
return this.models.includes(item);
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
size(): number {
|
|
417
|
+
return this.models.length;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
countBy(f: string | ((model: T) => string) | Partial<ModelAttributes>): Record<string, number> {
|
|
421
|
+
return countBy(this.models, isFunction(f) ? f : (m) => (isString(f) ? m.get(f) : m.matches(f)));
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
groupBy(pred: string | ((model: T) => string | number)): Record<string, T[]> {
|
|
425
|
+
return groupBy(this.models, isFunction(pred) ? pred : (m) => (isString(pred) ? m.get(pred) : m.matches(pred)));
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
indexOf(model: T, fromIndex?: number): number {
|
|
429
|
+
return this.models.indexOf(model, fromIndex);
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
findLastIndex(pred: ((model: T) => boolean) | string | Partial<ModelAttributes>, fromIndex?: number): number {
|
|
433
|
+
return this.models.findLastIndex(
|
|
434
|
+
isFunction(pred)
|
|
435
|
+
? (pred as (model: T) => boolean)
|
|
436
|
+
: (m) => (isString(pred) ? m.get(pred as string) : m.matches(pred as Partial<ModelAttributes>)),
|
|
437
|
+
fromIndex
|
|
438
|
+
);
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
lastIndexOf(model: T, fromIndex?: number): number {
|
|
442
|
+
return this.models.lastIndexOf(model, fromIndex);
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
findIndex(pred: ((model: T) => boolean) | string | Partial<ModelAttributes>): number {
|
|
446
|
+
return this.models.findIndex(
|
|
447
|
+
isFunction(pred)
|
|
448
|
+
? (pred as (model: T) => boolean)
|
|
449
|
+
: (m) => (isString(pred) ? m.get(pred as string) : m.matches(pred as Partial<ModelAttributes>))
|
|
450
|
+
);
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
last(): T | undefined {
|
|
454
|
+
const length = this.models == null ? 0 : this.models.length;
|
|
455
|
+
return length ? this.models[length - 1] : undefined;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
head(): T | undefined {
|
|
459
|
+
return this.models[0];
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
first(): T | undefined {
|
|
463
|
+
return this.head();
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
map<U>(cb: string | ((model: T) => U) | Partial<ModelAttributes>, thisArg?: any): U[] {
|
|
467
|
+
return this.models.map(
|
|
468
|
+
isFunction(cb)
|
|
469
|
+
? (cb as (model: T) => U)
|
|
470
|
+
: (m) => (isString(cb) ? m.get(cb as string) : m.matches(cb as Partial<ModelAttributes>)),
|
|
471
|
+
thisArg
|
|
472
|
+
);
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
reduce(callback: (accumulator: T, model: T, index: number, array: T[]) => T, initialValue?: T): T {
|
|
476
|
+
return this.models.reduce(callback, initialValue || this.models[0]);
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
reduceRight(callback: (accumulator: T, model: T, index: number, array: T[]) => T, initialValue?: T): T {
|
|
480
|
+
return this.models.reduceRight(callback, initialValue || this.models[0]);
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
toArray(): T[] {
|
|
484
|
+
return Array.from(this.models);
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
/**
|
|
488
|
+
* Get a model from the set by id, cid, model object with id or cid
|
|
489
|
+
* properties, or an attributes object that is transformed through modelId.
|
|
490
|
+
*/
|
|
491
|
+
get(obj?: string | number | ModelAttributes | T | null): T | undefined {
|
|
492
|
+
if (obj == null) return undefined;
|
|
493
|
+
return (
|
|
494
|
+
this._byId[obj as string] ||
|
|
495
|
+
this._byId[this.modelId(this._isModel(obj) ? (obj as T).attributes : (obj as ModelAttributes)) as string] ||
|
|
496
|
+
((obj as T).cid && this._byId[(obj as T).cid])
|
|
497
|
+
);
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
/**
|
|
501
|
+
* Returns `true` if the model is in the collection.
|
|
502
|
+
*/
|
|
503
|
+
has(obj: string | number | ModelAttributes | T | null): boolean {
|
|
504
|
+
return this.get(obj) != null;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
/**
|
|
508
|
+
* Get the model at the given index.
|
|
509
|
+
*/
|
|
510
|
+
at(index: number): T | undefined {
|
|
511
|
+
if (index < 0) index += this.length;
|
|
512
|
+
return this.models[index];
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
/**
|
|
516
|
+
* Return models with matching attributes. Useful for simple cases of
|
|
517
|
+
* `filter`.
|
|
518
|
+
*/
|
|
519
|
+
where(attrs: ModelAttributes | Partial<ModelAttributes>, first?: boolean): T[] | T | undefined {
|
|
520
|
+
return this[first ? 'find' : 'filter'](attrs);
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
/**
|
|
524
|
+
* Return the first model with matching attributes. Useful for simple cases
|
|
525
|
+
* of `find`.
|
|
526
|
+
*/
|
|
527
|
+
findWhere(attrs: ModelAttributes): T | undefined {
|
|
528
|
+
return this.where(attrs, true) as T | undefined;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
find(predicate: ((model: T) => boolean) | Partial<ModelAttributes> | string, fromIndex?: number): T | undefined {
|
|
532
|
+
const pred = isFunction(predicate)
|
|
533
|
+
? (predicate as (model: T) => boolean)
|
|
534
|
+
: (m: T) => m.matches(predicate as Partial<ModelAttributes>);
|
|
535
|
+
return this.models.find(pred, fromIndex);
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
/**
|
|
539
|
+
* Force the collection to re-sort itself. You don't need to call this under
|
|
540
|
+
* normal circumstances, as the set will maintain sort order as each item
|
|
541
|
+
* is added.
|
|
542
|
+
*/
|
|
543
|
+
sort(options?: Options): this {
|
|
544
|
+
let comparator = this.comparator;
|
|
545
|
+
if (!comparator) throw new Error('Cannot sort a set without a comparator');
|
|
546
|
+
options = options || {};
|
|
547
|
+
|
|
548
|
+
const length = isFunction(comparator) ? comparator.length : 0;
|
|
549
|
+
if (isFunction(comparator)) comparator = (comparator as (a: T, b: T) => number).bind(this);
|
|
550
|
+
|
|
551
|
+
// Run sort based on type of `comparator`.
|
|
552
|
+
if (length === 1 || isString(comparator)) {
|
|
553
|
+
this.models = this.sortBy(comparator as string | ((model: T) => any));
|
|
554
|
+
} else {
|
|
555
|
+
this.models.sort(comparator as (a: T, b: T) => number);
|
|
556
|
+
}
|
|
557
|
+
if (!options.silent) this.trigger('sort', this, options);
|
|
558
|
+
return this;
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
/**
|
|
562
|
+
* Pluck an attribute from each model in the collection.
|
|
563
|
+
*/
|
|
564
|
+
pluck(attr: string): any[] {
|
|
565
|
+
return this.map(attr + '');
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
/**
|
|
569
|
+
* Fetch the default set of models for this collection, resetting the
|
|
570
|
+
* collection when they arrive. If `reset: true` is passed, the response
|
|
571
|
+
* data will be passed through the `reset` method instead of `set`.
|
|
572
|
+
*/
|
|
573
|
+
fetch(options?: Options): Promise<any> | any {
|
|
574
|
+
options = Object.assign({parse: true}, options);
|
|
575
|
+
const success = options.success;
|
|
576
|
+
// eslint-disable-next-line @typescript-eslint/no-this-alias
|
|
577
|
+
const collection = this;
|
|
578
|
+
const promise = options.promise && getResolveablePromise();
|
|
579
|
+
options.success = function (resp: any) {
|
|
580
|
+
const method = options.reset ? 'reset' : 'set';
|
|
581
|
+
collection[method](resp, options);
|
|
582
|
+
if (success) success.call(options.context, collection, resp, options);
|
|
583
|
+
promise && promise.resolve();
|
|
584
|
+
collection.trigger('sync', collection, resp, options);
|
|
585
|
+
};
|
|
586
|
+
wrapError(this, options);
|
|
587
|
+
return promise ? promise : this.sync('read', this, options);
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
/**
|
|
591
|
+
* Create a new instance of a model in this collection. Add the model to the
|
|
592
|
+
* collection immediately, unless `wait: true` is passed, in which case we
|
|
593
|
+
* wait for the server to agree.
|
|
594
|
+
*/
|
|
595
|
+
create(model: T | ModelAttributes, options?: Options): Promise<T> | T | false {
|
|
596
|
+
options = options ? clone(options) : {};
|
|
597
|
+
const wait = options.wait;
|
|
598
|
+
const return_promise = options.promise;
|
|
599
|
+
const promise = return_promise && getResolveablePromise();
|
|
600
|
+
|
|
601
|
+
const preparedModel = this._prepareModel(model, options);
|
|
602
|
+
if (!preparedModel) return false;
|
|
603
|
+
if (!wait) this.add(preparedModel, options);
|
|
604
|
+
// eslint-disable-next-line @typescript-eslint/no-this-alias
|
|
605
|
+
const collection = this;
|
|
606
|
+
const success = options.success;
|
|
607
|
+
const error = options.error;
|
|
608
|
+
options.success = function (m: T, resp: any, callbackOpts: Options) {
|
|
609
|
+
if (wait) {
|
|
610
|
+
collection.add(m, callbackOpts);
|
|
611
|
+
}
|
|
612
|
+
if (success) {
|
|
613
|
+
success.call(callbackOpts.context, m, resp, callbackOpts);
|
|
614
|
+
}
|
|
615
|
+
if (return_promise) {
|
|
616
|
+
promise.resolve(m);
|
|
617
|
+
}
|
|
618
|
+
};
|
|
619
|
+
options.error = function (model: T, e: any, options: Options) {
|
|
620
|
+
error && error.call(options.context, model, e, options);
|
|
621
|
+
return_promise && promise.reject(e);
|
|
622
|
+
};
|
|
623
|
+
|
|
624
|
+
preparedModel.save(null, Object.assign(options, {'promise': false}));
|
|
625
|
+
if (return_promise) {
|
|
626
|
+
return promise;
|
|
627
|
+
} else {
|
|
628
|
+
return preparedModel;
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
/**
|
|
633
|
+
* **parse** converts a response into a list of models to be added to the
|
|
634
|
+
* collection. The default implementation is just to pass it through.
|
|
635
|
+
*/
|
|
636
|
+
parse(resp: any, _options?: Options): any {
|
|
637
|
+
return resp;
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
/**
|
|
641
|
+
* Define how to uniquely identify models in the collection.
|
|
642
|
+
*/
|
|
643
|
+
modelId(attrs: ModelAttributes): string | number | undefined {
|
|
644
|
+
return attrs[this.model.prototype?.idAttribute || 'id'];
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
/** Get an iterator of all models in this collection. */
|
|
648
|
+
values(): CollectionIterator<T> {
|
|
649
|
+
return new CollectionIterator(this, ITERATOR_VALUES);
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
/**
|
|
653
|
+
* @public
|
|
654
|
+
* Enable for...of iteration over the collection.
|
|
655
|
+
*/
|
|
656
|
+
[Symbol.iterator] = this.values;
|
|
657
|
+
|
|
658
|
+
/** Get an iterator of all model IDs in this collection. */
|
|
659
|
+
keys(): CollectionIterator<T> {
|
|
660
|
+
return new CollectionIterator(this, ITERATOR_KEYS);
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
/** Get an iterator of all [ID, model] tuples in this collection. */
|
|
664
|
+
entries(): CollectionIterator<T> {
|
|
665
|
+
return new CollectionIterator(this, ITERATOR_KEYSVALUES);
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
/**
|
|
669
|
+
* Private method to reset all internal state. Called when the collection
|
|
670
|
+
* is first initialized or reset.
|
|
671
|
+
*/
|
|
672
|
+
_reset(): void {
|
|
673
|
+
this.models = [];
|
|
674
|
+
this._byId = {};
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
createModel(attrs: ModelAttributes, options?: Options): T {
|
|
678
|
+
const Klass = this.model;
|
|
679
|
+
return new Klass(attrs, options) as T;
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
/**
|
|
683
|
+
* Prepare a hash of attributes (or other model) to be added to this
|
|
684
|
+
* collection.
|
|
685
|
+
*/
|
|
686
|
+
_prepareModel(attrs: ModelAttributes | T, options?: Options): T | null {
|
|
687
|
+
if (this._isModel(attrs)) {
|
|
688
|
+
if (!(attrs as T).collection) (attrs as T).collection = this;
|
|
689
|
+
return attrs as T;
|
|
690
|
+
}
|
|
691
|
+
options = options ? clone(options) : {};
|
|
692
|
+
options.collection = this;
|
|
693
|
+
const model = this.createModel(attrs as ModelAttributes, options);
|
|
694
|
+
if (!model.validationError) return model;
|
|
695
|
+
this.trigger('invalid', this, model.validationError, options);
|
|
696
|
+
return null;
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
/**
|
|
700
|
+
* Internal method called by both remove and set.
|
|
701
|
+
*/
|
|
702
|
+
_removeModels(models: (T | ObjectWithId)[], options?: Options): T[] {
|
|
703
|
+
const removed: T[] = [];
|
|
704
|
+
for (let i = 0; i < models.length; i++) {
|
|
705
|
+
const model = this.get(models[i]);
|
|
706
|
+
if (!model) continue;
|
|
707
|
+
|
|
708
|
+
const index = this.indexOf(model);
|
|
709
|
+
this.models.splice(index, 1);
|
|
710
|
+
|
|
711
|
+
// Remove references before triggering 'remove' event to prevent an
|
|
712
|
+
// infinite loop. #3693
|
|
713
|
+
delete this._byId[model.cid];
|
|
714
|
+
const id = this.modelId(model.attributes);
|
|
715
|
+
if (id != null) delete this._byId[id];
|
|
716
|
+
|
|
717
|
+
if (!options?.silent) {
|
|
718
|
+
options = options || {};
|
|
719
|
+
options.index = index;
|
|
720
|
+
model.trigger('remove', model, this, options);
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
removed.push(model);
|
|
724
|
+
this._removeReference(model, options);
|
|
725
|
+
}
|
|
726
|
+
return removed;
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
/**
|
|
730
|
+
* Method for checking whether an object should be considered a model for
|
|
731
|
+
* the purposes of adding to the collection.
|
|
732
|
+
*/
|
|
733
|
+
_isModel(model: any): model is T {
|
|
734
|
+
return model instanceof Model;
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
/**
|
|
738
|
+
* Internal method to create a model's ties to a collection.
|
|
739
|
+
*/
|
|
740
|
+
_addReference(model: T, _options?: Options): void {
|
|
741
|
+
this._byId[model.cid] = model;
|
|
742
|
+
const id = this.modelId(model.attributes);
|
|
743
|
+
if (id != null) this._byId[id as string] = model;
|
|
744
|
+
model.on('all', this._onModelEvent, this);
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
/**
|
|
748
|
+
* Internal method to sever a model's ties to a collection.
|
|
749
|
+
*/
|
|
750
|
+
_removeReference(model: T, _options?: Options): void {
|
|
751
|
+
delete this._byId[model.cid];
|
|
752
|
+
const id = this.modelId(model.attributes);
|
|
753
|
+
if (id != null) delete this._byId[id as string];
|
|
754
|
+
if (this === model.collection) delete model.collection;
|
|
755
|
+
model.off('all', this._onModelEvent, this);
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
/**
|
|
759
|
+
* Internal method called every time a model in the set fires an event.
|
|
760
|
+
* Sets need to update their indexes when models change ids. All other
|
|
761
|
+
* events simply proxy through. "add" and "remove" events that originate
|
|
762
|
+
* in other collections are ignored.
|
|
763
|
+
*/
|
|
764
|
+
_onModelEvent(event: string, model: T, collection: Collection<T>, options?: Options): void {
|
|
765
|
+
if (model) {
|
|
766
|
+
if ((event === 'add' || event === 'remove') && collection !== this) return;
|
|
767
|
+
if (event === 'destroy') this.remove(model, options);
|
|
768
|
+
if (event === 'change') {
|
|
769
|
+
const prevId = this.modelId(model.previousAttributes());
|
|
770
|
+
const id = this.modelId(model.attributes);
|
|
771
|
+
if (prevId !== id) {
|
|
772
|
+
if (prevId != null) delete this._byId[prevId as string];
|
|
773
|
+
if (id != null) this._byId[id as string] = model;
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
this.trigger.apply(this, arguments as any);
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
// This "enum" defines the three possible kinds of values which can be emitted
|
|
782
|
+
// by a CollectionIterator that correspond to the values(), keys() and entries()
|
|
783
|
+
// methods on Collection, respectively.
|
|
784
|
+
const ITERATOR_VALUES = 1;
|
|
785
|
+
const ITERATOR_KEYS = 2;
|
|
786
|
+
const ITERATOR_KEYSVALUES = 3;
|
|
787
|
+
|
|
788
|
+
/**
|
|
789
|
+
* @public
|
|
790
|
+
*/
|
|
791
|
+
export class CollectionIterator<T extends Model> {
|
|
792
|
+
private _collection: Collection<T> | undefined;
|
|
793
|
+
private _kind: number;
|
|
794
|
+
private _index: number;
|
|
795
|
+
|
|
796
|
+
/**
|
|
797
|
+
* A CollectionIterator implements JavaScript's Iterator protocol, allowing the
|
|
798
|
+
* use of `for of` loops in modern browsers and interoperation between
|
|
799
|
+
* Collection and other JavaScript functions and third-party libraries
|
|
800
|
+
* which can operate on Iterables.
|
|
801
|
+
*/
|
|
802
|
+
constructor(collection: Collection<T>, kind: number) {
|
|
803
|
+
this._collection = collection;
|
|
804
|
+
this._kind = kind;
|
|
805
|
+
this._index = 0;
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
next(): IteratorResult<any> {
|
|
809
|
+
if (this._collection) {
|
|
810
|
+
// Only continue iterating if the iterated collection is long enough.
|
|
811
|
+
if (this._index < this._collection.length) {
|
|
812
|
+
const model = this._collection.at(this._index);
|
|
813
|
+
this._index++;
|
|
814
|
+
|
|
815
|
+
if (!model) {
|
|
816
|
+
return {value: undefined, done: true};
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
// Construct a value depending on what kind of values should be iterated.
|
|
820
|
+
let value: T | string | number | [string | number, T];
|
|
821
|
+
if (this._kind === ITERATOR_VALUES) {
|
|
822
|
+
value = model;
|
|
823
|
+
} else {
|
|
824
|
+
const id = this._collection.modelId(model.attributes);
|
|
825
|
+
if (this._kind === ITERATOR_KEYS) {
|
|
826
|
+
value = id;
|
|
827
|
+
} else {
|
|
828
|
+
// ITERATOR_KEYSVALUES
|
|
829
|
+
value = [id, model];
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
return {value, done: false};
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
// Once exhausted, remove the reference to the collection so future
|
|
836
|
+
// calls to the next method always return done.
|
|
837
|
+
this._collection = undefined;
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
return {value: undefined, done: true};
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
[Symbol.iterator](): IterableIterator<any> {
|
|
844
|
+
return this;
|
|
845
|
+
}
|
|
846
|
+
}
|