////////////////////////////////////////////////////////////////////////// // // // This is a generated file. You can view the original // // source in your browser if your browser supports source maps. // // Source maps are supported by all recent versions of Chrome, Safari, // // and Firefox, and by Internet Explorer 11. // // // ////////////////////////////////////////////////////////////////////////// (function () { /* Imports */ var Meteor = Package.meteor.Meteor; var global = Package.meteor.global; var meteorEnv = Package.meteor.meteorEnv; var Tracker = Package.tracker.Tracker; var Deps = Package.tracker.Deps; var MongoID = Package['mongo-id'].MongoID; var DiffSequence = Package['diff-sequence'].DiffSequence; 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 () { if (ObserveSequence._suppressWarnings) { ObserveSequence._suppressWarnings--; } else { if (typeof console !== 'undefined' && console.warn) console.warn.apply(console, arguments); ObserveSequence._loggedWarnings++; } }; // isArray returns true for arrays of these types: // standard arrays: instanceof Array === true, _.isArray(arr) === true // vm generated arrays: instanceOf Array === false, _.isArray(arr) === true // subclassed arrays: instanceof Array === true, _.isArray(arr) === false // see specific tests function isArray(arr) { return arr instanceof Array || _.isArray(arr); } var idStringify = MongoID.idStringify; var idParse = MongoID.idParse; ObserveSequence = { _suppressWarnings: 0, _loggedWarnings: 0, // A mechanism similar to cursor.observe which receives a reactive // function returning a sequence type and firing appropriate callbacks // when the value changes. // // @param sequenceFunc {Function} a reactive function returning a // sequence type. The currently supported sequence types are: // Array, Cursor, and null. // // @param callbacks {Object} similar to a specific subset of // callbacks passed to `cursor.observe` // (http://docs.meteor.com/#observe), with minor variations to // support the fact that not all sequences contain objects with // _id fields. Specifically: // // * addedAt(id, item, atIndex, beforeId) // * changedAt(id, newItem, oldItem, atIndex) // * removedAt(id, oldItem, atIndex) // * movedTo(id, item, fromIndex, toIndex, beforeId) // // @returns {Object(stop: Function)} call 'stop' on the return value // to stop observing this sequence function. // // We don't make any assumptions about our ability to compare sequence // elements (ie, we don't assume EJSON.equals works; maybe there is extra // state/random methods on the objects) so unlike cursor.observe, we may // sometimes call changedAt() when nothing actually changed. // XXX consider if we *can* make the stronger assumption and avoid // no-op changedAt calls (in some cases?) // // XXX currently only supports the callbacks used by our // implementation of {{#each}}, but this can be expanded. // // XXX #each doesn't use the indices (though we'll eventually need // a way to get them when we support `@index`), but calling // `cursor.observe` causes the index to be calculated on every // callback using a linear scan (unless you turn it off by passing // `_no_indices`). Any way to avoid calculating indices on a pure // cursor observe like we used to? observe: function (sequenceFunc, callbacks) { var lastSeq = null; var activeObserveHandle = null; // 'lastSeqArray' contains the previous value of the sequence // we're observing. It is an array of objects with '_id' and // 'item' fields. 'item' is the element in the array, or the // document in the cursor. // // '_id' is whichever of the following is relevant, unless it has // already appeared -- in which case it's randomly generated. // // * if 'item' is an object: // * an '_id' field, if present // * otherwise, the index in the array // // * if 'item' is a number or string, use that value // // XXX this can be generalized by allowing {{#each}} to accept a // general 'key' argument which could be a function, a dotted // field name, or the special @index value. var lastSeqArray = []; // elements are objects of form {_id, item} var computation = Tracker.autorun(function () { var seq = sequenceFunc(); Tracker.nonreactive(function () { var seqArray; // same structure as `lastSeqArray` above. if (activeObserveHandle) { // If we were previously observing a cursor, replace lastSeqArray with // more up-to-date information. Then stop the old observe. lastSeqArray = _.map(lastSeq.fetch(), function (doc) { return {_id: doc._id, item: doc}; }); activeObserveHandle.stop(); activeObserveHandle = null; } if (!seq) { seqArray = seqChangedToEmpty(lastSeqArray, callbacks); } else if (isArray(seq)) { seqArray = seqChangedToArray(lastSeqArray, seq, callbacks); } else if (isStoreCursor(seq)) { var result /* [seqArray, activeObserveHandle] */ = seqChangedToCursor(lastSeqArray, seq, callbacks); seqArray = result[0]; activeObserveHandle = result[1]; } else { throw badSequenceError(); } diffArray(lastSeqArray, seqArray, callbacks); lastSeq = seq; lastSeqArray = seqArray; }); }); return { stop: function () { computation.stop(); if (activeObserveHandle) activeObserveHandle.stop(); } }; }, // Fetch the items of `seq` into an array, where `seq` is of one of the // sequence types accepted by `observe`. If `seq` is a cursor, a // dependency is established. fetch: function (seq) { if (!seq) { return []; } else if (isArray(seq)) { return seq; } else if (isStoreCursor(seq)) { return seq.fetch(); } else { throw badSequenceError(); } } }; var badSequenceError = function () { return new Error("{{#each}} currently only accepts " + "arrays, cursors or falsey values."); }; var isStoreCursor = function (cursor) { return cursor && _.isObject(cursor) && _.isFunction(cursor.observe) && _.isFunction(cursor.fetch); }; // Calculates the differences between `lastSeqArray` and // `seqArray` and calls appropriate functions from `callbacks`. // Reuses Minimongo's diff algorithm implementation. var diffArray = function (lastSeqArray, seqArray, callbacks) { var diffFn = Package['diff-sequence'].DiffSequence.diffQueryOrderedChanges; var oldIdObjects = []; var newIdObjects = []; var posOld = {}; // maps from idStringify'd ids var posNew = {}; // ditto var posCur = {}; var lengthCur = lastSeqArray.length; _.each(seqArray, function (doc, i) { newIdObjects.push({_id: doc._id}); posNew[idStringify(doc._id)] = i; }); _.each(lastSeqArray, function (doc, i) { oldIdObjects.push({_id: doc._id}); posOld[idStringify(doc._id)] = i; posCur[idStringify(doc._id)] = i; }); // Arrays can contain arbitrary objects. We don't diff the // objects. Instead we always fire 'changedAt' callback on every // object. The consumer of `observe-sequence` should deal with // it appropriately. diffFn(oldIdObjects, newIdObjects, { addedBefore: function (id, doc, before) { var position = before ? posCur[idStringify(before)] : lengthCur; if (before) { // If not adding at the end, we need to update indexes. // XXX this can still be improved greatly! _.each(posCur, function (pos, id) { if (pos >= position) posCur[id]++; }); } lengthCur++; posCur[idStringify(id)] = position; callbacks.addedAt( id, seqArray[posNew[idStringify(id)]].item, position, before); }, movedBefore: function (id, before) { if (id === before) return; var oldPosition = posCur[idStringify(id)]; var newPosition = before ? posCur[idStringify(before)] : lengthCur; // Moving the item forward. The new element is losing one position as it // was removed from the old position before being inserted at the new // position. // Ex.: 0 *1* 2 3 4 // 0 2 3 *1* 4 // The original issued callback is "1" before "4". // The position of "1" is 1, the position of "4" is 4. // The generated move is (1) -> (3) if (newPosition > oldPosition) { newPosition--; } // Fix up the positions of elements between the old and the new positions // of the moved element. // // There are two cases: // 1. The element is moved forward. Then all the positions in between // are moved back. // 2. The element is moved back. Then the positions in between *and* the // element that is currently standing on the moved element's future // position are moved forward. _.each(posCur, function (elCurPosition, id) { if (oldPosition < elCurPosition && elCurPosition < newPosition) posCur[id]--; else if (newPosition <= elCurPosition && elCurPosition < oldPosition) posCur[id]++; }); // Finally, update the position of the moved element. posCur[idStringify(id)] = newPosition; callbacks.movedTo( id, seqArray[posNew[idStringify(id)]].item, oldPosition, newPosition, before); }, removed: function (id) { var prevPosition = posCur[idStringify(id)]; _.each(posCur, function (pos, id) { if (pos >= prevPosition) posCur[id]--; }); delete posCur[idStringify(id)]; lengthCur--; callbacks.removedAt( id, lastSeqArray[posOld[idStringify(id)]].item, prevPosition); } }); _.each(posNew, function (pos, idString) { var id = idParse(idString); if (_.has(posOld, idString)) { // specifically for primitive types, compare equality before // firing the 'changedAt' callback. otherwise, always fire it // because doing a deep EJSON comparison is not guaranteed to // work (an array can contain arbitrary objects, and 'transform' // can be used on cursors). also, deep diffing is not // necessarily the most efficient (if only a specific subfield // of the object is later accessed). var newItem = seqArray[pos].item; var oldItem = lastSeqArray[posOld[idString]].item; if (typeof newItem === 'object' || newItem !== oldItem) callbacks.changedAt(id, newItem, oldItem, pos); } }); }; seqChangedToEmpty = function (lastSeqArray, callbacks) { return []; }; seqChangedToArray = function (lastSeqArray, array, callbacks) { var idsUsed = {}; var seqArray = _.map(array, function (item, index) { var id; if (typeof item === 'string') { // ensure not empty, since other layers (eg DomRange) assume this as well id = "-" + item; } else if (typeof item === 'number' || typeof item === 'boolean' || item === undefined || item === null) { id = item; } else if (typeof item === 'object') { id = (item && ('_id' in item)) ? item._id : index; } else { throw new Error("{{#each}} doesn't support arrays with " + "elements of type " + typeof item); } var idString = idStringify(id); if (idsUsed[idString]) { if (item && typeof item === 'object' && '_id' in item) warn("duplicate id " + id + " in", array); id = Random.id(); } else { idsUsed[idString] = true; } return { _id: id, item: item }; }); return seqArray; }; seqChangedToCursor = function (lastSeqArray, cursor, callbacks) { var initial = true; // are we observing initial data from cursor? var seqArray = []; var observeHandle = cursor.observe({ addedAt: function (document, atIndex, before) { if (initial) { // keep track of initial data so that we can diff once // we exit `observe`. if (before !== null) throw new Error("Expected initial data from observe in order"); seqArray.push({ _id: document._id, item: document }); } else { callbacks.addedAt(document._id, document, atIndex, before); } }, changedAt: function (newDocument, oldDocument, atIndex) { callbacks.changedAt(newDocument._id, newDocument, oldDocument, atIndex); }, removedAt: function (oldDocument, atIndex) { callbacks.removedAt(oldDocument._id, oldDocument, atIndex); }, movedTo: function (document, fromIndex, toIndex, before) { callbacks.movedTo( document._id, document, fromIndex, toIndex, before); } }); initial = false; return [seqArray, observeHandle]; }; /////////////////////////////////////////////////////////////////////////////////// }).call(this); /* Exports */ Package._define("observe-sequence", { ObserveSequence: ObserveSequence }); })();