385 lines
31 KiB
JavaScript
385 lines
31 KiB
JavaScript
|
(function () {
|
||
|
|
||
|
/* Imports */
|
||
|
var Meteor = Package.meteor.Meteor;
|
||
|
var Tracker = Package.tracker.Tracker;
|
||
|
var Deps = Package.tracker.Deps;
|
||
|
var LocalCollection = Package.minimongo.LocalCollection;
|
||
|
var Minimongo = Package.minimongo.Minimongo;
|
||
|
var _ = Package.underscore._;
|
||
|
var Random = Package.random.Random;
|
||
|
|
||
|
/* Package-scope variables */
|
||
|
var ObserveSequence, seqChangedToEmpty, seqChangedToArray, seqChangedToCursor;
|
||
|
|
||
|
(function () {
|
||
|
|
||
|
///////////////////////////////////////////////////////////////////////////////////
|
||
|
// //
|
||
|
// packages/observe-sequence/observe_sequence.js //
|
||
|
// //
|
||
|
///////////////////////////////////////////////////////////////////////////////////
|
||
|
//
|
||
|
var warn = function () { // 1
|
||
|
if (ObserveSequence._suppressWarnings) { // 2
|
||
|
ObserveSequence._suppressWarnings--; // 3
|
||
|
} else { // 4
|
||
|
if (typeof console !== 'undefined' && console.warn) // 5
|
||
|
console.warn.apply(console, arguments); // 6
|
||
|
// 7
|
||
|
ObserveSequence._loggedWarnings++; // 8
|
||
|
} // 9
|
||
|
}; // 10
|
||
|
// 11
|
||
|
var idStringify = LocalCollection._idStringify; // 12
|
||
|
var idParse = LocalCollection._idParse; // 13
|
||
|
// 14
|
||
|
ObserveSequence = { // 15
|
||
|
_suppressWarnings: 0, // 16
|
||
|
_loggedWarnings: 0, // 17
|
||
|
// 18
|
||
|
// A mechanism similar to cursor.observe which receives a reactive // 19
|
||
|
// function returning a sequence type and firing appropriate callbacks // 20
|
||
|
// when the value changes. // 21
|
||
|
// // 22
|
||
|
// @param sequenceFunc {Function} a reactive function returning a // 23
|
||
|
// sequence type. The currently supported sequence types are: // 24
|
||
|
// 'null', arrays and cursors. // 25
|
||
|
// // 26
|
||
|
// @param callbacks {Object} similar to a specific subset of // 27
|
||
|
// callbacks passed to `cursor.observe` // 28
|
||
|
// (http://docs.meteor.com/#observe), with minor variations to // 29
|
||
|
// support the fact that not all sequences contain objects with // 30
|
||
|
// _id fields. Specifically: // 31
|
||
|
// // 32
|
||
|
// * addedAt(id, item, atIndex, beforeId) // 33
|
||
|
// * changedAt(id, newItem, oldItem, atIndex) // 34
|
||
|
// * removedAt(id, oldItem, atIndex) // 35
|
||
|
// * movedTo(id, item, fromIndex, toIndex, beforeId) // 36
|
||
|
// // 37
|
||
|
// @returns {Object(stop: Function)} call 'stop' on the return value // 38
|
||
|
// to stop observing this sequence function. // 39
|
||
|
// // 40
|
||
|
// We don't make any assumptions about our ability to compare sequence // 41
|
||
|
// elements (ie, we don't assume EJSON.equals works; maybe there is extra // 42
|
||
|
// state/random methods on the objects) so unlike cursor.observe, we may // 43
|
||
|
// sometimes call changedAt() when nothing actually changed. // 44
|
||
|
// XXX consider if we *can* make the stronger assumption and avoid // 45
|
||
|
// no-op changedAt calls (in some cases?) // 46
|
||
|
// // 47
|
||
|
// XXX currently only supports the callbacks used by our // 48
|
||
|
// implementation of {{#each}}, but this can be expanded. // 49
|
||
|
// // 50
|
||
|
// XXX #each doesn't use the indices (though we'll eventually need // 51
|
||
|
// a way to get them when we support `@index`), but calling // 52
|
||
|
// `cursor.observe` causes the index to be calculated on every // 53
|
||
|
// callback using a linear scan (unless you turn it off by passing // 54
|
||
|
// `_no_indices`). Any way to avoid calculating indices on a pure // 55
|
||
|
// cursor observe like we used to? // 56
|
||
|
observe: function (sequenceFunc, callbacks) { // 57
|
||
|
var lastSeq = null; // 58
|
||
|
var activeObserveHandle = null; // 59
|
||
|
// 60
|
||
|
// 'lastSeqArray' contains the previous value of the sequence // 61
|
||
|
// we're observing. It is an array of objects with '_id' and // 62
|
||
|
// 'item' fields. 'item' is the element in the array, or the // 63
|
||
|
// document in the cursor. // 64
|
||
|
// // 65
|
||
|
// '_id' is whichever of the following is relevant, unless it has // 66
|
||
|
// already appeared -- in which case it's randomly generated. // 67
|
||
|
// // 68
|
||
|
// * if 'item' is an object: // 69
|
||
|
// * an '_id' field, if present // 70
|
||
|
// * otherwise, the index in the array // 71
|
||
|
// // 72
|
||
|
// * if 'item' is a number or string, use that value // 73
|
||
|
// // 74
|
||
|
// XXX this can be generalized by allowing {{#each}} to accept a // 75
|
||
|
// general 'key' argument which could be a function, a dotted // 76
|
||
|
// field name, or the special @index value. // 77
|
||
|
var lastSeqArray = []; // elements are objects of form {_id, item} // 78
|
||
|
var computation = Tracker.autorun(function () { // 79
|
||
|
var seq = sequenceFunc(); // 80
|
||
|
// 81
|
||
|
Tracker.nonreactive(function () { // 82
|
||
|
var seqArray; // same structure as `lastSeqArray` above. // 83
|
||
|
// 84
|
||
|
if (activeObserveHandle) { // 85
|
||
|
// If we were previously observing a cursor, replace lastSeqArray with // 86
|
||
|
// more up-to-date information. Then stop the old observe. // 87
|
||
|
lastSeqArray = _.map(lastSeq.fetch(), function (doc) { // 88
|
||
|
return {_id: doc._id, item: doc}; // 89
|
||
|
}); // 90
|
||
|
activeObserveHandle.stop(); // 91
|
||
|
activeObserveHandle = null; // 92
|
||
|
} // 93
|
||
|
// 94
|
||
|
if (!seq) { // 95
|
||
|
seqArray = seqChangedToEmpty(lastSeqArray, callbacks); // 96
|
||
|
} else if (seq instanceof Array) { // 97
|
||
|
seqArray = seqChangedToArray(lastSeqArray, seq, callbacks); // 98
|
||
|
} else if (isStoreCursor(seq)) { // 99
|
||
|
var result /* [seqArray, activeObserveHandle] */ = // 100
|
||
|
seqChangedToCursor(lastSeqArray, seq, callbacks); // 101
|
||
|
seqArray = result[0]; // 102
|
||
|
activeObserveHandle = result[1]; // 103
|
||
|
} else { // 104
|
||
|
throw badSequenceError(); // 105
|
||
|
} // 106
|
||
|
// 107
|
||
|
diffArray(lastSeqArray, seqArray, callbacks); // 108
|
||
|
lastSeq = seq; // 109
|
||
|
lastSeqArray = seqArray; // 110
|
||
|
}); // 111
|
||
|
}); // 112
|
||
|
// 113
|
||
|
return { // 114
|
||
|
stop: function () { // 115
|
||
|
computation.stop(); // 116
|
||
|
if (activeObserveHandle) // 117
|
||
|
activeObserveHandle.stop(); // 118
|
||
|
} // 119
|
||
|
}; // 120
|
||
|
}, // 121
|
||
|
// 122
|
||
|
// Fetch the items of `seq` into an array, where `seq` is of one of the // 123
|
||
|
// sequence types accepted by `observe`. If `seq` is a cursor, a // 124
|
||
|
// dependency is established. // 125
|
||
|
fetch: function (seq) { // 126
|
||
|
if (!seq) { // 127
|
||
|
return []; // 128
|
||
|
} else if (seq instanceof Array) { // 129
|
||
|
return seq; // 130
|
||
|
} else if (isStoreCursor(seq)) { // 131
|
||
|
return seq.fetch(); // 132
|
||
|
} else { // 133
|
||
|
throw badSequenceError(); // 134
|
||
|
} // 135
|
||
|
} // 136
|
||
|
}; // 137
|
||
|
// 138
|
||
|
var badSequenceError = function () { // 139
|
||
|
return new Error("{{#each}} currently only accepts " + // 140
|
||
|
"arrays, cursors or falsey values."); // 141
|
||
|
}; // 142
|
||
|
// 143
|
||
|
var isStoreCursor = function (cursor) { // 144
|
||
|
return cursor && _.isObject(cursor) && // 145
|
||
|
_.isFunction(cursor.observe) && _.isFunction(cursor.fetch); // 146
|
||
|
}; // 147
|
||
|
// 148
|
||
|
// Calculates the differences between `lastSeqArray` and // 149
|
||
|
// `seqArray` and calls appropriate functions from `callbacks`. // 150
|
||
|
// Reuses Minimongo's diff algorithm implementation. // 151
|
||
|
var diffArray = function (lastSeqArray, seqArray, callbacks) { // 152
|
||
|
var diffFn = Package.minimongo.LocalCollection._diffQueryOrderedChanges; // 153
|
||
|
var oldIdObjects = []; // 154
|
||
|
var newIdObjects = []; // 155
|
||
|
var posOld = {}; // maps from idStringify'd ids // 156
|
||
|
var posNew = {}; // ditto // 157
|
||
|
var posCur = {}; // 158
|
||
|
var lengthCur = lastSeqArray.length; // 159
|
||
|
// 160
|
||
|
_.each(seqArray, function (doc, i) { // 161
|
||
|
newIdObjects.push({_id: doc._id}); // 162
|
||
|
posNew[idStringify(doc._id)] = i; // 163
|
||
|
}); // 164
|
||
|
_.each(lastSeqArray, function (doc, i) { // 165
|
||
|
oldIdObjects.push({_id: doc._id}); // 166
|
||
|
posOld[idStringify(doc._id)] = i; // 167
|
||
|
posCur[idStringify(doc._id)] = i; // 168
|
||
|
}); // 169
|
||
|
// 170
|
||
|
// Arrays can contain arbitrary objects. We don't diff the // 171
|
||
|
// objects. Instead we always fire 'changedAt' callback on every // 172
|
||
|
// object. The consumer of `observe-sequence` should deal with // 173
|
||
|
// it appropriately. // 174
|
||
|
diffFn(oldIdObjects, newIdObjects, { // 175
|
||
|
addedBefore: function (id, doc, before) { // 176
|
||
|
var position = before ? posCur[idStringify(before)] : lengthCur; // 177
|
||
|
// 178
|
||
|
if (before) { // 179
|
||
|
// If not adding at the end, we need to update indexes. // 180
|
||
|
// XXX this can still be improved greatly! // 181
|
||
|
_.each(posCur, function (pos, id) { // 182
|
||
|
if (pos >= position) // 183
|
||
|
posCur[id]++; // 184
|
||
|
}); // 185
|
||
|
} // 186
|
||
|
// 187
|
||
|
lengthCur++; // 188
|
||
|
posCur[idStringify(id)] = position; // 189
|
||
|
// 190
|
||
|
callbacks.addedAt( // 191
|
||
|
id, // 192
|
||
|
seqArray[posNew[idStringify(id)]].item, // 193
|
||
|
position, // 194
|
||
|
before); // 195
|
||
|
}, // 196
|
||
|
movedBefore: function (id, before) { // 197
|
||
|
if (id === before) // 198
|
||
|
return; // 199
|
||
|
// 200
|
||
|
var oldPosition = posCur[idStringify(id)]; // 201
|
||
|
var newPosition = before ? posCur[idStringify(before)] : lengthCur; // 202
|
||
|
// 203
|
||
|
// Moving the item forward. The new element is losing one position as it // 204
|
||
|
// was removed from the old position before being inserted at the new // 205
|
||
|
// position. // 206
|
||
|
// Ex.: 0 *1* 2 3 4 // 207
|
||
|
// 0 2 3 *1* 4 // 208
|
||
|
// The original issued callback is "1" before "4". // 209
|
||
|
// The position of "1" is 1, the position of "4" is 4. // 210
|
||
|
// The generated move is (1) -> (3) // 211
|
||
|
if (newPosition > oldPosition) { // 212
|
||
|
newPosition--; // 213
|
||
|
} // 214
|
||
|
// 215
|
||
|
// Fix up the positions of elements between the old and the new positions // 216
|
||
|
// of the moved element. // 217
|
||
|
// // 218
|
||
|
// There are two cases: // 219
|
||
|
// 1. The element is moved forward. Then all the positions in between // 220
|
||
|
// are moved back. // 221
|
||
|
// 2. The element is moved back. Then the positions in between *and* the // 222
|
||
|
// element that is currently standing on the moved element's future // 223
|
||
|
// position are moved forward. // 224
|
||
|
_.each(posCur, function (elCurPosition, id) { // 225
|
||
|
if (oldPosition < elCurPosition && elCurPosition < newPosition) // 226
|
||
|
posCur[id]--; // 227
|
||
|
else if (newPosition <= elCurPosition && elCurPosition < oldPosition) // 228
|
||
|
posCur[id]++; // 229
|
||
|
}); // 230
|
||
|
// 231
|
||
|
// Finally, update the position of the moved element. // 232
|
||
|
posCur[idStringify(id)] = newPosition; // 233
|
||
|
// 234
|
||
|
callbacks.movedTo( // 235
|
||
|
id, // 236
|
||
|
seqArray[posNew[idStringify(id)]].item, // 237
|
||
|
oldPosition, // 238
|
||
|
newPosition, // 239
|
||
|
before); // 240
|
||
|
}, // 241
|
||
|
removed: function (id) { // 242
|
||
|
var prevPosition = posCur[idStringify(id)]; // 243
|
||
|
// 244
|
||
|
_.each(posCur, function (pos, id) { // 245
|
||
|
if (pos >= prevPosition) // 246
|
||
|
posCur[id]--; // 247
|
||
|
}); // 248
|
||
|
// 249
|
||
|
delete posCur[idStringify(id)]; // 250
|
||
|
lengthCur--; // 251
|
||
|
// 252
|
||
|
callbacks.removedAt( // 253
|
||
|
id, // 254
|
||
|
lastSeqArray[posOld[idStringify(id)]].item, // 255
|
||
|
prevPosition); // 256
|
||
|
} // 257
|
||
|
}); // 258
|
||
|
// 259
|
||
|
_.each(posNew, function (pos, idString) { // 260
|
||
|
var id = idParse(idString); // 261
|
||
|
if (_.has(posOld, idString)) { // 262
|
||
|
// specifically for primitive types, compare equality before // 263
|
||
|
// firing the 'changedAt' callback. otherwise, always fire it // 264
|
||
|
// because doing a deep EJSON comparison is not guaranteed to // 265
|
||
|
// work (an array can contain arbitrary objects, and 'transform' // 266
|
||
|
// can be used on cursors). also, deep diffing is not // 267
|
||
|
// necessarily the most efficient (if only a specific subfield // 268
|
||
|
// of the object is later accessed). // 269
|
||
|
var newItem = seqArray[pos].item; // 270
|
||
|
var oldItem = lastSeqArray[posOld[idString]].item; // 271
|
||
|
// 272
|
||
|
if (typeof newItem === 'object' || newItem !== oldItem) // 273
|
||
|
callbacks.changedAt(id, newItem, oldItem, pos); // 274
|
||
|
} // 275
|
||
|
}); // 276
|
||
|
}; // 277
|
||
|
// 278
|
||
|
seqChangedToEmpty = function (lastSeqArray, callbacks) { // 279
|
||
|
return []; // 280
|
||
|
}; // 281
|
||
|
// 282
|
||
|
seqChangedToArray = function (lastSeqArray, array, callbacks) { // 283
|
||
|
var idsUsed = {}; // 284
|
||
|
var seqArray = _.map(array, function (item, index) { // 285
|
||
|
var id; // 286
|
||
|
if (typeof item === 'string') { // 287
|
||
|
// ensure not empty, since other layers (eg DomRange) assume this as well // 288
|
||
|
id = "-" + item; // 289
|
||
|
} else if (typeof item === 'number' || // 290
|
||
|
typeof item === 'boolean' || // 291
|
||
|
item === undefined) { // 292
|
||
|
id = item; // 293
|
||
|
} else if (typeof item === 'object') { // 294
|
||
|
id = (item && item._id) || index; // 295
|
||
|
} else { // 296
|
||
|
throw new Error("{{#each}} doesn't support arrays with " + // 297
|
||
|
"elements of type " + typeof item); // 298
|
||
|
} // 299
|
||
|
// 300
|
||
|
var idString = idStringify(id); // 301
|
||
|
if (idsUsed[idString]) { // 302
|
||
|
if (typeof item === 'object' && '_id' in item) // 303
|
||
|
warn("duplicate id " + id + " in", array); // 304
|
||
|
id = Random.id(); // 305
|
||
|
} else { // 306
|
||
|
idsUsed[idString] = true; // 307
|
||
|
} // 308
|
||
|
// 309
|
||
|
return { _id: id, item: item }; // 310
|
||
|
}); // 311
|
||
|
// 312
|
||
|
return seqArray; // 313
|
||
|
}; // 314
|
||
|
// 315
|
||
|
seqChangedToCursor = function (lastSeqArray, cursor, callbacks) { // 316
|
||
|
var initial = true; // are we observing initial data from cursor? // 317
|
||
|
var seqArray = []; // 318
|
||
|
// 319
|
||
|
var observeHandle = cursor.observe({ // 320
|
||
|
addedAt: function (document, atIndex, before) { // 321
|
||
|
if (initial) { // 322
|
||
|
// keep track of initial data so that we can diff once // 323
|
||
|
// we exit `observe`. // 324
|
||
|
if (before !== null) // 325
|
||
|
throw new Error("Expected initial data from observe in order"); // 326
|
||
|
seqArray.push({ _id: document._id, item: document }); // 327
|
||
|
} else { // 328
|
||
|
callbacks.addedAt(document._id, document, atIndex, before); // 329
|
||
|
} // 330
|
||
|
}, // 331
|
||
|
changedAt: function (newDocument, oldDocument, atIndex) { // 332
|
||
|
callbacks.changedAt(newDocument._id, newDocument, oldDocument, // 333
|
||
|
atIndex); // 334
|
||
|
}, // 335
|
||
|
removedAt: function (oldDocument, atIndex) { // 336
|
||
|
callbacks.removedAt(oldDocument._id, oldDocument, atIndex); // 337
|
||
|
}, // 338
|
||
|
movedTo: function (document, fromIndex, toIndex, before) { // 339
|
||
|
callbacks.movedTo( // 340
|
||
|
document._id, document, fromIndex, toIndex, before); // 341
|
||
|
} // 342
|
||
|
}); // 343
|
||
|
initial = false; // 344
|
||
|
// 345
|
||
|
return [seqArray, observeHandle]; // 346
|
||
|
}; // 347
|
||
|
// 348
|
||
|
///////////////////////////////////////////////////////////////////////////////////
|
||
|
|
||
|
}).call(this);
|
||
|
|
||
|
|
||
|
/* Exports */
|
||
|
if (typeof Package === 'undefined') Package = {};
|
||
|
Package['observe-sequence'] = {
|
||
|
ObserveSequence: ObserveSequence
|
||
|
};
|
||
|
|
||
|
})();
|
||
|
|
||
|
//# sourceMappingURL=observe-sequence.js.map
|