Bumping version to 0.1.6

This commit is contained in:
Michael Cordingley
2014-02-28 10:21:47 -05:00
parent 8a58536275
commit 72c2011c91
3 changed files with 904 additions and 1 deletions
@@ -0,0 +1,902 @@
(function(root, factory) {
if(typeof exports === 'object') {
module.exports = factory(require('underscore'), require('backbone'));
}
else if(typeof define === 'function' && define.amd) {
define(['underscore', 'backbone'], factory);
}
else {
root.Backbone.Obscura = factory(root._, root.Backbone);
}
}(this, function(_, Backbone) {
var require=function(name){return {"backbone":Backbone,"underscore":_}[name];};
require=(function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);throw new Error("Cannot find module '"+o+"'")}var f=n[o]={exports:{}};t[o][0].call(f.exports,function(e){var n=t[o][1][e];return s(n?n:e)},f,f.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({"HusaU0":[function(require,module,exports){
var _ = require('underscore');
var Backbone = require('backbone');
var FilteredCollection = require('backbone-filtered-collection');
var SortedCollection = require('backbone-sorted-collection');
var PaginatedCollection = require('backbone-paginated-collection');
var proxyCollection = require('backbone-collection-proxy');
var proxyEvents = require('./src/proxy-events.js');
function Obscura(superset, options) {
this._superset = superset;
this._filtered = new FilteredCollection(superset, options);
this._sorted = new SortedCollection(this._filtered, options);
this._paginated = new PaginatedCollection(this._sorted, options);
proxyCollection(this._paginated, this);
proxyEvents.call(this, this._filtered, filteredEvents);
proxyEvents.call(this, this._sorted, sortedEvents);
proxyEvents.call(this, this._paginated, paginatedEvents);
}
var methods = {
superset: function() {
return this._superset;
},
getFilteredLength: function() {
return this._filtered.length;
},
removeTransforms: function() {
this._filtered.resetFilters();
this._sorted.removeSort();
this._paginated.removePagination();
return this;
},
destroy: function() {
this.stopListening();
this._filtered.destroy();
this._sorted.destroy();
this._paginated.destroy();
this.length = 0;
this.trigger('obscura:destroy');
}
};
// Methods on `this._filtered` we will expose to the outside world
var filteredMethods = [
'filterBy', 'removeFilter', 'resetFilters', 'refilter', 'hasFilter',
'getFilters'
];
// Events fired from `this._filtered` that we will forward
var filteredEvents = [
'filtered:add', 'filtered:remove', 'filtered:reset'
];
// Methods on `this._sorted` we will expose to the outside world
var sortedMethods = [ 'setSort', 'reverseSort', 'removeSort' ];
// Events fired from `this._sorted` that we will forward
var sortedEvents = [
'sorted:add', 'sorted:remove'
];
// Methods on `this._paginated` we will expose to the outside world
var paginatedMethods = [
'setPerPage', 'setPage', 'getPerPage', 'getNumPages', 'getPage',
'hasNextPage', 'hasPrevPage', 'nextPage', 'prevPage', 'movePage',
'removePagination', 'firstPage', 'lastPage'
];
// Events fired from `this._paginated` that we will forward
var paginatedEvents = [
'paginated:change:perPage', 'paginated:change:page', 'paginated:change:numPages'
];
// Extend obscura with each of the above methods, passing the call to the underlying
// collection.
//
// The return value is checked because some of the methods return `this` to allow
// chaining, and returning the internal collection would break the abstraction. In
// the cases where it would return the internal collection, we can return a reference
// to the Obscura proxy, which gives it the expected behavior.
_.each(filteredMethods, function(method) {
methods[method] = function() {
var result = FilteredCollection.prototype[method].apply(this._filtered, arguments);
return result === this._filtered ? this : result;
};
});
_.each(paginatedMethods, function(method) {
methods[method] = function() {
var result = PaginatedCollection.prototype[method].apply(this._paginated, arguments);
return result === this._paginated ? this : result;
};
});
_.each(sortedMethods, function(method) {
methods[method] = function() {
var result = SortedCollection.prototype[method].apply(this._sorted, arguments);
return result === this._sorted ? this : result;
};
});
_.extend(Obscura.prototype, methods, Backbone.Events);
// Expose the other proxy types so that the user can use them on their own if they want
Obscura.FilteredCollection = FilteredCollection;
Obscura.SortedCollection = SortedCollection;
Obscura.PaginatedCollection = PaginatedCollection;
module.exports = Obscura;
},{"./src/proxy-events.js":9,"backbone":false,"backbone-collection-proxy":3,"backbone-filtered-collection":4,"backbone-paginated-collection":6,"backbone-sorted-collection":7,"underscore":false}],"obscura":[function(require,module,exports){
module.exports=require('HusaU0');
},{}],3:[function(require,module,exports){
var _ = require('underscore');
var Backbone = require('backbone');
// Methods in the collection prototype that we won't expose
var blacklistedMethods = [
"_onModelEvent", "_prepareModel", "_removeReference", "_reset", "add",
"initialize", "sync", "remove", "reset", "set", "push", "pop", "unshift",
"shift", "sort", "parse", "fetch", "create", "model", "off", "on",
"listenTo", "listenToOnce", "bind", "trigger", "once", "stopListening"
];
var eventWhiteList = [
'add', 'remove', 'reset', 'sort', 'destroy'
];
function proxyCollection(from, target) {
function updateLength() {
target.length = from.length;
}
function pipeEvents(eventName) {
var args = _.toArray(arguments);
var isChangeEvent = eventName === 'change' ||
eventName.slice(0, 7) === 'change:';
// In the case of a `reset` event, the Collection.models reference
// is updated to a new array, so we need to update our reference.
if (eventName === 'reset') {
target.models = from.models;
}
if (_.contains(eventWhiteList, eventName)) {
if (_.contains(['add', 'remove', 'destory'], eventName)) {
args[2] = target;
} else if (_.contains(['reset', 'sort'], eventName)) {
args[1] = target;
}
target.trigger.apply(this, args);
} else if (isChangeEvent) {
// In some cases I was seeing change events fired after the model
// had already been removed from the collection.
if (target.contains(args[1])) {
target.trigger.apply(this, args);
}
}
}
var methods = {};
_.each(_.functions(Backbone.Collection.prototype), function(method) {
if (!_.contains(blacklistedMethods, method)) {
methods[method] = function() {
return from[method].apply(from, arguments);
};
}
});
_.extend(target, Backbone.Events, methods);
target.listenTo(from, 'all', updateLength);
target.listenTo(from, 'all', pipeEvents);
target.models = from.models;
updateLength();
return target;
}
module.exports = proxyCollection;
},{"backbone":false,"underscore":false}],4:[function(require,module,exports){
var _ = require('underscore');
var Backbone = require('backbone');
var proxyCollection = require('backbone-collection-proxy');
var createFilter = require('./src/create-filter.js');
// Beware of `this`
// All of the following functions are meant to be called in the context
// of the FilteredCollection object, but are not public functions.
function invalidateCache() {
this._filterResultCache = {};
}
function invalidateCacheForFilter(filterName) {
for (var cid in this._filterResultCache) {
if (this._filterResultCache.hasOwnProperty(cid)) {
delete this._filterResultCache[cid][filterName];
}
}
}
function addFilter(filterName, filterObj) {
// If we've already had a filter of this name, we need to invalidate
// any and all of the cached results
if (this._filters[filterName]) {
invalidateCacheForFilter.call(this, filterName);
}
this._filters[filterName] = filterObj;
this.trigger('filtered:add', filterName);
}
function removeFilter(filterName) {
delete this._filters[filterName];
invalidateCacheForFilter.call(this, filterName);
this.trigger('filtered:remove', filterName);
}
function execFilterOnModel(model) {
if (!this._filterResultCache[model.cid]) {
this._filterResultCache[model.cid] = {};
}
var cache = this._filterResultCache[model.cid];
for (var filterName in this._filters) {
if (this._filters.hasOwnProperty(filterName)) {
// if we haven't already calculated this, calculate it and cache
if (!cache.hasOwnProperty(filterName)) {
cache[filterName] = this._filters[filterName].fn(model);
}
if (!cache[filterName]) {
return false;
}
}
}
return true;
}
function execFilter() {
var filtered = [];
// Filter the collection
if (this._superset) {
filtered = this._superset.filter(_.bind(execFilterOnModel, this));
}
this._collection.reset(filtered);
this.length = this._collection.length;
}
function onAddChange(model) {
// reset the cached results
this._filterResultCache[model.cid] = {};
if (execFilterOnModel.call(this, model)) {
if (!this._collection.get(model.cid)) {
var index = this.superset().indexOf(model);
// Find the index at which to insert the model in the
// filtered collection by finding the index of the
// previous non-filtered model in the filtered collection
var filteredIndex = null;
for (var i = index - 1; i >= 0; i -= 1) {
if (this.contains(this.superset().at(i))) {
filteredIndex = this.indexOf(this.superset().at(i)) + 1;
break;
}
}
filteredIndex = filteredIndex || 0;
this._collection.add(model, { at: filteredIndex });
}
} else {
if (this._collection.get(model.cid)) {
this._collection.remove(model);
}
}
this.length = this._collection.length;
}
// This fires on 'change:[attribute]' events. We only want to
// remove this model if it fails the test, but not add it if
// it does. If we remove it, it will prevent the 'change'
// events from being forwarded, and if we add it, it will cause
// an unneccesary 'change' event to be forwarded without the
// 'change:[attribute]' that goes along with it.
function onModelAttributeChange(model) {
// reset the cached results
this._filterResultCache[model.cid] = {};
if (!execFilterOnModel.call(this, model)) {
if (this._collection.get(model.cid)) {
this._collection.remove(model);
}
}
}
function onAll(eventName, model, value) {
if (eventName.slice(0, 7) === "change:") {
onModelAttributeChange.call(this, arguments[1]);
}
}
function onModelRemove(model) {
if (this.contains(model)) {
this._collection.remove(model);
}
this.length = this._collection.length;
}
function Filtered(superset) {
// Save a reference to the original collection
this._superset = superset;
// The idea is to keep an internal backbone collection with the filtered
// set, and expose limited functionality.
this._collection = new Backbone.Collection(superset.toArray());
proxyCollection(this._collection, this);
// Set up the filter data structures
this.resetFilters();
this.listenTo(this._superset, 'reset sort', execFilter);
this.listenTo(this._superset, 'add change', onAddChange);
this.listenTo(this._superset, 'remove', onModelRemove);
this.listenTo(this._superset, 'all', onAll);
}
var methods = {
defaultFilterName: '__default',
filterBy: function(filterName, filter) {
// Allow the user to skip the filter name if they're only using one filter
if (!filter) {
filter = filterName;
filterName = this.defaultFilterName;
}
addFilter.call(this, filterName, createFilter(filter));
execFilter.call(this);
return this;
},
removeFilter: function(filterName) {
if (!filterName) {
filterName = this.defaultFilterName;
}
removeFilter.call(this, filterName);
execFilter.call(this);
return this;
},
resetFilters: function() {
this._filters = {};
invalidateCache.call(this);
this.trigger('filtered:reset');
execFilter.call(this);
return this;
},
superset: function() {
return this._superset;
},
refilter: function(arg) {
if (typeof arg === "object" && arg.cid) {
// is backbone model, refilter that one
onAddChange.call(this, arg);
} else {
// refilter everything
invalidateCache.call(this);
execFilter.call(this);
}
return this;
},
getFilters: function() {
return _.keys(this._filters);
},
hasFilter: function(name) {
return _.contains(this.getFilters(), name);
},
destroy: function() {
this.stopListening();
this._collection.reset([]);
this._superset = this._collection;
this.length = 0;
this.trigger('filtered:destroy');
}
};
// Build up the prototype
_.extend(Filtered.prototype, methods, Backbone.Events);
module.exports = Filtered;
},{"./src/create-filter.js":5,"backbone":false,"backbone-collection-proxy":3,"underscore":false}],5:[function(require,module,exports){
var _ = require('underscore');
// Converts a key and value into a function that accepts a model
// and returns a boolean.
function convertKeyValueToFunction(key, value) {
return function(model) {
return model.get(key) === value;
};
}
// Converts a key and an associated filter function into a function
// that accepts a model and returns a boolean.
function convertKeyFunctionToFunction(key, fn) {
return function(model) {
return fn(model.get(key));
};
}
function createFilterObject(filterFunction, keys) {
// Make sure the keys value is either an array or null
if (!_.isArray(keys)) {
keys = null;
}
return { fn: filterFunction, keys: keys };
}
// Accepts an object in the form of:
//
// {
// key: value,
// key: function(val) { ... }
// }
//
// and turns it into a function that accepts a model an returns a
// boolean + a list of the keys that the function depends on.
function createFilterFromObject(filterObj) {
var keys = _.keys(filterObj);
var filterFunctions = _.map(keys, function(key) {
var val = filterObj[key];
if (_.isFunction(val)) {
return convertKeyFunctionToFunction(key, val);
}
return convertKeyValueToFunction(key, val);
});
// Iterate through each of the generated filter functions. If any
// are false, kill the computation and return false. The function
// is only true if all of the subfunctions are true.
var filterFunction = function(model) {
for (var i = 0; i < filterFunctions.length; i++) {
if (!filterFunctions[i](model)) {
return false;
}
}
return true;
};
return createFilterObject(filterFunction, keys);
}
// Expects one of the following:
//
// - A filter function that accepts a model + (optional) array of
// keys to listen to changes for or null)
// - An object describing a filter
function createFilter(filter, keys) {
// This must go first because _.isObject(fn) === true
if (_.isFunction(filter)) {
return createFilterObject(filter, keys);
}
// If the filter is an object describing a filter, generate the
// appropriate function.
if (_.isObject(filter)) {
return createFilterFromObject(filter);
}
}
module.exports = createFilter;
},{"underscore":false}],6:[function(require,module,exports){
var _ = require('underscore');
var Backbone = require('backbone');
var proxyCollection = require('backbone-collection-proxy');
function getPageLimits() {
var start = this.getPage() * this.getPerPage();
var end = start + this.getPerPage();
return [start, end];
}
function updatePagination() {
var pages = getPageLimits.call(this);
this._collection.reset(this.superset().slice(pages[0], pages[1]));
}
function updateNumPages() {
var currentNumPages = this._totalPages;
var length = this.superset().length;
var perPage = this.getPerPage();
// If the # of objects can be exactly divided by the number
// of pages, it would leave an empty last page if we took
// the floor.
var totalPages = length % perPage === 0 ?
(length / perPage) : Math.floor(length / perPage) + 1;
var numPagesChanged = this._totalPages !== totalPages;
this._totalPages = totalPages;
if (numPagesChanged) {
this.trigger('paginated:change:numPages', { numPages: totalPages });
}
// Test to see if we are past the last page, and if so,
// move back. Return true so that we can test to see if
// this happened.
if (this.getPage() >= totalPages) {
this.setPage(totalPages - 1);
return true;
}
}
function recalculatePagination() {
if (updateNumPages.call(this)) { return; }
updatePagination.call(this);
}
// Given two arrays of backbone models, with at most one model added
// and one model removed from each, return the model in arrayA that
// is not in arrayB or undefined.
function difference(arrayA, arrayB) {
var maxLength = _.max([ arrayA.length, arrayB.length ]);
for (var i = 0, j = 0; i < maxLength; i += 1, j += 1) {
if (arrayA[i] !== arrayB[j]) {
if (arrayB[i-1] === arrayA[i]) {
j -= 1;
} else if (arrayB[i+1] === arrayA[i]) {
j += 1;
} else {
return arrayA[i];
}
}
}
}
function onAddRemove(model, collection, options) {
if (updateNumPages.call(this)) { return; }
var pages = getPageLimits.call(this);
var start = pages[0], end = pages[1];
// We are only adding and removing at most one model at a time,
// so we can find just those two models. We could probably rewrite
// `collectionDifference` to only make on pass instead of two. This
// is a bottleneck on the total size of collections. I was getting
// slow unit tests around 30,000 models / page in Firefox.
var toAdd = difference(this.superset().slice(start, end), this._collection.toArray());
var toRemove = difference(this._collection.toArray(), this.superset().slice(start, end));
if (toRemove) {
this._collection.remove(toRemove);
}
if (toAdd) {
this._collection.add(toAdd, {
at: this.superset().indexOf(toAdd) - start
});
}
}
function Paginated(superset, options) {
// Save a reference to the original collection
this._superset = superset;
// The idea is to keep an internal backbone collection with the paginated
// set, and expose limited functionality.
this._collection = new Backbone.Collection(superset.toArray());
this._page = 0;
this.setPerPage((options && options.perPage) ? options.perPage : null);
proxyCollection(this._collection, this);
this.listenTo(this._superset, 'add remove', onAddRemove);
this.listenTo(this._superset, 'reset sort', recalculatePagination);
}
var methods = {
removePagination: function() {
this.setPerPage(null);
return this;
},
setPerPage: function(perPage) {
this._perPage = perPage;
recalculatePagination.call(this);
this.setPage(0);
this.trigger('paginated:change:perPage', {
perPage: perPage,
numPages: this.getNumPages()
});
return this;
},
setPage: function(page) {
// The lowest page we could set
var lowerLimit = 0;
// The highest page we could set
var upperLimit = this.getNumPages() - 1;
// If the page is higher or lower than these limits,
// set it to the limit.
page = page > lowerLimit ? page : lowerLimit;
page = page < upperLimit ? page : upperLimit;
page = page < 0 ? 0 : page;
this._page = page;
updatePagination.call(this);
this.trigger('paginated:change:page', { page: page });
return this;
},
getPerPage: function() {
return this._perPage || this.superset().length || 1;
},
getNumPages: function() {
return this._totalPages;
},
getPage: function() {
return this._page;
},
hasNextPage: function() {
return this.getPage() < this.getNumPages() - 1;
},
hasPrevPage: function() {
return this.getPage() > 0;
},
nextPage: function() {
this.movePage(1);
return this;
},
prevPage: function() {
this.movePage(-1);
return this;
},
firstPage: function() {
this.setPage(0);
},
lastPage: function() {
this.setPage(this.getNumPages() - 1);
},
movePage: function(delta) {
this.setPage(this.getPage() + delta);
return this;
},
superset: function() {
return this._superset;
},
destroy: function() {
this.stopListening();
this._collection.reset([]);
this._superset = this._collection;
this._page = 0;
this._totalPages = 0;
this.length = 0;
this.trigger('paginated:destroy');
}
};
// Build up the prototype
_.extend(Paginated.prototype, methods, Backbone.Events);
module.exports = Paginated;
},{"backbone":false,"backbone-collection-proxy":3,"underscore":false}],7:[function(require,module,exports){
var _ = require('underscore');
var Backbone =require('backbone');
var proxyCollection = require('backbone-collection-proxy');
var reverseSortedIndex = require('./src/reverse-sorted-index.js');
function lookupIterator(value) {
return _.isFunction(value) ? value : function(obj){ return obj.get(value); };
}
function modelInsertIndex(model) {
if (!this._comparator) {
return this._superset.indexOf(model);
} else {
if (!this._reverse) {
return _.sortedIndex(this._collection.toArray(), model, lookupIterator(this._comparator));
} else {
return reverseSortedIndex(this._collection.toArray(), model, lookupIterator(this._comparator));
}
}
}
function onAdd(model) {
var index = modelInsertIndex.call(this, model);
this._collection.add(model, { at: index });
}
function onRemove(model) {
if (this.contains(model)) {
this._collection.remove(model);
}
}
function onChange(model) {
if (this.contains(model) && this._collection.indexOf(model) !== modelInsertIndex.call(this, model)) {
this._collection.remove(model);
onAdd.call(this, model);
}
}
function sort() {
if (!this._comparator) {
this._collection.reset(this._superset.toArray());
return;
}
var newOrder = this._superset.sortBy(this._comparator);
this._collection.reset(this._reverse ? newOrder.reverse() : newOrder);
}
function Sorted(superset) {
// Save a reference to the original collection
this._superset = superset;
this._reverse = false;
this._comparator = null;
// The idea is to keep an internal backbone collection with the paginated
// set, and expose limited functionality.
this._collection = new Backbone.Collection(superset.toArray());
proxyCollection(this._collection, this);
this.listenTo(this._superset, 'add', onAdd);
this.listenTo(this._superset, 'remove', onRemove);
this.listenTo(this._superset, 'change', onChange);
this.listenTo(this._superset, 'reset', sort);
}
var methods = {
setSort: function(comparator, direction) {
this._reverse = direction === 'desc' ? true : false;
this._comparator = comparator;
sort.call(this);
if (!comparator) {
this.trigger('sorted:remove');
} else {
this.trigger('sorted:add');
}
return this;
},
reverseSort: function() {
this._reverse = !this._reverse;
sort.call(this);
return this;
},
removeSort: function() {
this.setSort();
return this;
},
superset: function() {
return this._superset;
},
destroy: function() {
this.stopListening();
this._collection.reset([]);
this._superset = this._collection;
this.length = 0;
this.trigger('sorted:destroy');
}
};
// Build up the prototype
_.extend(Sorted.prototype, methods, Backbone.Events);
module.exports = Sorted;
},{"./src/reverse-sorted-index.js":8,"backbone":false,"backbone-collection-proxy":3,"underscore":false}],8:[function(require,module,exports){
var _ = require('underscore');
// Underscore provides a .sortedIndex function that works
// when sorting ascending based on a function or a key, but there's no
// way to do the same thing when sorting descending. This is a slight
// modification of the underscore / backbone code to do the same thing
// but descending.
function lookupIterator(value) {
return _.isFunction(value) ? value : function(obj){ return obj[value]; };
}
function reverseSortedIndex(array, obj, iterator, context) {
iterator = iterator == null ? _.identity : lookupIterator(iterator);
var value = iterator.call(context, obj);
var low = 0, high = array.length;
while (low < high) {
var mid = (low + high) >>> 1;
iterator.call(context, array[mid]) < value ? high = mid : low = mid + 1;
}
return low;
}
module.exports = reverseSortedIndex;
},{"underscore":false}],9:[function(require,module,exports){
var _ = require('underscore');
function proxyEvents(from, eventNames) {
_.each(eventNames, function(eventName) {
this.listenTo(from, eventName, function() {
var args = _.toArray(arguments);
args.unshift(eventName);
this.trigger.apply(this, args);
});
}, this);
}
module.exports = proxyEvents;
},{"underscore":false}]},{},[])
return require('obscura');
}));
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "backbone.obscura",
"filename": "backbone.obscura.min.js",
"version": "0.1.5",
"version": "0.1.6",
"description": "A read-only proxy of a Backbone.Collection that can easily be filtered, sorted, and paginated.",
"homepage": "https://github.com/jmorrell/backbone.obscura",
"main": "index.js",