-
Notifications
You must be signed in to change notification settings - Fork 28
/
Store.js
411 lines (381 loc) · 15.2 KB
/
Store.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
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
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
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
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
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
408
409
410
411
/** @module delite/Store */
define([
"dcl/dcl",
"dojo-dstore/Memory",
"requirejs-dplugins/has",
"ibm-decor/Invalidating"
], function (dcl, Memory, has, Invalidating) {
var emptyObject = {};
// Function to compare queries. Queries can be functions or objects, but the objects
// can contain regular expressions, so we can't just use JSON.stringify().
function deepEqual (a, b) {
if (a instanceof RegExp && b instanceof RegExp || typeof a === "function" && typeof b === "function") {
return a.toString() === b.toString();
} else if (a && typeof a === "object" && b && typeof b === "object") {
var aKeys = Object.keys(a);
return aKeys.length === Object.keys(b).length && aKeys.every(function (key) {
return key in b && deepEqual(a[key], b[key]);
});
} else {
return a === b;
}
}
/**
* Dispatched once the query has been executed and the `renderItems` array
* has been initialized with the list of initial render items.
* @example
* widget.on("query-success", function (evt) {
* console.log("query done, initial renderItems: " + evt.renderItems);
* });
* @event module:delite/Store#query-success
* @property {Object[]} renderItems - The array of initial render items.
* @property {boolean} cancelable - Indicates whether the event is cancelable or not.
* @property {boolean} bubbles - Indicates whether the given event bubbles up through the DOM or not.
*/
/**
* Mixin for store management that creates render items from store items after
* querying the store. The receiving class must extend decor/Evented or delite/Widget.
*
* Classes extending this mixin automatically create render items that are consumable
* from store items after querying the store. This happens each time the `store`, `query` or
* `queryOptions` properties are set. If that store is Trackable it will be observed and render items
* will be automatically updated, added or deleted based on store notifications.
*
* @mixin module:delite/Store
*/
return dcl(Invalidating, /** @lends module:delite/Store# */ {
declaredClass: "delite/Store",
/**
* The source that contains the items to display.
* @member {(dstore/Store|Array)}
* @default null
*/
source: null,
/**
* A query filter to apply to the store.
* @member {Object}
* @default {}
*/
query: dcl.prop({
set: function (newQuery) {
// Avoid triggering refresh when query hasn't really changed.
if (!newQuery) {
newQuery = emptyObject;
}
if (!deepEqual(newQuery, this.query)) {
this._set("query", newQuery);
}
},
get: function () {
return this._get("query") || emptyObject;
},
enumerable: true,
configurable: true
}),
/**
* A function that processes the collection or the array returned by the source query and returns a new
* collection or a new array (to sort it, etc...). This processing is applied before potentially tracking
* the source for modifications (if Trackable or Observable).
* Be careful you can not use the same function for both arrays and collections.
* Changing this function on the instance will not automatically refresh the class.
* @default identity function
*/
processQueryResult: function (source) { return source; },
/**
* The render items corresponding to the store items for this widget. This is filled from the store and
* is not supposed to be modified directly. Initially null.
* @member {Object[]}
* @default null
*/
renderItems: null,
/**
* Creates a store item based from the widget internal item.
* @param {Object} renderItem - The render item.
* @returns {Object}
*/
renderItemToItem: function (renderItem) {
return renderItem;
},
/**
* Returns the widget internal item for a given store item. By default it returns the store
* item itself.
* @param {Object} item - The store item.
* @returns {Object}
* @protected
*/
itemToRenderItem: function (item) {
return item;
},
beforeInitializeRendering: function () {
// If the control seems to contain JSON, then parse it as our read-only data source.
if (!this.firstElementChild && this.textContent.trim()) {
var data = JSON.parse("[" + this.textContent + "]");
if (data.length) {
this.source = data;
for (var j = 0; j < data.length; j++) {
if (!data[j].id) {
data[j].id = Math.random();
}
}
}
this.textContent = "";
}
},
/**
* This method is called once the query has been executed to initialize the renderItems array
* with the list of initial render items.
*
* This method sets the renderItems property to the render items array passed as parameter. Once
* done, it fires a 'query-success' event.
* @param {Object[]} renderItems - The array of initial render items to be set in the renderItems property.
* @returns {Object[]} the renderItems array.
* @protected
* @fires module:delite/Store#query-success
*/
initItems: function (renderItems) {
this.renderItems = renderItems;
this.emit("query-success", { renderItems: renderItems, cancelable: false, bubbles: true });
return renderItems;
},
computeProperties: dcl.after(function (args) {
// Runs after the subclass computeProperties() methods run and possibly set this.query and this.source.
// If this call is upon widget creation but `this.source` is not available, don't bother querying store.
// If the store parameters are invalidated, queries the store, creates the render items
// and calls initItems() when ready. If an error occurs a 'query-error' event will be fired.
// If this call is upon widget creation but `this.store` is not available, don't bother querying store.
var props = args[0], isAfterCreation = args[1];
if (("source" in props || "query" in props) && (this.source || !isAfterCreation)) {
this.queryStoreAndInitItems(this.processQueryResult);
}
}),
/**
* Queries the store, creates the render items and calls initItems() when ready. If an error occurs
* a 'query-error' event will be fired.
*
* This method is not supposed to be called by application developer.
* It will be called automatically when modifying the store related properties or by the subclass
* if needed.
* @param processQueryResult - A function that processes the collection returned by the store query
* and returns a new collection (to sort it, etc...)., applied before tracking.
* @returns {Promise} If store to be processed is not null a promise that will be resolved when the loading
* process will be finished.
* @protected
*/
queryStoreAndInitItems: function (processQueryResult) {
this._untrack();
if (this.source) {
var collection = this._store = Array.isArray(this.source) ? new Memory({
data: this.source,
getIdentity: function (item) {
return item.id !== undefined ? item.id : this.data.indexOf(item);
}
}) : this.source;
if (typeof this.query === "function" || (this.query && Object.keys(this.query).length)) {
// Only call filter() when there's a real filter, because applying any filter stops dstore/Cache
// from caching.
collection = collection.filter(this.query);
}
collection = this._collection = processQueryResult(collection);
if (collection.track) {
collection = this._tracked = collection.track();
this._addListener = collection.on("add", this._itemAdded.bind(this));
this._deleteListener = collection.on("delete", this._itemRemoved.bind(this));
this._updateListener = collection.on("update", this._itemUpdated.bind(this));
this._newQueryListener = collection.on("_new-query-asked", function (evt) {
this.emit("new-query-asked", evt);
}.bind(this));
}
return this.processCollection(collection);
} else {
this.initItems([]);
}
},
/**
* Synchronously deliver change records to all listeners registered via `observe()`.
*/
deliver: dcl.superCall(function (sup) {
return function () {
sup.apply(this, arguments);
if (this._collection && typeof this._collection.deliver === "function") {
this._collection.deliver();
}
};
}),
/**
* Discard change records for all listeners registered via `observe()`.
*/
discardChanges: dcl.superCall(function (sup) {
return function () {
sup.apply(this, arguments);
if (this._collection && typeof this._collection.discardChanges === "function") {
this._collection.discardChanges();
}
};
}),
/**
* Called to process the items returned after querying the store.
* @param {dstore/Collection} collection - Items to be displayed.
* @protected
*/
processCollection: function (collection) {
return this.fetch(collection).then(function (items) {
return this.initItems(items.map(this.itemToRenderItem.bind(this)));
}.bind(this), this._queryError.bind(this));
},
/**
* Called to perform the fetch operation on the collection.
* @param {dstore/Collection} collection - Items to be displayed.
* @protected
*/
fetch: function (collection) {
return collection.fetch();
},
_queryError: function (error) {
console.log(error);
this.emit("query-error", { error: error, cancelable: false, bubbles: true });
},
_untrack: function () {
if (this._addListener) {
this._addListener.remove(this._addListener);
}
if (this._deleteListener) {
this._deleteListener.remove(this._deleteListener);
}
if (this._updateListener) {
this._updateListener.remove(this._updateListener);
}
if (this._newQueryListener) {
this._newQueryListener.remove(this._newQueryListener);
}
if (this._tracked) {
this._tracked.tracking.remove();
this._tracked = null;
}
},
destroy: function () {
this._untrack();
},
/**
* This method is called when an item is removed from an observable store. The default
* implementation actually removes a renderItem from the renderItems array. This can be redefined but
* must not be called directly.
* @param {number} index - The index of the render item to remove.
* @param {Object[]} renderItems - The array of render items to remove the render item from.
* @protected
*/
itemRemoved: function (index, renderItems) {
renderItems.splice(index, 1);
},
/**
* This method is called when an item is added in an observable store. The default
* implementation actually adds the renderItem to the renderItems array. This can be redefined but
* must not be called directly.
* @param {number} index - The index where to add the render item.
* @param {Object} renderItem - The render item to be added.
* @param {Object[]} renderItems - The array of render items to add the render item to.
* @protected
*/
itemAdded: function (index, renderItem, renderItems) {
renderItems.splice(index, 0, renderItem);
},
/**
* This method is called when an item is updated in an observable store. The default
* implementation actually updates the renderItem in the renderItems array. This can be redefined but
* must not be called directly.
* @param {number} index - The index of the render item to update.
* @param {Object} renderItem - The render item data the render item must be updated with.
* @param {Object[]} renderItems - The array of render items to render item to be updated is part of.
* @protected
*/
itemUpdated: function (index, renderItem, renderItems) {
// we want to keep the same item object and mixin new values into old object
for (var n in renderItem) {
renderItems[index][n] = renderItem[n];
}
},
/**
* This method is called when an item is moved in an observable store. The default
* implementation actually moves the renderItem in the renderItems array. This can be redefined but
* must not be called directly.
* @param {number} previousIndex - The previous index of the render item.
* @param {number} newIndex - The new index of the render item.
* @param {Object} renderItem - The render item to be moved.
* @param {Object[]} renderItems - The array of render items to render item to be moved is part of.
* @protected
*/
itemMoved: function (previousIndex, newIndex, renderItem, renderItems) {
// we want to keep the same item object and mixin new values into old object
this.itemRemoved(previousIndex, renderItems);
this.itemAdded(newIndex, renderItem, renderItems);
},
_refreshHandler: function () {
this.queryStoreAndInitItems(this.processQueryResult);
},
/**
* When the store is observed and an item is removed in the store this method is called to remove the
* corresponding render item. This can be redefined but must not be called directly.
* @param {Event} event - The "remove" `dstore/Trackable` event.
* @private
*/
_itemRemoved: function (event) {
if (event.previousIndex !== undefined) {
this.itemRemoved(event.previousIndex, this.renderItems);
// the change of the value of the renderItems property (splice of the array)
// does not automatically trigger a notification. Hence:
this.notifyCurrentValue("renderItems");
}
// if no previousIndex the items is removed outside of the range we monitor so we don't care
},
/**
* When the store is observed and an item is updated in the store this method is called to update the
* corresponding render item. This can be redefined but must not be called directly.
* @param {Event} event - The "update" `dstore/Trackable` event.
* @private
*/
_itemUpdated: function (event) {
if (event.previousIndex === undefined && event.index === undefined) {
// Workaround SitePen/dstore#188, can be removed when dstore 1.1.3 (or 1.2.0) released.
return;
} else if (event.index === undefined) {
// this is actually a remove
this.itemRemoved(event.previousIndex, this.renderItems);
} else if (event.previousIndex === undefined) {
// this is actually a add
this.itemAdded(event.index, this.itemToRenderItem(event.target), this.renderItems);
} else if (event.index !== event.previousIndex) {
// this is a move
this.itemMoved(event.previousIndex, event.index, this.itemToRenderItem(event.target), this.renderItems);
} else {
// we want to keep the same item object and mixin new values into old object
this.itemUpdated(event.index, this.itemToRenderItem(event.target), this.renderItems);
}
// the change of the value of the renderItems property (splice of the array)
// does not automatically trigger a notification. Hence:
this.notifyCurrentValue("renderItems");
},
/**
* When the store is observed and an item is added in the store this method is called to add the
* corresponding render item. This can be redefined but must not be called directly.
* @param {Event} event - The "add" `dstore/Trackable` event.
* @private
*/
_itemAdded: function (event) {
if (event.index !== undefined) {
this.itemAdded(event.index, this.itemToRenderItem(event.target), this.renderItems);
// the change of the value of the renderItems property (splice of the array)
// does not automatically trigger a notification. Hence:
this.notifyCurrentValue("renderItems");
}
// if no index the item is added outside of the range we monitor so we don't care
},
/**
* Return the identity of an item.
* @param {Object} item - The item
* @returns {Object}
* @protected
*/
getIdentity: function (item) {
return this._store.getIdentity(item);
}
});
});