Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP

Comparing changes

Choose two branches to see what's changed or to start a new pull request. If you need to, you can also compare across forks.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also compare across forks.
base: 8399d6307f
...
compare: df1bae359e
Checking mergeability… Don't worry, you can still create the pull request.
  • 9 commits
  • 18 files changed
  • 0 commit comments
  • 2 contributors
Commits on Mar 22, 2013
@creationix creationix Get basic client-server interaction working f70a464
@creationix creationix Lint cleanup 1e499a3
@creationix creationix Remove placeholder data for now, it clutters the output fab030d
@jugglinmike jugglinmike Correct error in compatability layer 2319a60
@jugglinmike jugglinmike Begin modularizing GUI
This version, while technically functional, still uses some global state
and tightly-coupled componenets.
6da63d4
@jugglinmike jugglinmike Update Lodash to Underscore.js compatability build 7f9f554
@jugglinmike jugglinmike Correct error in compatability layer 7deb40a
@jugglinmike jugglinmike Re-factor UI
Split component parts into specialized views and make some simplistic
style improvements.
13b9546
Commits on Mar 25, 2013
@jugglinmike jugglinmike Begin client-side integration of Transport
Still to do:

- Extend UI to support user log in
- Extend UI to support user name specification in calls
- Extend UI to support accepting/rejecting calls
- Implement custom message to properly support hanging up
- Implement custom message to support sharing of ICE candidates
df1bae3
View
94 prototype/lib/transport.js
@@ -82,6 +82,7 @@
}
// Plug-in the new socket.
+ var deferred = Q.defer();
this.socket = socket;
var transport = this;
socket.onmessage = function (evt) {
@@ -98,24 +99,51 @@
else if (message.reply) {
transport.onReply(message.reply);
}
- throw new Error('Unknown message type');
+ else {
+ throw new Error('Unknown message type: ' + json);
+ }
}
catch (err) {
transport.emit('error', err);
}
};
socket.onclose = function (evt) {
- if (evt.wasClean) {
- transport.emit('closed', evt.reason);
- }
- else {
+ transport.emit('closed', evt.reason);
+ if (!evt.wasClean) {
transport.emit('error', new Error(evt.reason));
}
+ deferred.reject();
};
socket.onopen = function () {
transport.emit('opened');
+ deferred.resolve();
};
- return this.emit('open');
+ this.emit('open');
+ return deferred.promise;
+ };
+
+ Transport.prototype.result = function (request, result, isReply) {
+ var message = {
+ $id: request.$id,
+ $method: request.$method,
+ $timestamp: Date.now() / 1000
+ };
+ for (var key in result) {
+ if (key[0] !== '$') {
+ message[key] = result[key];
+ }
+ }
+ if (isReply) {
+ console.log('reply', message);
+ message = {reply: message};
+ }
+ else {
+ console.log('result', message);
+ message = {result: message};
+ }
+
+ var json = JSON.stringify(message);
+ this.socket.send(json);
};
Transport.prototype.request = function (method, request) {
@@ -123,19 +151,18 @@
// TODO: Use real secure random in real app.
var id;
do {
- id = (Math.random() * 0x10000000).toString(36);
+ id = (Math.random() * 0x100000000).toString(32);
} while (id in this.pending);
// Generate the metadata for the request
var message = {
- $domain: 'hookflash.com',
$id: id,
- $handler: 'peer-finder',
$method: method
};
for (var key in request) {
message[key] = request[key];
}
+ console.log('request', message);
message = {request: message};
var deferred = Q.defer();
@@ -157,20 +184,34 @@
};
Transport.prototype.onRequest = function (request) {
+ console.log('onRequest', request);
var handler = this.api[request.$method];
+ var isReply = false;
+ if (!handler && request.$method === 'peer-location-find') {
+ handler = this.api.invite;
+ isReply = true;
+ }
if (!handler) {
throw new Error('Unknown request method: ' + request.$method);
}
- handler(request).then(function (result) {
- console.log(result);
- throw new Error('TODO: send the result back to the caller');
- }, function (err) {
+ var transport = this;
+ Q.fcall(handler, request, this).then(function (result) {
+ if (!result) {
+ result = {};
+ }
+ transport.result(request, result, isReply);
+ }).fail(function (err) {
// TODO: What should I do here?
- throw err;
+ // What is the proper way to communicate this failure across the wire?
+ console.error(err);
});
};
Transport.prototype.onResult = function (result) {
+ console.log('onResult', result);
+ if (result.$method === 'peer-location-find') {
+ return;
+ }
var deferred = this.pending[result.$id];
if (!deferred) {
throw new Error('Received result with invalid $id: ' + result.$id);
@@ -180,11 +221,28 @@
};
Transport.prototype.onReply = function (reply) {
- var handler = this.api.reply;
- if (!handler) {
- throw new Error('Missing reply api handler');
+ console.log('onReply', reply);
+ var deferred = this.pending[reply.$id];
+ if (!deferred) {
+ return;
+ // We don't care about this reply anymore
}
- handler(reply);
+ // TODO: Allow multiple replies somehow
+ delete this.pending[reply.$id];
+ deferred.resolve(reply);
+ };
+
+ Transport.prototype.sessionCreate = function (username) {
+ return this.request('session-create', {
+ username: username
+ });
+ };
+
+ Transport.prototype.peerLocationFind = function (username, blob) {
+ return this.request('peer-location-find', {
+ username: username,
+ blob: blob
+ });
};
return Transport;
View
13 prototype/public/index.html
@@ -1,17 +1,14 @@
<!doctype html>
<html>
<head>
- <title>Prototype</title>
+ <title>OpenPeer Prototype</title>
+ <link href="styles/reset.css" rel="stylesheet" type="text/css"></link>
<link href="styles/main.css" rel="stylesheet" type="text/css"></link>
</head>
<body>
- <h1>WebRTC Demo using WebSockets</h1>
- <video id="webrtc-source-vid"></video>
- <button type="button" id="start-video">Start video</button>
- <button type="button" id="stop-video">Stop video</button>
- <video id="webrtc-remote-vid"></video>
- <button type="button" id="connect">Connect</button>
- <button type="button" id="hang-up">Hang Up</button>
+ <div id="app" class="cf">
+ <h1>OpenPeer Prototype</h1>
+ </div>
<script src="scripts/lib/require.js" data-built-src="scripts/dist/op.js" data-main="scripts/require-config"></script>
</body>
</html>
View
208 prototype/public/scripts/app.js
@@ -1,10 +1,10 @@
require([
- 'modules/nder', 'modules/pc', 'modules/gum-compat', 'jquery'
- ], function(Nder, PC, gum, $) {
+ 'modules/transport', 'modules/pc', 'modules/layout', 'backbone'
+ ], function(Transport, PC, Layout, Backbone) {
'use strict';
var config = {
- socketServer: window.location.host,
+ socketServer: 'ws://' + window.location.host,
pcConfig: {
iceServers: [
{ url: 'stun:stun.l.google.com:19302' },
@@ -12,65 +12,21 @@ require([
]
}
};
- var localStream = null;
+ // TODO: Fetch contacts from remote identitiy provider
+ var contacts = [
+ { name: 'creationix' },
+ { name: 'robin' },
+ { name: 'erik' },
+ { name: 'lawrence' },
+ { name: 'cassie' },
+ { name: 'jugglinmike' }
+ ];
var mediaConstraints = {
mandatory: {
OfferToReceiveAudio: true,
OfferToReceiveVideo: true
}
};
- var sourceVid = document.getElementById('webrtc-source-vid');
- var remoteVid = document.getElementById('webrtc-remote-vid');
- var $cache = {
- startVideo: $('#start-video'),
- stopVideo: $('#stop-video'),
- connect: $('#connect'),
- hangUp: $('#hang-up')
- };
- var handlers = {
- gum: {
- success: function(stream) {
- localStream = stream;
- gum.playStream(sourceVid, stream);
- },
- failure: function(error) {
- console.error('An error occurred: [CODE ' + error.code + ']');
- }
- },
- user: {
- startVideo: function() {
- gum.getUserMedia({
- video: true,
- audio: true
- }, handlers.gum.success, handlers.gum.failure);
- },
- stopVideo: function() {
- gum.stopStream(sourceVid);
- },
- connect: function() {
- if (!pc.isActive() && localStream && nder.is('open')) {
- pc.init(config.pcConfig);
- pc.addStream(localStream);
- pc.createOffer(
- setLocalAndSendMessage,
- createOfferFailed,
- mediaConstraints);
- } else {
- alert('Local stream not running yet - try again.');
- }
- },
- hangUp: function() {
- console.log('Hang up.');
- nder.send({ type: 'bye' });
- pc.destroy();
- }
- }
- };
- var setLocalAndSendMessage = function(sessionDescription) {
- this.setLocalDescription(sessionDescription);
- console.log('Sending SDP:', sessionDescription);
- nder.send(sessionDescription);
- };
function createOfferFailed() {
console.error('Create Answer failed');
@@ -81,61 +37,113 @@ require([
}
var pc = new PC();
+ var layout = new Layout({
+ el: '#app',
+ contacts: new Backbone.Collection(contacts)
+ });
+ layout.render();
+ layout.on('connectRequest', function(stream) {
+ // TODO: Derive target user from application state
+ var targetUser = 'creationix';
+ if (!pc.isActive() && transport.state === 'OPEN') {
+
+ pc.init(config.pcConfig);
+ pc.addStream(stream);
+ pc.createOffer(
+ function(sessionDescription) {
+ this.setLocalDescription(sessionDescription);
+ transport.peerLocationFind(targetUser, {
+ session: sessionDescription,
+ userName: userName
+ });
+ },
+ createOfferFailed,
+ mediaConstraints);
+ }
+ });
+ layout.on('hangup', function() {
+ // TODO: implement `Transport#bye` method (or similar)
+ // transport.bye();
+ pc.destroy();
+ });
pc.on('addstream', function(stream) {
console.log('Remote stream added');
- console.log(arguments);
- gum.playStream(remoteVid, stream);
+ layout.playRemoteStream(stream);
});
pc.on('removestream', function() {
console.log('Remove remote stream');
- gum.stopStream(remoteVid);
+ layout.stopRemoteStream();
});
pc.on('ice', function(msg) {
console.log('Sending ICE candidate:', msg);
- nder.send(msg);
+ // TODO: implement `Transport#sendIce` method (or similar)
+ // transport.sendIce(msg);
});
- var nder = new Nder({
- socketAddr: config.socketServer,
- handlers: {
- offer: function(msg) {
- console.log('Received offer...');
- if (!pc.isActive()) {
- pc.init(config.pcConfig);
- pc.addStream(localStream);
- }
- console.log('Creating remote session description:', msg);
- pc.setRemoteDescription(msg);
- console.log('Sending answer...');
- pc.createAnswer(setLocalAndSendMessage,
- createAnswerFailed, mediaConstraints);
- },
- answer: function(msg) {
- if (!pc.isActive()) {
- return;
- }
- console.log('Received answer. Setting remote session description:',
- msg);
- pc.setRemoteDescription(msg);
- },
- candidate: function(msg) {
- if (!pc.isActive()) {
- return;
- }
- console.log('Received ICE candidate:', msg);
- pc.addIceCandidate(msg);
- },
- bye: function() {
- if (!pc.isActive()) {
- return;
- }
- console.log('Received bye');
- pc.destroy();
+ var transport = new Transport({
+ invite: function(request) {
+ var blob = request && request.username && request.username.blob;
+ var remoteSession;
+ if (!blob) {
+ console.error('No blob found. Ignoring invite.');
+ return;
+ }
+ remoteSession = blob.session;
+ if (!remoteSession) {
+ console.error('Remote session not specified. Ignoring invite.');
+ return;
}
+
+ // TODO: Prompt user to accept/reject call (instead of blindly accepting)
+ // and move following logic into "Accept" handler.
+ console.log('Receiving call from ' + blob.userName +
+ '. Would you like to answer?');
+
+ if (!pc.isActive()) {
+ pc.init(config.pcConfig);
+ // TODO: Refactor so transport is not so tightly-coupled to the layout.
+ // This should also allow recieving calls without sharing the local
+ // stream.
+ pc.addStream(layout.localStreamView.getStream());
+ }
+ console.log('Creating remote session description:', remoteSession);
+ pc.setRemoteDescription(remoteSession);
+ console.log('Sending answer...');
+ pc.createAnswer(function(sessionDescription) {
+ this.setLocalDescription(sessionDescription);
+ },
+ createAnswerFailed, mediaConstraints);
}
+ // TODO: Implement `ice` message (or similar)
+ /*ice: function(candidate) {
+ if (!pc.isActive()) {
+ return;
+ }
+ console.log('Received ICE candidate:', candidate);
+ pc.addIceCandidate(candidate);
+ }*/
});
- $cache.startVideo.on('click', handlers.user.startVideo);
- $cache.stopVideo.on('click', handlers.user.stopVideo);
- $cache.connect.on('click', handlers.user.connect);
- $cache.hangUp.on('click', handlers.user.hangUp);
+ // Infer username from 'username' query string parameter (default to
+ // 'creationx') and immediately initiate a connection.
+ // TODO: First prompt user for name, then initiate a connection.
+ var userName = 'creationix';
+ window.location.search
+ // Remove leading '?'
+ .slice(1)
+ .split('&')
+ .forEach(function(pair) {
+ pair = pair.split('=');
+ if (pair[0] === 'username') {
+ userName = pair[1];
+ }
+ });
+ transport.open(new WebSocket(config.socketServer))
+ .then(function() {
+ return transport.sessionCreate(userName);
+ })
+ .then(function() {
+ console.log('Logged in!');
+ // TODO: Update UI to reflect logged-in state.
+ }, console.error.bind(console));
+
});
View
868 prototype/public/scripts/lib/backbone.layoutmanager.js
@@ -0,0 +1,868 @@
+/*!
+ * backbone.layoutmanager.js v0.8.6
+ * Copyright 2013, Tim Branyen (@tbranyen)
+ * backbone.layoutmanager.js may be freely distributed under the MIT license.
+ */
+(function(window) {
+
+"use strict";
+
+// Hoisted, referenced at the bottom of the source. This caches a list of all
+// LayoutManager options at definition time.
+var keys;
+
+// Localize global dependency references.
+var Backbone = window.Backbone;
+var _ = window._;
+var $ = Backbone.$;
+
+// Used for issuing warnings and debugging.
+var warn = window.console && window.console.warn;
+var trace = window.console && window.console.trace;
+
+// Maintain references to the two `Backbone.View` functions that are
+// overwritten so that they can be proxied.
+var _configure = Backbone.View.prototype._configure;
+var render = Backbone.View.prototype.render;
+
+// Cache these methods for performance.
+var aPush = Array.prototype.push;
+var aConcat = Array.prototype.concat;
+var aSplice = Array.prototype.splice;
+
+// LayoutManager is a wrapper around a `Backbone.View`.
+var LayoutManager = Backbone.View.extend({
+ // This named function allows for significantly easier debugging.
+ constructor: function Layout(options) {
+ // Options may not always be passed to the constructor, this ensures it is
+ // always an object.
+ options = options || {};
+
+ // Grant this View superpowers.
+ LayoutManager.setupView(this, options);
+
+ // Have Backbone set up the rest of this View.
+ Backbone.View.call(this, options);
+ },
+
+ // Shorthand to `setView` function with the `insert` flag set.
+ insertView: function(selector, view) {
+ // If the `view` argument exists, then a selector was passed in. This code
+ // path will forward the selector on to `setView`.
+ if (view) {
+ return this.setView(selector, view, true);
+ }
+
+ // If no `view` argument is defined, then assume the first argument is the
+ // View, somewhat now confusingly named `selector`.
+ return this.setView(selector, true);
+ },
+
+ // Iterate over an object and ensure every value is wrapped in an array to
+ // ensure they will be inserted, then pass that object to `setViews`.
+ insertViews: function(views) {
+ // If an array of views was passed it should be inserted into the
+ // root view. Much like calling insertView without a selector.
+ if (_.isArray(views)) {
+ return this.setViews({ "": views });
+ }
+
+ _.each(views, function(view, selector) {
+ views[selector] = _.isArray(view) ? view : [view];
+ });
+
+ return this.setViews(views);
+ },
+
+ // Returns the View that matches the `getViews` filter function.
+ getView: function(fn) {
+ // If `getView` is invoked with undefined as the first argument, then the
+ // second argument will be used instead. This is to allow
+ // `getViews(undefined, fn)` to work as `getViews(fn)`. Useful for when
+ // you are allowing an optional selector.
+ if (fn == null) {
+ fn = arguments[1];
+ }
+
+ return this.getViews(fn).first().value();
+ },
+
+ // Provide a filter function to get a flattened array of all the subviews.
+ // If the filter function is omitted it will return all subviews. If a
+ // String is passed instead, it will return the Views for that selector.
+ getViews: function(fn) {
+ // Generate an array of all top level (no deeply nested) Views flattened.
+ var views = _.chain(this.views).map(function(view) {
+ return _.isArray(view) ? view : [view];
+ }, this).flatten().value();
+
+ // If the filter argument is a String, then return a chained Version of the
+ // elements.
+ if (typeof fn === "string") {
+ return _.chain([this.views[fn]]).flatten();
+ }
+
+ // If the argument passed is an Object, then pass it to `_.where`.
+ if (typeof fn === "object") {
+ return _.chain([_.where(views, fn)]).flatten();
+ }
+
+ // If a filter function is provided, run it on all Views and return a
+ // wrapped chain. Otherwise, simply return a wrapped chain of all Views.
+ return _.chain(typeof fn === "function" ? _.filter(views, fn) : views);
+ },
+
+ // Use this to remove Views, internally uses `getViews` so you can pass the
+ // same argument here as you would to that method.
+ removeView: function(fn) {
+ // Allow an optional selector or function to find the right model and
+ // remove nested Views based off the results of the selector or filter.
+ return this.getViews(fn).each(function(nestedView) {
+ nestedView.remove();
+ });
+ },
+
+ // This takes in a partial name and view instance and assigns them to
+ // the internal collection of views. If a view is not a LayoutManager
+ // instance, then mix in the LayoutManager prototype. This ensures
+ // all Views can be used successfully.
+ //
+ // Must definitely wrap any render method passed in or defaults to a
+ // typical render function `return layout(this).render()`.
+ setView: function(name, view, insert) {
+ var manager, existing, options;
+ // Parent view, the one you are setting a View on.
+ var root = this;
+
+ // If no name was passed, use an empty string and shift all arguments.
+ if (typeof name !== "string") {
+ insert = view;
+ view = name;
+ name = "";
+ }
+
+ // If the parent views object doesn't exist... create it.
+ this.views = this.views || {};
+
+ // Shorthand the `__manager__` property.
+ manager = view.__manager__;
+
+ // Shorthand the View that potentially already exists.
+ existing = this.views[name];
+
+ // If the View has not been properly set up, throw an Error message
+ // indicating that the View needs `manage: true` set.
+ if (!manager) {
+ throw new Error("Please set `View#manage` property with selector '" +
+ name + "' to `true`.");
+ }
+
+ // Assign options.
+ options = view.getAllOptions();
+
+ // Add reference to the parentView.
+ manager.parent = root;
+
+ // Add reference to the placement selector used.
+ manager.selector = name;
+
+ // Set up event bubbling, inspired by Backbone.ViewMaster. Do not bubble
+ // internal events that are triggered.
+ view.on("all", function(name) {
+ if (name !== "beforeRender" && name !== "afterRender") {
+ root.trigger.apply(root, arguments);
+ }
+ }, view);
+
+ // Code path is less complex for Views that are not being inserted. Simply
+ // remove existing Views and bail out with the assignment.
+ if (!insert) {
+ // If the View we are adding has already been rendered, simply inject it
+ // into the parent.
+ if (manager.hasRendered) {
+ // Apply the partial.
+ options.partial(root.$el, view.$el, root.__manager__, manager);
+ }
+
+ // Ensure remove is called when swapping View's.
+ if (existing) {
+ // If the views are an array, iterate and remove each individually.
+ _.each(aConcat.call([], existing), function(nestedView) {
+ nestedView.remove();
+ });
+ }
+
+ // Assign to main views object and return for chainability.
+ return this.views[name] = view;
+ }
+
+ // Ensure this.views[name] is an array and push this View to the end.
+ this.views[name] = aConcat.call([], existing || [], view);
+
+ // Put the view into `insert` mode.
+ manager.insert = true;
+
+ return view;
+ },
+
+ // Allows the setting of multiple views instead of a single view.
+ setViews: function(views) {
+ // Iterate over all the views and use the View's view method to assign.
+ _.each(views, function(view, name) {
+ // If the view is an array put all views into insert mode.
+ if (_.isArray(view)) {
+ return _.each(view, function(view) {
+ this.insertView(name, view);
+ }, this);
+ }
+
+ // Assign each view using the view function.
+ this.setView(name, view);
+ }, this);
+
+ // Allow for chaining
+ return this;
+ },
+
+ // By default this should find all nested views and render them into
+ // the this.el and call done once all of them have successfully been
+ // resolved.
+ //
+ // This function returns a promise that can be chained to determine
+ // once all subviews and main view have been rendered into the view.el.
+ render: function() {
+ var root = this;
+ var options = root.getAllOptions();
+ var manager = root.__manager__;
+ var parent = manager.parent;
+ var rentManager = parent && parent.__manager__;
+ var def = options.deferred();
+
+ // Triggered once the render has succeeded.
+ function resolve() {
+ var next, afterRender;
+
+ // If there is a parent, attach.
+ if (parent) {
+ if (!options.contains(parent.el, root.el)) {
+ // Apply the partial.
+ options.partial(parent.$el, root.$el, rentManager, manager);
+ }
+ }
+
+ // Ensure events are always correctly bound after rendering.
+ root.delegateEvents();
+
+ // Set this View as successfully rendered.
+ manager.hasRendered = true;
+
+ // Only process the queue if it exists.
+ if (next = manager.queue.shift()) {
+ // Ensure that the next render is only called after all other
+ // `done` handlers have completed. This will prevent `render`
+ // callbacks from firing out of order.
+ next();
+ } else {
+ // Once the queue is depleted, remove it, the render process has
+ // completed.
+ delete manager.queue;
+ }
+
+ // Reusable function for triggering the afterRender callback and event
+ // and setting the hasRendered flag.
+ function completeRender() {
+ var afterRender = options.afterRender;
+
+ if (afterRender) {
+ afterRender.call(root, root);
+ }
+
+ // Always emit an afterRender event.
+ root.trigger("afterRender", root);
+
+ // If there are multiple top level elements and `el: false` is used,
+ // display a warning message and a stack trace.
+ if (manager.noel && root.$el.length > 1) {
+ // Do not display a warning while testing or if warning suppression
+ // is enabled.
+ if (warn && !options.suppressWarnings) {
+ window.console.warn("Using `el: false` with multiple top level " +
+ "elements is not supported.");
+
+ // Provide a stack trace if available to aid with debugging.
+ if (trace) { window.console.trace(); }
+ }
+ }
+ }
+
+ // If the parent is currently rendering, wait until it has completed
+ // until calling the nested View's `afterRender`.
+ if (rentManager && rentManager.queue) {
+ // Wait until the parent View has finished rendering, which could be
+ // asynchronous, and trigger afterRender on this View once it has
+ // compeleted.
+ parent.once("afterRender", completeRender);
+ } else {
+ // This View and its parent have both rendered.
+ completeRender();
+ }
+
+ return def.resolveWith(root, [root]);
+ }
+
+ // Actually facilitate a render.
+ function actuallyRender() {
+ var options = root.getAllOptions();
+ var manager = root.__manager__;
+ var parent = manager.parent;
+ var rentManager = parent && parent.__manager__;
+
+ // The `_viewRender` method is broken out to abstract away from having
+ // too much code in `actuallyRender`.
+ root._render(LayoutManager._viewRender, options).done(function() {
+ // If there are no children to worry about, complete the render
+ // instantly.
+ if (!_.keys(root.views).length) {
+ return resolve();
+ }
+
+ // Create a list of promises to wait on until rendering is done.
+ // Since this method will run on all children as well, its sufficient
+ // for a full hierarchical.
+ var promises = _.map(root.views, function(view) {
+ var insert = _.isArray(view);
+
+ // If items are being inserted, they will be in a non-zero length
+ // Array.
+ if (insert && view.length) {
+ // Schedule each view to be rendered in order and return a promise
+ // representing the result of the final rendering.
+ return _.reduce(view.slice(1), function(prevRender, view) {
+ return prevRender.then(function() {
+ return view.render();
+ });
+ // The first view should be rendered immediately, and the resulting
+ // promise used to initialize the reduction.
+ }, view[0].render());
+ }
+
+ // Only return the fetch deferred, resolve the main deferred after
+ // the element has been attached to it's parent.
+ return !insert ? view.render() : view;
+ });
+
+ // Once all nested Views have been rendered, resolve this View's
+ // deferred.
+ options.when(promises).done(resolve);
+ });
+ }
+
+ // Another render is currently happening if there is an existing queue, so
+ // push a closure to render later into the queue.
+ if (manager.queue) {
+ aPush.call(manager.queue, actuallyRender);
+ } else {
+ manager.queue = [];
+
+ // This the first `render`, preceeding the `queue` so render
+ // immediately.
+ actuallyRender(root, def);
+ }
+
+ // Add the View to the deferred so that `view.render().view.el` is
+ // possible.
+ def.view = root;
+
+ // This is the promise that determines if the `render` function has
+ // completed or not.
+ return def;
+ },
+
+ // Ensure the cleanup function is called whenever remove is called.
+ remove: function() {
+ // Force remove itself from its parent.
+ LayoutManager._removeView(this, true);
+
+ // Call the original remove function.
+ return this._remove.apply(this, arguments);
+ },
+
+ // Merge instance and global options.
+ getAllOptions: function() {
+ // Instance overrides take precedence, fallback to prototype options.
+ return _.extend({}, this, LayoutManager.prototype.options, this.options);
+ }
+},
+{
+ // Clearable cache.
+ _cache: {},
+
+ // Creates a deferred and returns a function to call when finished.
+ _makeAsync: function(options, done) {
+ var handler = options.deferred();
+
+ // Used to handle asynchronous renders.
+ handler.async = function() {
+ handler._isAsync = true;
+
+ return done;
+ };
+
+ return handler;
+ },
+
+ // This gets passed to all _render methods. The `root` value here is passed
+ // from the `manage(this).render()` line in the `_render` function
+ _viewRender: function(root, options) {
+ var url, contents, fetchAsync, renderedEl;
+ var manager = root.__manager__;
+
+ // This function is responsible for pairing the rendered template into
+ // the DOM element.
+ function applyTemplate(rendered) {
+ // Actually put the rendered contents into the element.
+ if (rendered) {
+ // If no container is specified, we must replace the content.
+ if (manager.noel) {
+ // Hold a reference to created element as replaceWith doesn't return new el.
+ renderedEl = $(rendered);
+
+ // Remove extra root elements
+ root.$el.slice(1).remove();
+
+ root.$el.replaceWith(renderedEl);
+ // Don't delegate events here - we'll do that in resolve()
+ root.setElement(renderedEl, false);
+ } else {
+ options.html(root.$el, rendered);
+ }
+ }
+
+ // Resolve only after fetch and render have succeeded.
+ fetchAsync.resolveWith(root, [root]);
+ }
+
+ // Once the template is successfully fetched, use its contents to proceed.
+ // Context argument is first, since it is bound for partial application
+ // reasons.
+ function done(context, contents) {
+ // Store the rendered template someplace so it can be re-assignable.
+ var rendered;
+ // This allows the `render` method to be asynchronous as well as `fetch`.
+ var renderAsync = LayoutManager._makeAsync(options, function(rendered) {
+ applyTemplate(rendered);
+ });
+
+ // Ensure the cache is up-to-date.
+ LayoutManager.cache(url, contents);
+
+ // Render the View into the el property.
+ if (contents) {
+ rendered = options.render.call(renderAsync, contents, context);
+ }
+
+ // If the function was synchronous, continue execution.
+ if (!renderAsync._isAsync) {
+ applyTemplate(rendered);
+ }
+ }
+
+ return {
+ // This `render` function is what gets called inside of the View render,
+ // when `manage(this).render` is called. Returns a promise that can be
+ // used to know when the element has been rendered into its parent.
+ render: function() {
+ var context = root.serialize || options.serialize;
+ var template = root.template || options.template;
+
+ // If data is a function, immediately call it.
+ if (_.isFunction(context)) {
+ context = context.call(root);
+ }
+
+ // This allows for `var done = this.async()` and then `done(contents)`.
+ fetchAsync = LayoutManager._makeAsync(options, function(contents) {
+ done(context, contents);
+ });
+
+ // Set the url to the prefix + the view's template property.
+ if (typeof template === "string") {
+ url = options.prefix + template;
+ }
+
+ // Check if contents are already cached and if they are, simply process
+ // the template with the correct data.
+ if (contents = LayoutManager.cache(url)) {
+ done(context, contents, url);
+
+ return fetchAsync;
+ }
+
+ // Fetch layout and template contents.
+ if (typeof template === "string") {
+ contents = options.fetch.call(fetchAsync, options.prefix + template);
+ // If the template is already a function, simply call it.
+ } else if (typeof template === "function") {
+ contents = template;
+ // If its not a string and not undefined, pass the value to `fetch`.
+ } else if (template != null) {
+ contents = options.fetch.call(fetchAsync, template);
+ }
+
+ // If the function was synchronous, continue execution.
+ if (!fetchAsync._isAsync) {
+ done(context, contents);
+ }
+
+ return fetchAsync;
+ }
+ };
+ },
+
+ // Remove all nested Views.
+ _removeViews: function(root, force) {
+ var views;
+
+ // Shift arguments around.
+ if (typeof root === "boolean") {
+ force = root;
+ root = this;
+ }
+
+ // Allow removeView to be called on instances.
+ root = root || this;
+
+ // Iterate over all of the nested View's and remove.
+ root.getViews().each(function(view) {
+ // Force doesn't care about if a View has rendered or not.
+ if (view.__manager__.hasRendered || force) {
+ LayoutManager._removeView(view, force);
+ }
+ });
+ },
+
+ // Remove a single nested View.
+ _removeView: function(view, force) {
+ var parentViews;
+ // Shorthand the manager for easier access.
+ var manager = view.__manager__;
+ // Test for keep.
+ var keep = typeof view.keep === "boolean" ? view.keep : view.options.keep;
+
+ // Only remove views that do not have `keep` attribute set, unless the
+ // View is in `insert` mode and the force flag is set.
+ if ((!keep && manager.insert === true) || force) {
+ // Clean out the events.
+ LayoutManager.cleanViews(view);
+
+ // Since we are removing this view, force subviews to remove
+ view._removeViews(true);
+
+ // Remove the View completely.
+ view.$el.remove();
+
+ // Bail out early if no parent exists.
+ if (!manager.parent) { return; }
+
+ // Assign (if they exist) the sibling Views to a property.
+ parentViews = manager.parent.views[manager.selector];
+
+ // If this is an array of items remove items that are not marked to
+ // keep.
+ if (_.isArray(parentViews)) {
+ // Remove duplicate Views.
+ return _.each(_.clone(parentViews), function(view, i) {
+ // If the managers match, splice off this View.
+ if (view && view.__manager__ === manager) {
+ aSplice.call(parentViews, i, 1);
+ }
+ });
+ }
+
+ // Otherwise delete the parent selector.
+ delete manager.parent.views[manager.selector];
+ }
+ },
+
+ // Cache templates into LayoutManager._cache.
+ cache: function(path, contents) {
+ // If template path is found in the cache, return the contents.
+ if (path in this._cache && contents == null) {
+ return this._cache[path];
+ // Ensure path and contents aren't undefined.
+ } else if (path != null && contents != null) {
+ return this._cache[path] = contents;
+ }
+
+ // If the template is not in the cache, return undefined.
+ },
+
+ // Accept either a single view or an array of views to clean of all DOM
+ // events internal model and collection references and all Backbone.Events.
+ cleanViews: function(views) {
+ // Clear out all existing views.
+ _.each(aConcat.call([], views), function(view) {
+ // Remove all custom events attached to this View.
+ view.unbind();
+
+ // Automatically unbind `model`.
+ if (view.model instanceof Backbone.Model) {
+ view.model.off(null, null, view);
+ }
+
+ // Automatically unbind `collection`.
+ if (view.collection instanceof Backbone.Collection) {
+ view.collection.off(null, null, view);
+ }
+
+ // Automatically unbind events bound to this View.
+ view.stopListening();
+
+ // If a custom cleanup method was provided on the view, call it after
+ // the initial cleanup is done
+ _.result(view.getAllOptions(), "cleanup");
+ });
+ },
+
+ // This static method allows for global configuration of LayoutManager.
+ configure: function(options) {
+ _.extend(LayoutManager.prototype.options, options);
+
+ // Allow LayoutManager to manage Backbone.View.prototype.
+ if (options.manage) {
+ Backbone.View.prototype.manage = true;
+ }
+
+ // Disable the element globally.
+ if (options.el === false) {
+ Backbone.View.prototype.el = false;
+ }
+
+ // Allow global configuration of `suppressWarnings`.
+ if (options.suppressWarnings === true) {
+ Backbone.View.prototype.suppressWarnings = true;
+ }
+ },
+
+ // Configure a View to work with the LayoutManager plugin.
+ setupView: function(views, options) {
+ // Set up all Views passed.
+ _.each(aConcat.call([], views), function(view) {
+ // If the View has already been setup, no need to do it again.
+ if (view.__manager__) {
+ return;
+ }
+
+ var views, declaredViews, viewOptions;
+ var proto = LayoutManager.prototype;
+ var viewOverrides = _.pick(view, keys);
+
+ // Ensure necessary properties are set.
+ _.defaults(view, {
+ // Ensure a view always has a views object.
+ views: {},
+
+ // Internal state object used to store whether or not a View has been
+ // taken over by layout manager and if it has been rendered into the DOM.
+ __manager__: {},
+
+ // Add the ability to remove all Views.
+ _removeViews: LayoutManager._removeViews,
+
+ // Add the ability to remove itself.
+ _removeView: LayoutManager._removeView
+
+ // Mix in all LayoutManager prototype properties as well.
+ }, LayoutManager.prototype);
+
+ // Extend the options with the prototype and passed options.
+ options = view.options = _.defaults(options || {}, view.options,
+ proto.options);
+
+ // Ensure view events are properly copied over.
+ viewOptions = _.pick(options, aConcat.call(["events"],
+ _.values(options.events)));
+
+ // Merge the View options into the View.
+ _.extend(view, viewOptions);
+
+ // If the View still has the Backbone.View#render method, remove it. Don't
+ // want it accidentally overriding the LM render.
+ if (viewOverrides.render === LayoutManager.prototype.render ||
+ viewOverrides.render === Backbone.View.prototype.render) {
+ delete viewOverrides.render;
+ }
+
+ // Pick out the specific properties that can be dynamically added at
+ // runtime and ensure they are available on the view object.
+ _.extend(options, viewOverrides);
+
+ // By default the original Remove function is the Backbone.View one.
+ view._remove = Backbone.View.prototype.remove;
+
+ // Always use this render function when using LayoutManager.
+ view._render = function(manage, options) {
+ // Keep the view consistent between callbacks and deferreds.
+ var view = this;
+ // Shorthand the manager.
+ var manager = view.__manager__;
+ // Cache these properties.
+ var beforeRender = options.beforeRender;
+
+ // Ensure all nested Views are properly scrubbed if re-rendering.
+ if (manager.hasRendered) {
+ this._removeViews();
+ }
+
+ // If a beforeRender function is defined, call it.
+ if (beforeRender) {
+ beforeRender.call(this, this);
+ }
+
+ // Always emit a beforeRender event.
+ this.trigger("beforeRender", this);
+
+ // Render!
+ return manage(this, options).render();
+ };
+
+ // Ensure the render is always set correctly.
+ view.render = LayoutManager.prototype.render;
+
+ // If the user provided their own remove override, use that instead of the
+ // default.
+ if (view.remove !== proto.remove) {
+ view._remove = view.remove;
+ view.remove = proto.remove;
+ }
+
+ // Normalize views to exist on either instance or options, default to
+ // options.
+ views = options.views || view.views;
+
+ // Set the internal views, only if selectors have been provided.
+ if (_.keys(views).length) {
+ // Keep original object declared containing Views.
+ declaredViews = views;
+
+ // Reset the property to avoid duplication or overwritting.
+ view.views = {};
+
+ // Set the declared Views.
+ view.setViews(declaredViews);
+ }
+
+ // If a template is passed use that instead.
+ if (view.options.template) {
+ view.options.template = options.template;
+ // Ensure the template is mapped over.
+ } else if (view.template) {
+ options.template = view.template;
+ }
+ });
+ }
+});
+
+// Convenience assignment to make creating Layout's slightly shorter.
+Backbone.Layout = LayoutManager;
+// Tack on the version.
+LayoutManager.VERSION = "0.8.6";
+
+// Override _configure to provide extra functionality that is necessary in
+// order for the render function reference to be bound during initialize.
+Backbone.View.prototype._configure = function(options) {
+ var noel, retVal;
+
+ // Remove the container element provided by Backbone.
+ if ("el" in options ? options.el === false : this.el === false) {
+ noel = true;
+ }
+
+ // Run the original _configure.
+ retVal = _configure.apply(this, arguments);
+
+ // If manage is set, do it!
+ if (options.manage || this.manage) {
+ // Set up this View.
+ LayoutManager.setupView(this);
+ }
+
+ // Assign the `noel` property once we're sure the View we're working with is
+ // managed by LayoutManager.
+ if (this.__manager__) {
+ this.__manager__.noel = noel;
+ this.__manager__.suppressWarnings = options.suppressWarnings;
+ }
+
+ // Act like nothing happened.
+ return retVal;
+};
+
+// Default configuration options; designed to be overriden.
+LayoutManager.prototype.options = {
+ // Prefix template/layout paths.
+ prefix: "",
+
+ // Can be used to supply a different deferred implementation.
+ deferred: function() {
+ return $.Deferred();
+ },
+
+ // Fetch is passed a path and is expected to return template contents as a
+ // function or string.
+ fetch: function(path) {
+ return _.template($(path).html());
+ },
+
+ // This is the most common way you will want to partially apply a view into
+ // a layout.
+ partial: function($root, $el, rentManager, manager) {
+ // If selector is specified, attempt to find it.
+ if (manager.selector) {
+ if (rentManager.noel) {
+ var $filtered = $root.filter(manager.selector);
+ $root = $filtered.length ? $filtered : $root.find(manager.selector);
+ } else {
+ $root = $root.find(manager.selector);
+ }
+ }
+
+ // Use the insert method if insert argument is true.
+ if (manager.insert) {
+ this.insert($root, $el);
+ } else {
+ this.html($root, $el);
+ }
+ },
+
+ // Override this with a custom HTML method, passed a root element and content
+ // (a jQuery collection or a string) to replace the innerHTML with.
+ html: function($root, content) {
+ $root.html(content);
+ },
+
+ // Very similar to HTML except this one will appendChild by default.
+ insert: function($root, $el) {
+ $root.append($el);
+ },
+
+ // Return a deferred for when all promises resolve/reject.
+ when: function(promises) {
+ return $.when.apply(null, promises);
+ },
+
+ // By default, render using underscore's templating.
+ render: function(template, context) {
+ return template(context);
+ },
+
+ // A method to determine if a View contains another.
+ contains: function(parent, child) {
+ return $.contains(parent, child);
+ }
+};
+
+// Maintain a list of the keys at define time.
+keys = _.keys(LayoutManager.prototype.options);
+
+})(typeof global === "object" ? global : this);
View
1,206 prototype/public/scripts/lib/lodash.js
@@ -1,7 +1,7 @@
/**
* @license
* Lo-Dash 1.0.1 (Custom Build) <http://lodash.com/>
- * Build: `lodash modern -o ./dist/lodash.js`
+ * Build: `lodash underscore -o ./dist/lodash.underscore.js`
* Copyright 2012-2013 The Dojo Foundation <http://dojofoundation.org/>
* Based on Underscore.js 1.4.4 <http://underscorejs.org/>
* Copyright 2009-2013 Jeremy Ashkenas, DocumentCloud Inc.
@@ -31,9 +31,6 @@
/** Used internally to indicate various things */
var indicatorObject = objectRef;
- /** Used by `cachedContains` as the default size when optimizations are enabled for large arrays */
- var largeArraySize = 30;
-
/** Used to restore the original `_` reference in `noConflict` */
var oldDash = window._;
@@ -80,7 +77,6 @@
var ceil = Math.ceil,
concat = arrayRef.concat,
floor = Math.floor,
- getPrototypeOf = reNative.test(getPrototypeOf = Object.getPrototypeOf) && getPrototypeOf,
hasOwnProperty = objectRef.hasOwnProperty,
push = arrayRef.push,
toString = objectRef.toString;
@@ -113,26 +109,21 @@
/* Detect if `Function#bind` exists and is inferred to be fast (all but V8) */
var isBindFast = nativeBind && !isV8;
- /* Detect if `Object.keys` exists and is inferred to be fast (IE, Opera, V8) */
- var isKeysFast = nativeKeys && (isIeOpera || isV8);
-
- /** Used to identify object classifications that `_.clone` supports */
- var cloneableClasses = {};
- cloneableClasses[funcClass] = false;
- cloneableClasses[argsClass] = cloneableClasses[arrayClass] =
- cloneableClasses[boolClass] = cloneableClasses[dateClass] =
- cloneableClasses[numberClass] = cloneableClasses[objectClass] =
- cloneableClasses[regexpClass] = cloneableClasses[stringClass] = true;
-
- /** Used to lookup a built-in constructor by [[Class]] */
- var ctorByClass = {};
- ctorByClass[arrayClass] = Array;
- ctorByClass[boolClass] = Boolean;
- ctorByClass[dateClass] = Date;
- ctorByClass[objectClass] = Object;
- ctorByClass[numberClass] = Number;
- ctorByClass[regexpClass] = RegExp;
- ctorByClass[stringClass] = String;
+ /**
+ * Detect if `Array#shift` and `Array#splice` augment array-like objects
+ * incorrectly:
+ *
+ * Firefox < 10, IE compatibility mode, and IE < 9 have buggy Array `shift()`
+ * and `splice()` functions that fail to remove the last element, `value[0]`,
+ * of array-like objects even though the `length` property is set to `0`.
+ * The `shift()` method is buggy in IE 8 compatibility mode, while `splice()`
+ * is buggy regardless of mode in IE < 9 and buggy in compatibility mode in IE 9.
+ */
+ var hasObjectSpliceBug = (hasObjectSpliceBug = { '0': 1, 'length': 1 },
+ arrayRef.splice.call(hasObjectSpliceBug, 0, 1), hasObjectSpliceBug[0]);
+
+ /** Detect if `arguments` objects are `Object` objects (all but Opera < 10.5) */
+ var argsAreObjects = arguments.constructor == Object;
/** Used to determine if values are of the language type Object */
var objectTypes = {
@@ -245,151 +236,12 @@
* @memberOf _.templateSettings
* @type String
*/
- 'variable': '',
-
- /**
- * Used to import variables into the compiled template.
- *
- * @memberOf _.templateSettings
- * @type Object
- */
- 'imports': {
-
- /**
- * A reference to the `lodash` function.
- *
- * @memberOf _.templateSettings.imports
- * @type Function
- */
- '_': lodash
- }
+ 'variable': ''
};
/*--------------------------------------------------------------------------*/
/**
- * The template used to create iterator functions.
- *
- * @private
- * @param {Obect} data The data object used to populate the text.
- * @returns {String} Returns the interpolated text.
- */
- var iteratorTemplate = function(obj) {
-
- var __p = 'var index, iterable = ' +
- (obj.firstArg ) +
- ', result = iterable;\nif (!iterable) return result;\n' +
- (obj.top ) +
- ';\n';
- if (obj.arrays) {
- __p += 'var length = iterable.length; index = -1;\nif (' +
- (obj.arrays ) +
- ') {\n while (++index < length) {\n ' +
- (obj.loop ) +
- '\n }\n}\nelse { ';
- } ;
-
- if (obj.isKeysFast && obj.useHas) {
- __p += '\n var ownIndex = -1,\n ownProps = objectTypes[typeof iterable] ? nativeKeys(iterable) : [],\n length = ownProps.length;\n\n while (++ownIndex < length) {\n index = ownProps[ownIndex];\n ' +
- (obj.loop ) +
- '\n } ';
- } else {
- __p += '\n for (index in iterable) {';
- if (obj.useHas) {
- __p += '\n if (';
- if (obj.useHas) {
- __p += 'hasOwnProperty.call(iterable, index)';
- } ;
- __p += ') { ';
- } ;
- __p +=
- (obj.loop ) +
- '; ';
- if (obj.useHas) {
- __p += '\n }';
- } ;
- __p += '\n } ';
- } ;
-
- if (obj.arrays) {
- __p += '\n}';
- } ;
- __p +=
- (obj.bottom ) +
- ';\nreturn result';
-
-
- return __p
- };
-
- /** Reusable iterator options for `assign` and `defaults` */
- var defaultsIteratorOptions = {
- 'args': 'object, source, guard',
- 'top':
- 'var args = arguments,\n' +
- ' argsIndex = 0,\n' +
- " argsLength = typeof guard == 'number' ? 2 : args.length;\n" +
- 'while (++argsIndex < argsLength) {\n' +
- ' iterable = args[argsIndex];\n' +
- ' if (iterable && objectTypes[typeof iterable]) {',
- 'loop': "if (typeof result[index] == 'undefined') result[index] = iterable[index]",
- 'bottom': ' }\n}'
- };
-
- /** Reusable iterator options shared by `each`, `forIn`, and `forOwn` */
- var eachIteratorOptions = {
- 'args': 'collection, callback, thisArg',
- 'top': "callback = callback && typeof thisArg == 'undefined' ? callback : createCallback(callback, thisArg)",
- 'arrays': "typeof length == 'number'",
- 'loop': 'if (callback(iterable[index], index, collection) === false) return result'
- };
-
- /** Reusable iterator options for `forIn` and `forOwn` */
- var forOwnIteratorOptions = {
- 'top': 'if (!objectTypes[typeof iterable]) return result;\n' + eachIteratorOptions.top,
- 'arrays': false
- };
-
- /*--------------------------------------------------------------------------*/
-
- /**
- * Creates a function optimized to search large arrays for a given `value`,
- * starting at `fromIndex`, using strict equality for comparisons, i.e. `===`.
- *
- * @private
- * @param {Array} array The array to search.
- * @param {Mixed} value The value to search for.
- * @param {Number} [fromIndex=0] The index to search from.
- * @param {Number} [largeSize=30] The length at which an array is considered large.
- * @returns {Boolean} Returns `true`, if `value` is found, else `false`.
- */
- function cachedContains(array, fromIndex, largeSize) {
- fromIndex || (fromIndex = 0);
-
- var length = array.length,
- isLarge = (length - fromIndex) >= (largeSize || largeArraySize);
-
- if (isLarge) {
- var cache = {},
- index = fromIndex - 1;
-
- while (++index < length) {
- // manually coerce `value` to a string because `hasOwnProperty`, in some
- // older versions of Firefox, coerces objects incorrectly
- var key = array[index] + '';
- (hasOwnProperty.call(cache, key) ? cache[key] : (cache[key] = [])).push(array[index]);
- }
- }
- return function(value) {
- if (isLarge) {
- var key = value + '';
- return hasOwnProperty.call(cache, key) && indexOf(cache[key], value) > -1;
- }
- return indexOf(array, value, fromIndex) > -1;
- }
- }
-
- /**
* Used by `_.max` and `_.min` as the default `callback` when a given
* `collection` is a string value.
*
@@ -513,7 +365,7 @@
var length = props.length,
result = false;
while (length--) {
- if (!(result = isEqual(object[props[length]], func[props[length]], indicatorObject))) {
+ if (!(result = object[props[length]] === func[props[length]])) {
break;
}
}
@@ -544,55 +396,6 @@
}
/**
- * Creates compiled iteration functions.
- *
- * @private
- * @param {Object} [options1, options2, ...] The compile options object(s).
- * arrays - A string of code to determine if the iterable is an array or array-like.
- * useHas - A boolean to specify using `hasOwnProperty` checks in the object loop.
- * args - A string of comma separated arguments the iteration function will accept.
- * top - A string of code to execute before the iteration branches.
- * loop - A string of code to execute in the object loop.
- * bottom - A string of code to execute after the iteration branches.
- *
- * @returns {Function} Returns the compiled function.
- */
- function createIterator() {
- var data = {
- // support properties
- 'isKeysFast': isKeysFast,
-
- // iterator options
- 'arrays': 'isArray(iterable)',
- 'bottom': '',
- 'loop': '',
- 'top': '',
- 'useHas': true
- };
-
- // merge options into a template data object
- for (var object, index = 0; object = arguments[index]; index++) {
- for (var key in object) {
- data[key] = object[key];
- }
- }
- var args = data.args;
- data.firstArg = /^[^,]+/.exec(args)[0];
-
- // create the function factory
- var factory = Function(
- 'createCallback, hasOwnProperty, isArguments, isArray, isString, ' +
- 'objectTypes, nativeKeys',
- 'return function(' + args + ') {\n' + iteratorTemplate(data) + '\n}'
- );
- // return the compiled function
- return factory(
- createCallback, hasOwnProperty, isArguments, isArray, isString,
- objectTypes, nativeKeys
- );
- }
-
- /**
* A function compiled to iterate `arguments` objects, arrays, objects, and
* strings consistenly across environments, executing the `callback` for each
* element in the `collection`. The `callback` is bound to `thisArg` and invoked
@@ -606,7 +409,24 @@
* @param {Mixed} [thisArg] The `this` binding of `callback`.
* @returns {Array|Object|String} Returns `collection`.
*/
- var each = createIterator(eachIteratorOptions);
+ var each = function (collection, callback, thisArg) {
+ var index, iterable = collection, result = iterable;
+ if (!iterable) return result;
+ callback = callback && typeof thisArg == 'undefined' ? callback : createCallback(callback, thisArg);
+ var length = iterable.length; index = -1;
+ if (typeof length == 'number') {
+ while (++index < length) {
+ if (callback(iterable[index], index, collection) === indicatorObject) return result
+ }
+ }
+ else {
+ for (index in iterable) {
+ if (hasOwnProperty.call(iterable, index)) {
+ if (callback(iterable[index], index, collection) === indicatorObject) return result;
+ }
+ }
+ }
+ };
/**
* Used by `template` to escape characters for inclusion in compiled
@@ -713,6 +533,12 @@
function isArguments(value) {
return toString.call(value) == argsClass;
}
+ // fallback for browsers that can't detect `arguments` objects by [[Class]]
+ if (!isArguments(arguments)) {
+ isArguments = function(value) {
+ return value ? hasOwnProperty.call(value, 'callee') : false;
+ };
+ }
/**
* Iterates over `object`'s own and inherited enumerable properties, executing
@@ -743,9 +569,17 @@
* });
* // => alerts 'name' and 'bark' (order is not guaranteed)
*/
- var forIn = createIterator(eachIteratorOptions, forOwnIteratorOptions, {
- 'useHas': false
- });
+ var forIn = function (collection, callback) {
+ var index, iterable = collection, result = iterable;
+ if (!iterable) return result;
+ if (!objectTypes[typeof iterable]) return result;
+ callback || (callback = identity);
+
+ for (index in iterable) {
+ if (callback(iterable[index], index, collection) === indicatorObject) return result;
+ }
+ return result
+ };
/**
* Iterates over an object's own enumerable properties, executing the `callback`
@@ -768,7 +602,19 @@
* });
* // => alerts '0', '1', and 'length' (order is not guaranteed)
*/
- var forOwn = createIterator(eachIteratorOptions, forOwnIteratorOptions);
+ var forOwn = function (collection, callback) {
+ var index, iterable = collection, result = iterable;
+ if (!iterable) return result;
+ if (!objectTypes[typeof iterable]) return result;
+ callback || (callback = identity);
+
+ for (index in iterable) {
+ if (hasOwnProperty.call(iterable, index)) {
+ if (callback(iterable[index], index, collection) === indicatorObject) return result;
+ }
+ }
+ return result
+ };
/**
* Checks if `value` is an array.
@@ -789,7 +635,7 @@
var isArray = nativeIsArray || function(value) {
// `instanceof` may cause a memory leak in IE 7 if `value` is a host object
// http://ajaxian.com/archives/working-aroung-the-instanceof-memory-leak
- return value instanceof Array || toString.call(value) == arrayClass;
+ return (argsAreObjects && value instanceof Array) || toString.call(value) == arrayClass;
};
/**
@@ -909,18 +755,20 @@
* defaults(food, { 'name': 'banana', 'type': 'fruit' });
* // => { 'name': 'apple', 'type': 'fruit' }
*/
- var assign = createIterator(defaultsIteratorOptions, {
- 'top':
- defaultsIteratorOptions.top.replace(';',
- ';\n' +
- "if (argsLength > 3 && typeof args[argsLength - 2] == 'function') {\n" +
- ' var callback = createCallback(args[--argsLength - 1], args[argsLength--], 2);\n' +
- "} else if (argsLength > 2 && typeof args[argsLength - 1] == 'function') {\n" +
- ' callback = args[--argsLength];\n' +
- '}'
- ),
- 'loop': 'result[index] = callback ? callback(result[index], iterable[index]) : iterable[index]'
- });
+ function assign(object) {
+ if (!object) {
+ return object;
+ }
+ for (var argsIndex = 1, argsLength = arguments.length; argsIndex < argsLength; argsIndex++) {
+ var iterable = arguments[argsIndex];
+ if (iterable) {
+ for (var key in iterable) {
+ object[key] = iterable[key];
+ }
+ }
+ }
+ return object;
+ }
/**
* Creates a clone of `value`. If `deep` is `true`, nested objects will also
@@ -964,133 +812,10 @@
* clone.childNodes.length;
* // => 0
*/
- function clone(value, deep, callback, thisArg, stackA, stackB) {
- var result = value;
-
- // allows working with "Collections" methods without using their `callback`
- // argument, `index|key`, for this method's `callback`
- if (typeof deep == 'function') {
- thisArg = callback;
- callback = deep;
- deep = false;
- }
- if (typeof callback == 'function') {
- callback = typeof thisArg == 'undefined' ? callback : createCallback(callback, thisArg, 1);
- result = callback(result);
-
- var done = typeof result != 'undefined';
- if (!done) {
- result = value;
- }
- }
- // inspect [[Class]]
- var isObj = isObject(result);
- if (isObj) {
- var className = toString.call(result);
- if (!cloneableClasses[className]) {
- return result;
- }
- var isArr = isArray(result);
- }
- // shallow clone
- if (!isObj || !deep) {
- return isObj && !done
- ? (isArr ? slice(result) : assign({}, result))
- : result;
- }
- var ctor = ctorByClass[className];
- switch (className) {
- case boolClass:
- case dateClass:
- return done ? result : new ctor(+result);
-
- case numberClass:
- case stringClass:
- return done ? result : new ctor(result);
-
- case regexpClass:
- return done ? result : ctor(result.source, reFlags.exec(result));
- }
- // check for circular references and return corresponding clone
- stackA || (stackA = []);
- stackB || (stackB = []);
-
- var length = stackA.length;
- while (length--) {
- if (stackA[length] == value) {
- return stackB[length];
- }
- }
- // init cloned object
- if (!done) {
- result = isArr ? ctor(result.length) : {};
-
- // add array properties assigned by `RegExp#exec`
- if (isArr) {
- if (hasOwnProperty.call(value, 'index')) {
- result.index = value.index;
- }
- if (hasOwnProperty.call(value, 'input')) {
- result.input = value.input;
- }
- }
- }
- // add the source value to the stack of traversed objects
- // and associate it with its clone
- stackA.push(value);
- stackB.push(result);
-
- // recursively populate clone (susceptible to call stack limits)
- (isArr ? forEach : forOwn)(done ? result : value, function(objValue, key) {
- result[key] = clone(objValue, deep, callback, undefined, stackA, stackB);
- });
-
- return result;
- }
-
- /**
- * Creates a deep clone of `value`. If a `callback` function is passed, it will
- * be executed to produce the cloned values. If `callback` returns the value it
- * was passed, cloning will be handled by the method instead. The `callback` is
- * bound to `thisArg` and invoked with one argument; (value).
- *
- * Note: This function is loosely based on the structured clone algorithm. Functions
- * and DOM nodes are **not** cloned. The enumerable properties of `arguments` objects and
- * objects created by constructors other than `Object` are cloned to plain `Object` objects.
- * See http://www.w3.org/TR/html5/infrastructure.html#internal-structured-cloning-algorithm.
- *
- * @static
- * @memberOf _
- * @category Objects
- * @param {Mixed} value The value to deep clone.
- * @param {Function} [callback] The function to customize cloning values.
- * @param {Mixed} [thisArg] The `this` binding of `callback`.
- * @returns {Mixed} Returns the deep cloned `value`.
- * @example
- *
- * var stooges = [
- * { 'name': 'moe', 'age': 40 },
- * { 'name': 'larry', 'age': 50 }
- * ];
- *
- * var deep = _.cloneDeep(stooges);
- * deep[0] === stooges[0];
- * // => false
- *
- * var view = {
- * 'label': 'docs',
- * 'node': element
- * };
- *
- * var clone = _.cloneDeep(view, function(value) {
- * return _.isElement(value) ? value.cloneNode(true) : value;
- * });
- *
- * clone.node == view.node;
- * // => false
- */
- function cloneDeep(value, callback, thisArg) {
- return clone(value, true, callback, thisArg);
+ function clone(value) {
+ return isObject(value)
+ ? (isArray(value) ? slice(value) : assign({}, value))
+ : value
}
/**
@@ -1113,7 +838,22 @@
* _.defaults(food, { 'name': 'banana', 'type': 'fruit' });
* // => { 'name': 'apple', 'type': 'fruit' }
*/
- var defaults = createIterator(defaultsIteratorOptions);
+ function defaults(object) {
+ if (!object) {
+ return object;
+ }
+ for (var argsIndex = 1, argsLength = arguments.length; argsIndex < argsLength; argsIndex++) {
+ var iterable = arguments[argsIndex];
+ if (iterable) {
+ for (var key in iterable) {
+ if (object[key] == null) {
+ object[key] = iterable[key];
+ }
+ }
+ }
+ }
+ return object;
+ }
/**
* Creates a sorted array of all enumerable properties, own and inherited,
@@ -1258,22 +998,18 @@
* // => true
*/
function isEmpty(value) {
- var result = true;
if (!value) {
- return result;
+ return true;
}
- var className = toString.call(value),
- length = value.length;
-
- if ((className == arrayClass || className == stringClass ||
- className == argsClass) ||
- (className == objectClass && typeof length == 'number' && isFunction(value.splice))) {
- return !length;
+ if (isArray(value) || isString(value)) {
+ return !value.length;
}
- forOwn(value, function() {
- return (result = false);
- });
- return result;
+ for (var key in value) {
+ if (hasOwnProperty.call(value, key)) {
+ return false;
+ }
+ }
+ return true;
}
/**
@@ -1316,83 +1052,52 @@
* });
* // => true
*/
- function isEqual(a, b, callback, thisArg, stackA, stackB) {
- // used to indicate that when comparing objects, `a` has at least the properties of `b`
- var whereIndicator = callback === indicatorObject;
- if (callback && !whereIndicator) {
- callback = typeof thisArg == 'undefined' ? callback : createCallback(callback, thisArg, 2);
- var result = callback(a, b);
- if (typeof result != 'undefined') {
- return !!result;
- }
- }
- // exit early for identical values
+ function isEqual(a, b, stackA, stackB) {
if (a === b) {
- // treat `+0` vs. `-0` as not equal
return a !== 0 || (1 / a == 1 / b);
}
var type = typeof a,
otherType = typeof b;
- // exit early for unlike primitive values
if (a === a &&
(!a || (type != 'function' && type != 'object')) &&
(!b || (otherType != 'function' && otherType != 'object'))) {
return false;
}
- // exit early for `null` and `undefined`, avoiding ES3's Function#call behavior
- // http://es5.github.com/#x15.3.4.4
if (a == null || b == null) {
return a === b;
}
- // compare [[Class]] names
var className = toString.call(a),
otherClass = toString.call(b);
- if (className == argsClass) {
- className = objectClass;
- }
- if (otherClass == argsClass) {
- otherClass = objectClass;
- }
if (className != otherClass) {
return false;
}
switch (className) {
case boolClass:
case dateClass:
- // coerce dates and booleans to numbers, dates to milliseconds and booleans
- // to `1` or `0`, treating invalid dates coerced to `NaN` as not equal
return +a == +b;
case numberClass:
- // treat `NaN` vs. `NaN` as equal
return a != +a
? b != +b
- // but treat `+0` vs. `-0` as not equal
: (a == 0 ? (1 / a == 1 / b) : a == +b);
case regexpClass:
case stringClass:
- // coerce regexes to strings (http://es5.github.com/#x15.10.6.4)
- // treat string primitives and their corresponding object instances as equal
return a == b + '';
}
var isArr = className == arrayClass;
if (!isArr) {
- // unwrap any `lodash` wrapped values
if (a.__wrapped__ || b.__wrapped__) {
- return isEqual(a.__wrapped__ || a, b.__wrapped__ || b, callback, thisArg, stackA, stackB);
+ return isEqual(a.__wrapped__ || a, b.__wrapped__ || b, stackA, stackB);
}
- // exit for functions and DOM nodes
if (className != objectClass) {
return false;
}
- // in older versions of Opera, `arguments` objects have `Array` constructors
var ctorA = a.constructor,
ctorB = b.constructor;
- // non `Object` object instances with different constructors are not equal
if (ctorA != ctorB && !(
isFunction(ctorA) && ctorA instanceof ctorA &&
isFunction(ctorB) && ctorB instanceof ctorB
@@ -1400,9 +1105,6 @@
return false;
}
}
- // assume cyclic structures are equal
- // the algorithm for detecting cyclic structures is adapted from ES 5.1
- // section 15.12.3, abstract operation `JO` (http://es5.github.com/#x15.12.3)
stackA || (stackA = []);
stackB || (stackB = []);
@@ -1412,57 +1114,36 @@
return stackB[length] == b;
}
}
- var size = 0;
- result = true;
+ var result = true,
+ size = 0;
- // add `a` and `b` to the stack of traversed objects
stackA.push(a);
stackB.push(b);
- // recursively compare objects and arrays (susceptible to call stack limits)
if (isArr) {
- length = a.length;
size = b.length;
-
- // compare lengths to determine if a deep comparison is necessary
result = size == a.length;
- if (!result && !whereIndicator) {
- return result;
- }
- // deep compare the contents, ignoring non-numeric properties
- while (size--) {
- var index = length,
- value = b[size];
-
- if (whereIndicator) {
- while (index--) {
- if ((result = isEqual(a[index], value, callback, thisArg, stackA, stackB))) {
- break;
- }
+
+ if (result) {
+ while (size--) {
+ if (!(result = isEqual(a[size], b[size], stackA, stackB))) {
+ break;
}
- } else if (!(result = isEqual(a[size], value, callback, thisArg, stackA, stackB))) {
- break;
}
}
return result;
}
- // deep compare objects using `forIn`, instead of `forOwn`, to avoid `Object.keys`
- // which, in this case, is more costly
forIn(b, function(value, key, b) {
if (hasOwnProperty.call(b, key)) {
- // count the number of properties.
size++;
- // deep compare each property value.
- return (result = hasOwnProperty.call(a, key) && isEqual(a[key], value, callback, thisArg, stackA, stackB));
+ return !(result = hasOwnProperty.call(a, key) && isEqual(a[key], value, stackA, stackB)) && indicatorObject;
}
});
- if (result && !whereIndicator) {
- // ensure both objects have the same number of properties
+ if (result) {
forIn(a, function(value, key, a) {
if (hasOwnProperty.call(a, key)) {
- // `size` will be `-1` if `a` has more properties than `b`
- return (result = --size > -1);
+ return !(result = --size > -1) && indicatorObject;
}
});
}
@@ -1621,42 +1302,6 @@
}
/**
- * Checks if a given `value` is an object created by the `Object` constructor.
- *
- * @static
- * @memberOf _
- * @category Objects
- * @param {Mixed} value The value to check.
- * @returns {Boolean} Returns `true`, if `value` is a plain object, else `false`.
- * @example
- *
- * function Stooge(name, age) {
- * this.name = name;
- * this.age = age;
- * }
- *
- * _.isPlainObject(new Stooge('moe', 40));
- * // => false
- *
- * _.isPlainObject([1, 2, 3]);
- * // => false
- *
- * _.isPlainObject({ 'name': 'moe', 'age': 40 });
- * // => true
- */
- var isPlainObject = function(value) {
- if (!(value && typeof value == 'object')) {
- return false;
- }
- var valueOf = value.valueOf,
- objProto = typeof valueOf == 'function' && (objProto = getPrototypeOf(valueOf)) && getPrototypeOf(objProto);
-
- return objProto
- ? value == objProto || (getPrototypeOf(value) == objProto && !isArguments(value))
- : shimIsPlainObject(value);
- };
-
- /**
* Checks if `value` is a regular expression.
*
* @static
@@ -1708,143 +1353,6 @@
}
/**
- * Recursively merges own enumerable properties of the source object(s), that
- * don't resolve to `undefined`, into the destination object. Subsequent sources
- * will overwrite propery assignments of previous sources. If a `callback` function
- * is passed, it will be executed to produce the merged values of the destination
- * and source properties. If `callback` returns `undefined`, merging will be
- * handled by the method instead. The `callback` is bound to `thisArg` and
- * invoked with two arguments; (objectValue, sourceValue).
- *
- * @static
- * @memberOf _
- * @category Objects
- * @param {Object} object The destination object.
- * @param {Object} [source1, source2, ...] The source objects.
- * @param {Function} [callback] The function to customize merging properties.
- * @param {Mixed} [thisArg] The `this` binding of `callback`.
- * @param- {Object} [deepIndicator] Internally used to indicate that `stackA`
- * and `stackB` are arrays of traversed objects instead of source objects.
- * @param- {Array} [stackA=[]] Internally used to track traversed source objects.
- * @param- {Array} [stackB=[]] Internally used to associate values with their
- * source counterparts.
- * @returns {Object} Returns the destination object.
- * @example
- *
- * var names = {
- * 'stooges': [
- * { 'name': 'moe' },
- * { 'name': 'larry' }
- * ]
- * };
- *
- * var ages = {
- * 'stooges': [
- * { 'age': 40 },
- * { 'age': 50 }
- * ]
- * };
- *
- * _.merge(names, ages);
- * // => { 'stooges': [{ 'name': 'moe', 'age': 40 }, { 'name': 'larry', 'age': 50 }] }
- *
- * var food = {
- * 'fruits': ['apple'],
- * 'vegetables': ['beet']
- * };
- *
- * var otherFood = {
- * 'fruits': ['banana'],
- * 'vegetables': ['carrot']
- * };
- *
- * _.merge(food, otherFood, function(a, b) {
- * return _.isArray(a) ? a.concat(b) : undefined;
- * });
- * // => { 'fruits': ['apple', 'banana'], 'vegetables': ['beet', 'carrot] }
- */
- function merge(object, source, deepIndicator) {
- var args = arguments,
- index = 0,
- length = 2;
-
- if (!isObject(object)) {
- return object;
- }
- if (deepIndicator === indicatorObject) {
- var callback = args[3],
- stackA = args[4],
- stackB = args[5];
- } else {
- stackA = [];
- stackB = [];
-
- // allows working with `_.reduce` and `_.reduceRight` without
- // using their `callback` arguments, `index|key` and `collection`
- if (typeof deepIndicator != 'number') {
- length = args.length;
- }
- if (length > 3 && typeof args[length - 2] == 'function') {
- callback = createCallback(args[--length - 1], args[length--], 2);
- } else if (length > 2 && typeof args[length - 1] == 'function') {
- callback = args[--length];
- }
- }
- while (++index < length) {
- (isArray(args[index]) ? forEach : forOwn)(args[index], function(source, key) {
- var found,
- isArr,
- result = source,
- value = object[key];
-
- if (source && ((isArr = isArray(source)) || isPlainObject(source))) {
- // avoid merging previously merged cyclic sources
- var stackLength = stackA.length;
- while (stackLength--) {
- if ((found = stackA[stackLength] == source)) {
- value = stackB[stackLength];
- break;
- }
- }
- if (!found) {
- value = isArr
- ? (isArray(value) ? value : [])
- : (isPlainObject(value) ? value : {});
-
- if (callback) {
- result = callback(value, source);
- if (typeof result != 'undefined') {
- value = result;
- }
- }
- // add `source` and associated `value` to the stack of traversed objects
- stackA.push(source);
- stackB.push(value);
-
- // recursively merge objects and arrays (susceptible to call stack limits)
- if (!callback) {
- value = merge(value, source, indicatorObject, callback, stackA, stackB);
- }
- }
- }
- else {
- if (callback) {
- result = callback(value, source);
- if (typeof result == 'undefined') {
- result = source;
- }
- }
- if (typeof result != 'undefined') {
- value = result;
- }
- }
- object[key] = value;
- });
- }
- return object;
- }
-
- /**
* Creates a shallow clone of `object` excluding the specified properties.
* Property names may be specified as individual arguments or as arrays of
* property names. If a `callback` function is passed, it will be executed
@@ -1870,20 +1378,12 @@
* });
* // => { 'name': 'moe' }
*/
- function omit(object, callback, thisArg) {
- var isFunc = typeof callback == 'function',
+ function omit(object) {
+ var props = concat.apply(arrayRef, arguments),
result = {};
- if (isFunc) {
- callback = createCallback(callback, thisArg);
- } else {
- var props = concat.apply(arrayRef, arguments);
- }
- forIn(object, function(value, key, object) {
- if (isFunc
- ? !callback(value, key, object)
- : indexOf(props, key, 1) < 0
- ) {
+ forIn(object, function(value, key) {
+ if (indexOf(props, key, 1) < 0) {
result[key] = value;
}
});
@@ -1942,26 +1442,17 @@
* });
* // => { 'name': 'moe' }
*/
- function pick(object, callback, thisArg) {
- var result = {};
- if (typeof callback != 'function') {
- var index = 0,
- props = concat.apply(arrayRef, arguments),
- length = isObject(object) ? props.length : 0;
+ function pick(object) {
+ var index = 0,
+ props = concat.apply(arrayRef, arguments),
+ length = props.length,
+ result = {};
- while (++index < length) {
- var key = props[index];
- if (key in object) {
- result[key] = object[key];
- }
+ while (++index < length) {
+ var prop = props[index];
+ if (prop in object) {
+ result[prop] = object[prop];
}
- } else {
- callback = createCallback(callback, thisArg);
- forIn(object, function(value, key, object) {
- if (callback(value, key, object)) {
- result[key] = value;
- }
- });
}
return result;
}
@@ -1994,39 +1485,6 @@
/*--------------------------------------------------------------------------*/
/**
- * Creates an array of elements from the specified indexes, or keys, of the
- * `collection`. Indexes may be specified as individual arguments or as arrays
- * of indexes.
- *
- * @static
- * @memberOf _
- * @category Collections
- * @param {Array|Object|String} collection The collection to iterate over.
- * @param {Array|Number|String} [index1, index2, ...] The indexes of
- * `collection` to retrieve, either as individual arguments or arrays.
- * @returns {Array} Returns a new array of elements corresponding to the
- * provided indexes.
- * @example
- *
- * _.at(['a', 'b', 'c', 'd', 'e'], [0, 2, 4]);
- * // => ['a', 'c', 'e']
- *
- * _.at(['moe', 'larry', 'curly'], 0, 2);
- * // => ['moe', 'curly']
- */
- function at(collection) {
- var index = -1,
- props = concat.apply(arrayRef, slice(arguments, 1)),
- length = props.length,
- result = Array(length);
-
- while(++index < length) {
- result[index] = collection[props[index]];
- }
- return result;
- }
-
- /**
* Checks if a given `target` element is present in a `collection` using strict
* equality for comparisons, i.e. `===`. If `fromIndex` is negative, it is used
* as the offset from the end of the collection.
@@ -2053,22 +1511,14 @@
* _.contains('curly', 'ur');
* // => true
*/
- function contains(collection, target, fromIndex) {
- var index = -1,
- length = collection ? collection.length : 0,
+ function contains(collection, target) {
+ var length = collection ? collection.length : 0,
result = false;
-
- fromIndex = (fromIndex < 0 ? nativeMax(0, length + fromIndex) : fromIndex) || 0;
if (typeof length == 'number') {
- result = (isString(collection)
- ? collection.indexOf(target, fromIndex)
- : indexOf(collection, target, fromIndex)
- ) > -1;
+ result = indexOf(collection, target) > -1;
} else {
each(collection, function(value) {
- if (++index >= fromIndex) {
- return !(result = value === target);
- }
+ return (result = value === target) && indicatorObject;
});
}
return result;
@@ -2174,7 +1624,7 @@
}
} else {
each(collection, function(value, index, collection) {
- return (result = !!callback(value, index, collection));
+ return !(result = !!callback(value, index, collection)) && indicatorObject;
});
}
return result;
@@ -2294,12 +1744,16 @@
forEach(collection, function(value, index, collection) {
if (callback(value, index, collection)) {
result = value;
- return false;
+ return indicatorObject;
}
});
return result;
}
+ function findWhere(object, properties) {
+ return where(object, properties, true);
+ }
+
/**
* Iterates over a `collection`, executing the `callback` for each element in
* the `collection`. The `callback` is bound to `thisArg` and invoked with three
@@ -2328,14 +1782,13 @@
length = collection.length;
while (++index < length) {
- if (callback(collection[index], index, collection) === false) {
+ if (callback(collection[index], index, collection) === indicatorObject) {
break;
}
}
} else {
each(collection, callback, thisArg);
- }
- return collection;
+ };
}
/**
@@ -2529,9 +1982,7 @@
}
}
} else {
- callback = !callback && isString(collection)
- ? charAtCallback
- : createCallback(callback, thisArg);
+ callback = createCallback(callback, thisArg);
each(collection, function(value, index, collection) {
var current = callback(value, index, collection);
@@ -2598,9 +2049,7 @@
}
}
} else {
- callback = !callback && isString(collection)
- ? charAtCallback
- : createCallback(callback, thisArg);
+ callback = createCallback(callback, thisArg);
each(collection, function(value, index, collection) {
var current = callback(value, index, collection);
@@ -2882,7 +2331,7 @@
}
} else {
each(collection, function(value, index, collection) {
- return !(result = callback(value, index, collection));
+ return (result = callback(value, index, collection)) && indicatorObject;
});
}
return !!result;
@@ -2988,7 +2437,11 @@
* _.where(stooges, { 'age': 40 });
* // => [{ 'name': 'moe', 'age': 40 }]
*/
- var where = filter;
+ function where(collection, properties, first) {