Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP

We’re showing branches in this repository, but you can also compare across forks.

base: df1bae359e
...
compare: d9fa758e54
  • 17 commits
  • 25 files changed
  • 0 commit comments
  • 2 contributors
Commits on Mar 22, 2013
Tim Caswell creationix Allow for custom usernames in finder test a3471ca
Commits on Mar 25, 2013
Tim Caswell creationix Add update and bye request forwarding in the server afe4c18
jugglinmike jugglinmike Rename iterator variable to avoid shadowing
While iterating over location transports, use a distinct name to refer
to each location transport so the requesting transport may still be
referenced (and resolved) from the iterator function.
e2d0d14
Tim Caswell creationix Merge pull request #17 from openpeer/transport-shadow
Rename iterator variable to avoid shadowing
10fa895
jugglinmike jugglinmike Include location ID in peer location find reply
This information is necessary to continue communication.
d856b71
Tim Caswell creationix Merge pull request #18 from openpeer/find-reply-loc
Include location ID in peer location find reply
b18a1a5
jugglinmike jugglinmike Prevent modification of request meta-data 7cc831e
Tim Caswell creationix Merge pull request #19 from openpeer/key-copy
Prevent modification of request meta-data
302ada7
jugglinmike jugglinmike Correct error in compatability layer c4dbb89
jugglinmike jugglinmike Begin modularizing GUI
This version, while technically functional, still uses some global state
and tightly-coupled componenets.
54abf61
jugglinmike jugglinmike Update Lodash to Underscore.js compatability build 1c4c9a2
jugglinmike jugglinmike Correct error in compatability layer 7abe98d
jugglinmike jugglinmike Re-factor UI
Split component parts into specialized views and make some simplistic
style improvements.
ef14ab3
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
adfd2dd
jugglinmike jugglinmike Extend UI to require user name input
This includes the addition of a "User" model and associated unit tests.
c925664
jugglinmike jugglinmike Integrate new Transport messages ff714fc
jugglinmike jugglinmike Create 'Peer' module from 'User' and 'PC' modules
This consolidation will help to make the prototype code base more
understandable. It is also the first step in supporting multiple
simultaneous peer connections.
d9fa758
Showing with 1,812 additions and 1,093 deletions.
  1. +1 −0  prototype/Gruntfile.js
  2. +3 −1 prototype/lib/transport.js
  3. +5 −8 prototype/public/index.html
  4. +122 −103 prototype/public/scripts/app.js
  5. +868 −0 prototype/public/scripts/lib/backbone.layoutmanager.js
  6. +265 −941 prototype/public/scripts/lib/lodash.js
  7. +25 −0 prototype/public/scripts/modules/contacts-view.js
  8. +2 −2 prototype/public/scripts/modules/gum-compat.js
  9. +61 −0 prototype/public/scripts/modules/layout.js
  10. +33 −0 prototype/public/scripts/modules/login-view.js
  11. +27 −21 prototype/public/scripts/modules/{pc.js → peer.js}
  12. +2 −1  prototype/public/scripts/modules/rtc-compat.js
  13. +94 −0 prototype/public/scripts/modules/stream-views.js
  14. +16 −0 prototype/public/scripts/require-config.js
  15. +96 −3 prototype/public/styles/main.css
  16. +48 −0 prototype/public/styles/reset.css
  17. +2 −0  prototype/public/templates/contacts-list.html
  18. +8 −0 prototype/public/templates/layout.html
  19. +11 −0 prototype/public/templates/login.html
  20. +6 −0 prototype/public/templates/stream-view-local.html
  21. +1 −0  prototype/public/templates/stream-view-remote.html
  22. +65 −5 prototype/public/testfinder.html
  23. +31 −8 prototype/server.js
  24. +1 −0  prototype/test/client/list_of_tests.js
  25. +19 −0 prototype/test/client/tests/peer.js
1  prototype/Gruntfile.js
View
@@ -37,6 +37,7 @@ module.exports = function(grunt) {
define: true,
assert: true,
suite: true,
+ suiteSetup: true,
test: true
}
},
4 prototype/lib/transport.js
View
@@ -160,7 +160,9 @@
$method: method
};
for (var key in request) {
- message[key] = request[key];
+ if (key[0] !== '$') {
+ message[key] = request[key];
+ }
}
console.log('request', message);
message = {request: message};
13 prototype/public/index.html
View
@@ -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>
225 prototype/public/scripts/app.js
View
@@ -1,10 +1,10 @@
require([
- 'modules/nder', 'modules/pc', 'modules/gum-compat', 'jquery'
- ], function(Nder, PC, gum, $) {
+ 'modules/peer', 'modules/transport', 'modules/layout', 'backbone', 'q'
+ ], function(Peer, Transport, Layout, Backbone, Q) {
'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');
@@ -80,62 +36,125 @@ require([
console.error('Create Answer failed');
}
- var pc = new PC();
+ var user = new Peer();
+ var pc = new Peer();
+ var layout = new Layout({
+ el: '#app',
+ user: user,
+ contacts: new Backbone.Collection(contacts)
+ });
+ layout.render();
+ layout.on('connectRequest', function(stream) {
+ // TODO: Derive remote peer ID from application state
+ var remotePeerID = 'creationix';
+ if (!pc.isActive() && transport.state === 'OPEN') {
+
+ pc.connect(config.pcConfig);
+ pc.addStream(stream);
+ pc.createOffer(
+ function(sessionDescription) {
+ this.setLocalDescription(sessionDescription);
+ transport.peerLocationFind(remotePeerID, {
+ session: sessionDescription,
+ userName: user.get('name')
+ }).then(function(findReply) {
+ console.log('Promise Resolved', findReply);
+ pc.setRemoteDescription(findReply.sessionDescription);
+ pc.set('locationID', findReply.from);
+ }, function() {
+ // TODO: Update the UI to reflect this failure.
+ console.error('Find request failed.');
+ });
+ },
+ createOfferFailed,
+ mediaConstraints);
+ }
+ });
+ layout.on('hangup', function() {
+ transport.request('bye', {
+ to: pc.get('locationID')
+ });
+ 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);
+ pc.on('ice', function(candidate) {
+ console.log('Sending ICE candidate:', candidate);
+ transport.request('update', {
+ candidate: candidate,
+ to: pc.get('locationID')
+ });
});
- 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.connect(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());
}
+ pc.set('locationID', request.username.from);
+ console.log('Creating remote session description:', remoteSession);
+ pc.setRemoteDescription(remoteSession);
+ console.log('Sending answer...');
+ var dfd = Q.defer();
+ pc.createAnswer(function(sessionDescription) {
+ this.setLocalDescription(sessionDescription);
+ dfd.resolve({
+ peer: true,
+ sessionDescription: sessionDescription
+ });
+ },
+ createAnswerFailed, mediaConstraints);
+ return dfd.promise;
+ },
+ bye: function() {
+ pc.destroy();
+ },
+ update: function(msg) {
+ if (!pc.isActive()) {
+ return;
+ }
+ console.log('Received ICE candidate:', msg.candidate);
+ pc.addIceCandidate(msg.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);
+ user.on('change:name', function() {
+ transport.open(new WebSocket(config.socketServer))
+ .then(function() {
+ return transport.sessionCreate(user.get('name'));
+ })
+ .then(function() {
+ // Simulate network latency
+ setTimeout(function() {
+ layout.login();
+ }, 800);
+ }, console.error.bind(console));
+ });
+
});
868 prototype/public/scripts/lib/backbone.layoutmanager.js
View
@@ -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);
1,206 prototype/public/scripts/lib/lodash.js
View
@@ -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) {
+ return (first && isEmpty(properties))
+ ? null
+ : (first ? find : filter)(collection, properties);
+ }
/*--------------------------------------------------------------------------*/
@@ -3038,18 +2491,17 @@
*/
function difference(array) {
var index = -1,
- length = array ? array.length : 0,
+ length = array.length,
flattened = concat.apply(arrayRef, arguments),
- contains = cachedContains(flattened, length),
result = [];
while (++index < length) {
- var value = array[index];
- if (!contains(value)) {
+ var value = array[index]
+ if (indexOf(flattened, value, length) < 0) {
result.push(value);
}
}
- return result;
+ return result
}
/**
@@ -3300,29 +2752,17 @@
function intersection(array) {
var args = arguments,
argsLength = args.length,
- cache = { '0': {} },
index = -1,
length = array ? array.length : 0,
- isLarge = length >= 100,
- result = [],
- seen = result;
+ result = [];
outer:
while (++index < length) {
var value = array[index];
- if (isLarge) {
- var key = value + '';
- var inited = hasOwnProperty.call(cache[0], key)
- ? !(seen = cache[0][key])
- : (seen = cache[0][key] = []);
- }
- if (inited || indexOf(seen, value) < 0) {
- if (isLarge) {
- seen.push(value);
- }
+ if (indexOf(result, value) < 0) {
var argsIndex = argsLength;
while (--argsIndex) {
- if (!(cache[argsIndex] || (cache[argsIndex] = cachedContains(args[argsIndex], 0, 100)))(value)) {
+ if (indexOf(args[argsIndex], value) < 0) {
continue outer;
}
}
@@ -3730,17 +3170,11 @@
result = [],
seen = result;
- // juggle arguments
if (typeof isSorted == 'function') {
thisArg = callback;
callback = isSorted;
isSorted = false;
}
- // init value cache for large arrays
- var isLarge = !isSorted && length >= 75;
- if (isLarge) {