-
Notifications
You must be signed in to change notification settings - Fork 1
/
reactive-list.js
313 lines (277 loc) · 8.94 KB
/
reactive-list.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
// #ReactiveList
// Provides a simple reactive list interface
var _noopCallback = function() {};
var _nonReactive = {
changed: _noopCallback,
depend: _noopCallback
};
/** @method ReactiveList Keeps a reactive list of key+value items
* @constructor
* @namespace ReactiveList
* @param {object} [options]
* @param {function} sort The sort algorithm to use
* @param {boolean} [reactive=true] If set false this list is not reactive
* Example:
* ```js
* var list = new ReactiveList();
* list.insert(1, { text: 'Hello id: 1' });
* list.insert(2, { text: 'Hello id: 2' });
* list.insert(3, { text: 'Hello id: 3' });
* list.update(2, { text: 'Updated 2'});
* list.remove(1);
*
* list.forEach(function(value, key) {
* console.log('GOT: ' + value.text);
* }, true); // Set noneReactive = true, default behaviour is reactive
*
* // Return from Template:
* Template.hello.list = function() {
* return list.fetch();
* };
* ```
*
* ####Example of a sort algorithm
* Sort can be used to define the order of the list
* ```js
* var list = new ReactiveList({
* sort: function(a, b) {
* // a and b are type of { key, value }
* // here we sort by the key:
* return a.key < b.key;
* }
* });
* ```
* ###Object chain
* ```
* first last
* undefined - obj - obj - obj - undefined
* (prev value next) (prev value next) (prev value next)
* ```
*/
ReactiveList = function(options) {
var self = this;
// Object container
self.lookup = {};
// Length
self._length = 0;
// First object in list
self.first;
// Last object in list
self.last;
// Set sort to options.sort or default to true (asc)
self.sort = (options && options.sort || function(a, b) {
return a.key < b.key;
});
// Allow user to disable reactivity, default true
self.isReactive = (options)? options.reactive !== false : true;
// If lifo queue
if (options === true || options && options.sort === true) {
self.sort = function(a, b) { return a.key > b.key; };
}
// Rig the dependencies
self._listDeps = (self.isReactive)? new Deps.Dependency() : _nonReactive;
self._lengthDeps = (self.isReactive)? new Deps.Dependency() : _nonReactive;
};
/** @method ReactiveList.prototype.length Returns the length of the list
* @reactive
* @returns {number} Length of the reactive list
*/
ReactiveList.prototype.length = function() {
var self = this;
// Make this reactive
self._lengthDeps.depend();
return self._length;
};
/** @method ReactiveList.prototype.reset Reset and empty the list
* @todo Check for memory leaks, if so we have to iterate over lookup and delete the items
*/
ReactiveList.prototype.reset = function() {
var self = this;
// Clear the reference to the first object
self.first = undefined;
// Clear the reference to the last object
self.last = undefined;
// Clear the lookup object
self.lookup = {};
// Set the length to 0
self._length = 0;
self._lengthDeps.changed();
// Invalidate the list
self._listDeps.changed();
};
/** @method ReactiveList.prototype.update
* @param {string|number} key Key to update
* @param {any} value Update with this value
*/
ReactiveList.prototype.update = function(key, value) {
var self = this;
// Make sure the key is found in the list
if (typeof self.lookup[key] === 'undefined') {
throw new Error('Reactive list cannot update, key "' + key + '" not found');
}
// Set the new value
self.lookup[key].value = value;
// Invalidate the list
self._listDeps.changed();
};
/** @method ReactiveList.prototype.insert
* @param {string|number} key Key to insert
* @param {any} value Insert item with this value
*/
ReactiveList.prototype.insert = function(key, value) {
var self = this;
if (typeof self.lookup[key] !== 'undefined') {
throw new Error('Reactive list could not insert: key "' + key +
'" allready found');
}
// Create the new item to insert into the list
var newItem = { key: key, value: value };
// Init current by pointing it at the first object in the list
var current = self.first;
// Init the isInserted flag
var isInserted = false;
// Iterate through list while not empty and item is not inserted
while (typeof current !== 'undefined' && !isInserted) {
// Sort the list by using the sort function
if (self.sort(newItem, current)) {
// Insert self.lookup[key] before
if (typeof current.prev === 'undefined') { self.first = newItem; }
// Set the references in the inserted object
newItem.prev = current.prev;
newItem.next = current;
// Update the two existing objects
if (current.prev) { current.prev.next = newItem; }
current.prev = newItem;
// Mark the item as inserted - job's done
isInserted = true;
}
// Goto next object
current = current.next;
}
if (!isInserted) {
// We append it to the list
newItem.prev = self.last;
if (self.last) { self.last.next = newItem; }
// Update the last pointing to newItem
self.last = newItem;
// Update first if we are appending to an empty list
if (self._length === 0) { self.first = newItem; }
}
// Reference the object for a quick lookup option
self.lookup[key] = newItem;
// Increase length
self._length++;
self._lengthDeps.changed();
// And invalidate the list
self._listDeps.changed();
};
/** @method ReactiveList.prototype.remove
* @param {string|number} key Key to remove
*/
ReactiveList.prototype.remove = function(key) {
var self = this;
// Get the item object
var item = self.lookup[key];
// Check that it exists
if (typeof item === 'undefined') {
return;
// throw new Error('ReactiveList cannot remove item, unknow key "' + key +
// '"');
}
// Rig the references
var prevItem = item.prev;
var nextItem = item.next;
// Update chain prev object next reference
if (typeof prevItem !== 'undefined') {
prevItem.next = nextItem;
} else {
self.first = nextItem;
}
// Update chain next object prev reference
if (typeof nextItem !== 'undefined') {
nextItem.prev = prevItem;
} else {
self.last = prevItem;
}
// Clean up
self.lookup[key].last = null;
self.lookup[key].prev = null;
self.lookup[key] = null;
prevItem = null;
delete self.lookup[key];
// Decrease the length
self._length--;
self._lengthDeps.changed();
// Invalidate the list
self._listDeps.changed();
};
/** @method ReactiveList.prototype.getLastItem
* @returns {any} Pops last item from the list - removes the item from the list
*/
ReactiveList.prototype.getLastItem = function(first) {
var self = this;
// Get the relevant item first or last
var item = (first)?self.first: self.last;
if (typeof item === 'undefined') {
return; // Empty list
}
// Remove the item from the list
self.remove(item.key);
// Return the value
return item.value;
};
/** @method ReactiveList.prototype.getFirstItem
* @returns {any} Pops first item from the list - removes the item from the list
*/
ReactiveList.prototype.getFirstItem = function() {
// This gets the first item...
return this.getLastItem(true);
};
/** @method ReactiveList.prototype.forEach
* @param {function} f Callback `funciton(value, key)`
* @param {boolean} [noneReactive=false] Set true if want to disable reactivity
* @param {boolean} [reverse=false] Set true to reverse iteration `forEachReverse`
*/
ReactiveList.prototype.forEach = function(f, noneReactive, reverse) {
var self = this;
// Check if f is a function
if (typeof f !== 'function') {
throw new Error('ReactiveList forEach requires a function');
}
// We allow this not to be reactive
if (!noneReactive) { self._listDeps.depend(); }
// Set current to the first object
var current = (reverse)?self.last: self.first;
// Iterate over the list while its not empty
while (current) {
// Call the callback function
f(current.value, current.key);
// Jump to the next item in the list
current = (reverse)?current.prev: current.next;
}
};
/** @method ReactiveList.prototype.forEachReverse
* @param {function} f Callback `funciton(value, key)`
* @param {boolean} [noneReactive=false] Set true if want to disable reactivity
*/
ReactiveList.prototype.forEachReverse = function(f, noneReactive) {
// Call forEach with the reverse flag
this.forEach(f, noneReactive, true);
};
/** @method ReactiveList.prototype.fetch Returns list as array
* @param {boolean} [noneReactive=false] Set true if want to disable reactivity
* @reactive This can be disabled
* @returns {array} List of items
*/
ReactiveList.prototype.fetch = function(noneReactive) {
var self = this;
// Init the result buffer
var result = [];
// Iterate over the list items
self.forEach(function fetchCallback(value) {
// Add the item value to the result
result.push(value);
}, noneReactive);
// Return the result
return result;
};