Merge lp:~trobz/web-addons/web-unleashed into lp:~webaddons-core-editors/web-addons/7.0

Proposed by Michel Meyer
Status: Needs review
Proposed branch: lp:~trobz/web-addons/web-unleashed
Merge into: lp:~webaddons-core-editors/web-addons/7.0
Diff against target: 34319 lines (+33875/-0)
72 files modified
web_unleashed/__init__.py (+2/-0)
web_unleashed/__openerp__.py (+82/-0)
web_unleashed/static/lib/backbone/backbone.js (+1581/-0)
web_unleashed/static/lib/jquery-addons/jquery.font_size.js (+11/-0)
web_unleashed/static/lib/jquery-addons/jquery.form_reset.js (+13/-0)
web_unleashed/static/lib/jquery-addons/jquery.serialize_object.js (+17/-0)
web_unleashed/static/lib/jquery-addons/jquery.when_all.js (+56/-0)
web_unleashed/static/lib/marionette/marionette.js (+2363/-0)
web_unleashed/static/lib/underscore-addons/underscore.deep_extend.js (+52/-0)
web_unleashed/static/lib/underscore-addons/underscore.find_index_where.js (+25/-0)
web_unleashed/static/lib/underscore/underscore.js (+1276/-0)
web_unleashed/static/src/css/pager.css (+4/-0)
web_unleashed/static/src/js/collections/base.js (+168/-0)
web_unleashed/static/src/js/collections/group.js (+236/-0)
web_unleashed/static/src/js/collections/iterator.js (+149/-0)
web_unleashed/static/src/js/collections/pager.js (+100/-0)
web_unleashed/static/src/js/controllers/pager.js (+402/-0)
web_unleashed/static/src/js/core/unleashed.js (+435/-0)
web_unleashed/static/src/js/models/base.js (+80/-0)
web_unleashed/static/src/js/models/iterator.js (+51/-0)
web_unleashed/static/src/js/models/query.js (+80/-0)
web_unleashed/static/src/js/models/state.js (+76/-0)
web_unleashed/static/src/js/utils/connector.js (+319/-0)
web_unleashed/static/src/js/utils/holder.js (+36/-0)
web_unleashed/static/src/js/views/base.js (+82/-0)
web_unleashed/static/src/js/views/pager.js (+124/-0)
web_unleashed/static/src/js/views/panel.js (+42/-0)
web_unleashed/static/src/js/views/region.js (+53/-0)
web_unleashed/static/src/js/views/unleashed.js (+246/-0)
web_unleashed/static/src/js/views/view.js (+19/-0)
web_unleashed/static/src/tests/connector.js (+240/-0)
web_unleashed/static/src/tests/group.js (+280/-0)
web_unleashed/static/src/tests/pager.js (+291/-0)
web_unleashed/static/src/xml/base.xml (+9/-0)
web_unleashed/static/src/xml/pager.xml (+41/-0)
web_unleashed/tests/__init__.py (+2/-0)
web_unleashed/tests/test_js.py (+35/-0)
web_unleashed_extra/__init__.py (+3/-0)
web_unleashed_extra/__openerp__.py (+76/-0)
web_unleashed_extra/static/lib/bootstrap-scoped/bootstrap-reset-openerp.css (+29/-0)
web_unleashed_extra/static/lib/bootstrap-scoped/bootstrap-scoped.css (+4689/-0)
web_unleashed_extra/static/lib/bootstrap-scoped/bootstrap.js (+2001/-0)
web_unleashed_extra/static/lib/font-awesome/css/font-awesome-ie7.css (+1203/-0)
web_unleashed_extra/static/lib/font-awesome/css/font-awesome-ie7.min.css (+384/-0)
web_unleashed_extra/static/lib/font-awesome/css/font-awesome.css (+1479/-0)
web_unleashed_extra/static/lib/font-awesome/css/font-awesome.min.css (+403/-0)
web_unleashed_extra/static/lib/font-awesome/font/fontawesome-webfont.svg (+399/-0)
web_unleashed_extra/static/lib/font-awesome/less/bootstrap.less (+84/-0)
web_unleashed_extra/static/lib/font-awesome/less/core.less (+129/-0)
web_unleashed_extra/static/lib/font-awesome/less/extras.less (+93/-0)
web_unleashed_extra/static/lib/font-awesome/less/font-awesome-ie7.less (+1953/-0)
web_unleashed_extra/static/lib/font-awesome/less/font-awesome.less (+33/-0)
web_unleashed_extra/static/lib/font-awesome/less/icons.less (+381/-0)
web_unleashed_extra/static/lib/font-awesome/less/mixins.less (+48/-0)
web_unleashed_extra/static/lib/font-awesome/less/path.less (+14/-0)
web_unleashed_extra/static/lib/font-awesome/less/variables.less (+735/-0)
web_unleashed_extra/static/lib/font-awesome/scss/_bootstrap.scss (+84/-0)
web_unleashed_extra/static/lib/font-awesome/scss/_core.scss (+129/-0)
web_unleashed_extra/static/lib/font-awesome/scss/_extras.scss (+93/-0)
web_unleashed_extra/static/lib/font-awesome/scss/_icons.scss (+381/-0)
web_unleashed_extra/static/lib/font-awesome/scss/_mixins.scss (+48/-0)
web_unleashed_extra/static/lib/font-awesome/scss/_path.scss (+14/-0)
web_unleashed_extra/static/lib/font-awesome/scss/_variables.scss (+734/-0)
web_unleashed_extra/static/lib/font-awesome/scss/font-awesome-ie7.scss (+1953/-0)
web_unleashed_extra/static/lib/font-awesome/scss/font-awesome.scss (+33/-0)
web_unleashed_extra/static/lib/moment-twix/twix.js (+2/-0)
web_unleashed_extra/static/lib/moment/moment.js (+6841/-0)
web_unleashed_extra/static/lib/numeral/numeral.js (+106/-0)
web_unleashed_extra/static/src/css/field_serialized.css (+28/-0)
web_unleashed_extra/static/src/css/global.css (+10/-0)
web_unleashed_extra/static/src/js/models/period.js (+86/-0)
web_unleashed_extra/static/src/js/widgets/field_serialized.js (+119/-0)
To merge this branch: bzr merge lp:~trobz/web-addons/web-unleashed
Reviewer Review Type Date Requested Status
Holger Brunn (Therp) Needs Resubmitting
Yannick Vaucher @ Camptocamp Abstain
Review via email: mp+195542@code.launchpad.net

Description of the change

New core module for an easy web module development:

- clear MVC pattern, based on Backbone and Marionnette... with all their documentations !
- new namespace to organize and get access to your objects
- full-featured Backbone Models with OpenERP JSON-RPC API support
 -QWeb rendering for Marionette views
- base objects to build custom views (Pager, Grouped Collection, Extended OpenERP View, State manager...)
 -unit tests for basic functionalities
- load/configuration commonly used libraries (momentjs, numeraljs, awesome-font,...)

Check the doc on https://github.com/trobz/openerp-web-unleashed

To post a comment you must log in.
Revision history for this message
Holger Brunn (Therp) (hbrunn) wrote :

just to inform you that I'm reading your code every now and then, but as it's a lot of it, a review will take time...

Revision history for this message
Michel Meyer (mmeyer-y) wrote :

Thanks for taking the time and feel free to ask me any questions :)

On 12/02/2013 09:02 PM, Holger Brunn (Therp) wrote:
> just to inform you that I'm reading your code every now and then, but as it's a lot of it, a review will take time...

Revision history for this message
Holger Brunn (Therp) (hbrunn) wrote :

Looks pretty good! I'm yet to try it in a serious project, but for the time being: Well done!

Some nits so far:

- your js tests will run twice, once from the web module, once triggered from tests/test_js.py. Is this possibly a leftover from development?

- any reason to deviate from putting qweb templates into static/src/xml?

- I understand the reasoning behind the name, but something more descriptive than web_unleashed would be better IMHO

- core/unleashed.js should use translated strings where appropriate

Now a serious question: Could you elaborate where the context you pass to sync() comes from? From my understanding, it's passed every time you'd call fetch() on the model, but where does that come from? UnleashedView gets it from the dataset (you shouldn't eval that then btw but a copy of it before passing it to the server in connector.js), which is fine, but I can't see where it's propagated further to the models? (I'm not a backbone expert if that's not obvious by now)

Revision history for this message
Michel Meyer (mmeyer-y) wrote :
Download full text (4.6 KiB)

Dear Holger,

It's an honor that you have looked at my code so deeply, glad to know
you're liking it :)

About your questions:

*- your js tests will run twice, once from the web module, once triggered from tests/test_js.py. Is this possibly a leftover from development?**
*
I did this because i want to launch my tests on Travis (for web unleashed sources on github), I think we can keep them as a "fast_suite".
Do you see any disadvantages to keep web unit test runnable on server side too ?

*- any reason to deviate from putting qweb templates into static/src/xml?**
*
I was not aware of community rules when I started this module, I will change it to be compliant with OpenERP rules.

*- I understand the reasoning behind the name, but something more descriptive than web_unleashed would be better IMHO**
*
I named it like this because it's my feeling when I'm using it.
The original web module on OpenERP is, at least, obscure and using well known libraries instead give so much advantages... (I'm still hope that OpenERP SA will go on this way one day...)

*- core/unleashed.js should use translated strings where appropriate
*
I don't see where there's strings to translate in the core, can you give me more details on this point ?
*
- Could you elaborate where the context you pass to sync() comes from? From my understanding, it's passed every time you'd call fetch() on the model, but where does that come from?**
**UnleashedView gets it from the dataset (you shouldn't eval that then btw but a copy of it before passing it to the server in connector.js), which is fine, but I can't see where it's propagated further to the models? (I'm not a backbone expert if that's not obvious by now)*

sync() is used by Backbone Model as a interface to achieve CRUD methods.
This function is called by Backbone with specific parameters (crud method, model/collection, options), so the js context at sync execution is the Backbone Model.

I use the Connector as an interface between Backbone models and the JSON-RPC API. For example, when fetch() is called, it will go through sync > connector > read, and the domain (filter), group by (group), order by (order), selected fields and the OpenERP context can be set on the "options" object and will be processed by the Connector.

sync() function should maybe set a default OpenERP context... but the context is only available at the OpenERP view level, not when sync() function is defined by web unleashed.
I think it's up to the developer to pass the context to the Backbone model, and then passing it to CRUD methods...
However we can imagine to add to BaseModel/BaseCollection a context option, if they have it, the model will pass it to CRUD methods automatically.

>*you shouldn't eval that then btw but a copy of it before passing it to the server in connector.js*

OK, but actually, I use the context only to get the user id and the language...
Maybe you can explain me what kind of side effects I can have by evaluating it in my view, at init ?

 From my understanding, the context is used only for view configuration on client side (get user id/language, some custom parameters, etc...).
Further, even if I don't pass the context to the serv...

Read more...

Revision history for this message
Holger Brunn (Therp) (hbrunn) wrote :

Michel, thanks for your comments. I already doubted my programming skills when I couldn't find out where the context is propagated, good to hear it's not.

On context: You definitely need to think of a way to pass the user's standard context (openerp.session.user_context) to your models. Otherwise, you lose stuff like the timezone (which causes all functions dealing with datetimes to assume UTC, and you really don't want that) and default values. That's what currently comes to my mind, there are also other things like the currently active project or the like.

On evaluating context: It can contain code line time(), so if you eval it on view initialization, you'll always pass the same time.

Translations: The messages in the Errors you throw are not translated to my understanding. Even though most of them look like they never should occur in a production environment, I still think it's good practice to translate them.

Tests: I don't see real world disadvantages, so if you've reason to keep them there, keep them

lp:~trobz/web-addons/web-unleashed updated
11. By Michel Meyer

[MERGE] GitHub changes (change files permission)

12. By Michel Meyer

rename templates folder into xml, to be compliant with OpenERP rules

13. By Michel Meyer

ensure a context can be passed on any JSON-RPC call made by backbone models

14. By Michel Meyer

remove this.context on UnleashedView (accessible with this.dataset.get_context())

15. By Michel Meyer

add a method to instantiate translated Error object + apply this method on all thrown errors

16. By Michel Meyer

fix mistakes after git > bzr merge...

Revision history for this message
Michel Meyer (mmeyer-y) wrote :
Download full text (4.3 KiB)

Hi Holger,

I did some changes on sources according to your comments, you can get
them on lp:~trobz/web-addons/web-unleashed
<https://code.launchpad.net/%7Etrobz/web-addons/web-unleashed> branch.

To resume my changes:
- 13
<http://bazaar.launchpad.net/%7Etrobz/web-addons/web-unleashed/revision/13>.
ensure a context can be passed on any JSON-RPC call made by backbone models
- 14
<http://bazaar.launchpad.net/%7Etrobz/web-addons/web-unleashed/revision/14>.
remove this.context on UnleashedView (accessible with
this.dataset.get_context())
- 15
<http://bazaar.launchpad.net/%7Etrobz/web-addons/web-unleashed/revision/15>.
add a method to instantiate translated Error object
- 12
<http://bazaar.launchpad.net/%7Etrobz/web-addons/web-unleashed/revision/12>.
rename template folder into xml

*Context*

I was not aware of all the implication of the context, thanks for all
these details ! It pushed me to look more deeply in OpenERP sources...
And after some research, this is what I understand about how OpenERP
manage the context internally:

/# for read queries
/
instance.web.Query generate a/compoundContext /based on multiple contexts:
- the user context (instance.session.user_context)
- the instance.web.Model.context, if any
- additional context specified for this query, if any (= options.context
on unleashed backbone models)
- re evaluate all these contexts together and generate the final one...
(really, don't ask me to look deeper in pyeval, seems so odd...)

# for count queries

instance.web.Query generate a/compoundContext /based on:
- the user context (instance.session.user_context)
- the instance.web.Model.context, if any
- additional context specified for this query, if any (= options.context
on unleashed backbone models)

This is done my calling this._model.context(this._context)}) at Query
execution.

/# for other method (create, update, delete)/
/
- /indirectly, in instance.web.JsonRPC core object, the user context is
set by default (if not context is passed as on options to call method)

It's done with:
_.defaults(params, {
     context: this.user_context || {}
});

I'm still not sure why OpenERP doesn't use the
instance.web.Model.context() method in instance.web.Model.call(), but
they may have a good reason to do so...

On unleashed code, at instance.web.Model instantiation in sync(), I pass
the options.context as the second parameter, so Model has a default
context used at execution for all type of query.

Finally, unleashed sync() function allow developers to pass a custom
context to instance.web.Model object, and the rest (user context,
etc...) is managed by OpenERP and I don't think there's extra work to do
on this...

*Error Translation**
*
Ok, I was thinking throwing errors is not made for the final user, it's
more for developers.

Nonetheless, I added a method on unleashed modules to instantiate a
translated Error object and I changed sources to use it on all error thrown.

Note: in OpenERP sources, at core level, Error are not translated, so we
may have a mix of translated/not translated error message...

It's a long email again, but I think the subject is interesting and
unleashed is made to be a ...

Read more...

Revision history for this message
Holger Brunn (Therp) (hbrunn) wrote :

Thanks for your changes concerning context, they seem fine to me.

Revision history for this message
Holger Brunn (Therp) (hbrunn) :
review: Approve
Revision history for this message
Yannick Vaucher @ Camptocamp (yvaucher-c2c) :
review: Abstain
Revision history for this message
Holger Brunn (Therp) (hbrunn) wrote :

This project is now hosted on https://github.com/OCA/web. Please move your proposal there. This guide may help you https://github.com/OCA/maintainers-tools/wiki/How-to-move-a-Merge-Proposal-to-GitHub

review: Needs Resubmitting

Unmerged revisions

16. By Michel Meyer

fix mistakes after git > bzr merge...

15. By Michel Meyer

add a method to instantiate translated Error object + apply this method on all thrown errors

14. By Michel Meyer

remove this.context on UnleashedView (accessible with this.dataset.get_context())

13. By Michel Meyer

ensure a context can be passed on any JSON-RPC call made by backbone models

12. By Michel Meyer

rename templates folder into xml, to be compliant with OpenERP rules

11. By Michel Meyer

[MERGE] GitHub changes (change files permission)

10. By Michel Meyer

[MERGE] GitHub web-unleashed modules with LP web-addons

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== added directory 'web_unleashed'
=== added file 'web_unleashed/__init__.py'
--- web_unleashed/__init__.py 1970-01-01 00:00:00 +0000
+++ web_unleashed/__init__.py 2014-01-28 13:12:54 +0000
@@ -0,0 +1,2 @@
1# -*- coding: utf-8 -*-
2import tests
03
=== added file 'web_unleashed/__openerp__.py'
--- web_unleashed/__openerp__.py 1970-01-01 00:00:00 +0000
+++ web_unleashed/__openerp__.py 2014-01-28 13:12:54 +0000
@@ -0,0 +1,82 @@
1# -*- coding: utf-8 -*-
2{
3 'name': 'Web Unleashed',
4 'version': '1.0',
5 'category': 'Hidden',
6
7 'description': """
8Core Web Module:
9
10- improve code architecture and organization
11- add support of Backbone and Marionette frameworks
12- native support of JSON-RPC API for Backbone
13 """,
14
15 'author': 'Trobz',
16 'website': 'https://github.com/trobz/openerp-web-unleashed',
17
18 'depends': [
19 'web'
20 ],
21
22 'qweb' : [
23 'static/src/xml/*.xml',
24 ],
25
26 'css' : [
27 'static/src/css/pager.css',
28 ],
29
30 'js': [
31 # backbone 1.1.0 and underscore 1.5.2 used in no conflict mode, see `unleashed.js` for more details
32 'static/lib/underscore/underscore.js',
33 'static/lib/backbone/backbone.js',
34
35 # backbone.marionette 1.1.0
36 'static/lib/marionette/marionette.js',
37
38 # addons for libs
39 'static/lib/jquery-addons/jquery.font_size.js',
40 'static/lib/jquery-addons/jquery.when_all.js',
41 'static/lib/jquery-addons/jquery.serialize_object.js',
42 'static/lib/jquery-addons/jquery.form_reset.js',
43 'static/lib/underscore-addons/underscore.deep_extend.js',
44 'static/lib/underscore-addons/underscore.find_index_where.js',
45
46 # manage object instanciation and sync support for backbone models
47 'static/src/js/core/unleashed.js',
48
49 # utils
50 'static/src/js/utils/holder.js',
51 'static/src/js/utils/connector.js',
52
53 # controller
54 'static/src/js/controllers/pager.js',
55
56 # backbone base models
57 'static/src/js/models/base.js',
58 'static/src/js/models/query.js',
59 'static/src/js/models/iterator.js',
60 'static/src/js/models/state.js',
61
62 # backbone base collections
63 'static/src/js/collections/base.js',
64 'static/src/js/collections/pager.js',
65 'static/src/js/collections/group.js',
66 'static/src/js/collections/iterator.js',
67
68 # backbone base views
69 'static/src/js/views/view.js',
70 'static/src/js/views/base.js',
71 'static/src/js/views/region.js',
72 'static/src/js/views/pager.js',
73 'static/src/js/views/panel.js',
74 'static/src/js/views/unleashed.js',
75 ],
76
77 'test': [
78 'static/src/tests/group.js',
79 'static/src/tests/pager.js',
80 'static/src/tests/connector.js',
81 ]
82}
083
=== added directory 'web_unleashed/static'
=== added directory 'web_unleashed/static/lib'
=== added directory 'web_unleashed/static/lib/backbone'
=== added file 'web_unleashed/static/lib/backbone/backbone.js'
--- web_unleashed/static/lib/backbone/backbone.js 1970-01-01 00:00:00 +0000
+++ web_unleashed/static/lib/backbone/backbone.js 2014-01-28 13:12:54 +0000
@@ -0,0 +1,1581 @@
1// Backbone.js 1.1.0
2
3// (c) 2010-2011 Jeremy Ashkenas, DocumentCloud Inc.
4// (c) 2011-2013 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors
5// Backbone may be freely distributed under the MIT license.
6// For all details and documentation:
7// http://backbonejs.org
8
9(function(){
10
11 // Initial Setup
12 // -------------
13
14 // Save a reference to the global object (`window` in the browser, `exports`
15 // on the server).
16 var root = this;
17
18 // Save the previous value of the `Backbone` variable, so that it can be
19 // restored later on, if `noConflict` is used.
20 var previousBackbone = root.Backbone;
21
22 // Create local references to array methods we'll want to use later.
23 var array = [];
24 var push = array.push;
25 var slice = array.slice;
26 var splice = array.splice;
27
28 // The top-level namespace. All public Backbone classes and modules will
29 // be attached to this. Exported for both the browser and the server.
30 var Backbone;
31 if (typeof exports !== 'undefined') {
32 Backbone = exports;
33 } else {
34 Backbone = root.Backbone = {};
35 }
36
37 // Current version of the library. Keep in sync with `package.json`.
38 Backbone.VERSION = '1.1.0';
39
40 // Require Underscore, if we're on the server, and it's not already present.
41 var _ = root._;
42 if (!_ && (typeof require !== 'undefined')) _ = require('underscore');
43
44 // For Backbone's purposes, jQuery, Zepto, Ender, or My Library (kidding) owns
45 // the `$` variable.
46 Backbone.$ = root.jQuery || root.Zepto || root.ender || root.$;
47
48 // Runs Backbone.js in *noConflict* mode, returning the `Backbone` variable
49 // to its previous owner. Returns a reference to this Backbone object.
50 Backbone.noConflict = function() {
51 root.Backbone = previousBackbone;
52 return this;
53 };
54
55 // Turn on `emulateHTTP` to support legacy HTTP servers. Setting this option
56 // will fake `"PATCH"`, `"PUT"` and `"DELETE"` requests via the `_method` parameter and
57 // set a `X-Http-Method-Override` header.
58 Backbone.emulateHTTP = false;
59
60 // Turn on `emulateJSON` to support legacy servers that can't deal with direct
61 // `application/json` requests ... will encode the body as
62 // `application/x-www-form-urlencoded` instead and will send the model in a
63 // form param named `model`.
64 Backbone.emulateJSON = false;
65
66 // Backbone.Events
67 // ---------------
68
69 // A module that can be mixed in to *any object* in order to provide it with
70 // custom events. You may bind with `on` or remove with `off` callback
71 // functions to an event; `trigger`-ing an event fires all callbacks in
72 // succession.
73 //
74 // var object = {};
75 // _.extend(object, Backbone.Events);
76 // object.on('expand', function(){ alert('expanded'); });
77 // object.trigger('expand');
78 //
79 var Events = Backbone.Events = {
80
81 // Bind an event to a `callback` function. Passing `"all"` will bind
82 // the callback to all events fired.
83 on: function(name, callback, context) {
84 if (!eventsApi(this, 'on', name, [callback, context]) || !callback) return this;
85 this._events || (this._events = {});
86 var events = this._events[name] || (this._events[name] = []);
87 events.push({callback: callback, context: context, ctx: context || this});
88 return this;
89 },
90
91 // Bind an event to only be triggered a single time. After the first time
92 // the callback is invoked, it will be removed.
93 once: function(name, callback, context) {
94 if (!eventsApi(this, 'once', name, [callback, context]) || !callback) return this;
95 var self = this;
96 var once = _.once(function() {
97 self.off(name, once);
98 callback.apply(this, arguments);
99 });
100 once._callback = callback;
101 return this.on(name, once, context);
102 },
103
104 // Remove one or many callbacks. If `context` is null, removes all
105 // callbacks with that function. If `callback` is null, removes all
106 // callbacks for the event. If `name` is null, removes all bound
107 // callbacks for all events.
108 off: function(name, callback, context) {
109 var retain, ev, events, names, i, l, j, k;
110 if (!this._events || !eventsApi(this, 'off', name, [callback, context])) return this;
111 if (!name && !callback && !context) {
112 this._events = {};
113 return this;
114 }
115 names = name ? [name] : _.keys(this._events);
116 for (i = 0, l = names.length; i < l; i++) {
117 name = names[i];
118 if (events = this._events[name]) {
119 this._events[name] = retain = [];
120 if (callback || context) {
121 for (j = 0, k = events.length; j < k; j++) {
122 ev = events[j];
123 if ((callback && callback !== ev.callback && callback !== ev.callback._callback) ||
124 (context && context !== ev.context)) {
125 retain.push(ev);
126 }
127 }
128 }
129 if (!retain.length) delete this._events[name];
130 }
131 }
132
133 return this;
134 },
135
136 // Trigger one or many events, firing all bound callbacks. Callbacks are
137 // passed the same arguments as `trigger` is, apart from the event name
138 // (unless you're listening on `"all"`, which will cause your callback to
139 // receive the true name of the event as the first argument).
140 trigger: function(name) {
141 if (!this._events) return this;
142 var args = slice.call(arguments, 1);
143 if (!eventsApi(this, 'trigger', name, args)) return this;
144 var events = this._events[name];
145 var allEvents = this._events.all;
146 if (events) triggerEvents(events, args);
147 if (allEvents) triggerEvents(allEvents, arguments);
148 return this;
149 },
150
151 // Tell this object to stop listening to either specific events ... or
152 // to every object it's currently listening to.
153 stopListening: function(obj, name, callback) {
154 var listeningTo = this._listeningTo;
155 if (!listeningTo) return this;
156 var remove = !name && !callback;
157 if (!callback && typeof name === 'object') callback = this;
158 if (obj) (listeningTo = {})[obj._listenId] = obj;
159 for (var id in listeningTo) {
160 obj = listeningTo[id];
161 obj.off(name, callback, this);
162 if (remove || _.isEmpty(obj._events)) delete this._listeningTo[id];
163 }
164 return this;
165 }
166
167 };
168
169 // Regular expression used to split event strings.
170 var eventSplitter = /\s+/;
171
172 // Implement fancy features of the Events API such as multiple event
173 // names `"change blur"` and jQuery-style event maps `{change: action}`
174 // in terms of the existing API.
175 var eventsApi = function(obj, action, name, rest) {
176 if (!name) return true;
177
178 // Handle event maps.
179 if (typeof name === 'object') {
180 for (var key in name) {
181 obj[action].apply(obj, [key, name[key]].concat(rest));
182 }
183 return false;
184 }
185
186 // Handle space separated event names.
187 if (eventSplitter.test(name)) {
188 var names = name.split(eventSplitter);
189 for (var i = 0, l = names.length; i < l; i++) {
190 obj[action].apply(obj, [names[i]].concat(rest));
191 }
192 return false;
193 }
194
195 return true;
196 };
197
198 // A difficult-to-believe, but optimized internal dispatch function for
199 // triggering events. Tries to keep the usual cases speedy (most internal
200 // Backbone events have 3 arguments).
201 var triggerEvents = function(events, args) {
202 var ev, i = -1, l = events.length, a1 = args[0], a2 = args[1], a3 = args[2];
203 switch (args.length) {
204 case 0: while (++i < l) (ev = events[i]).callback.call(ev.ctx); return;
205 case 1: while (++i < l) (ev = events[i]).callback.call(ev.ctx, a1); return;
206 case 2: while (++i < l) (ev = events[i]).callback.call(ev.ctx, a1, a2); return;
207 case 3: while (++i < l) (ev = events[i]).callback.call(ev.ctx, a1, a2, a3); return;
208 default: while (++i < l) (ev = events[i]).callback.apply(ev.ctx, args);
209 }
210 };
211
212 var listenMethods = {listenTo: 'on', listenToOnce: 'once'};
213
214 // Inversion-of-control versions of `on` and `once`. Tell *this* object to
215 // listen to an event in another object ... keeping track of what it's
216 // listening to.
217 _.each(listenMethods, function(implementation, method) {
218 Events[method] = function(obj, name, callback) {
219 var listeningTo = this._listeningTo || (this._listeningTo = {});
220 var id = obj._listenId || (obj._listenId = _.uniqueId('l'));
221 listeningTo[id] = obj;
222 if (!callback && typeof name === 'object') callback = this;
223 obj[implementation](name, callback, this);
224 return this;
225 };
226 });
227
228 // Aliases for backwards compatibility.
229 Events.bind = Events.on;
230 Events.unbind = Events.off;
231
232 // Allow the `Backbone` object to serve as a global event bus, for folks who
233 // want global "pubsub" in a convenient place.
234 _.extend(Backbone, Events);
235
236 // Backbone.Model
237 // --------------
238
239 // Backbone **Models** are the basic data object in the framework --
240 // frequently representing a row in a table in a database on your server.
241 // A discrete chunk of data and a bunch of useful, related methods for
242 // performing computations and transformations on that data.
243
244 // Create a new model with the specified attributes. A client id (`cid`)
245 // is automatically generated and assigned for you.
246 var Model = Backbone.Model = function(attributes, options) {
247 var attrs = attributes || {};
248 options || (options = {});
249 this.cid = _.uniqueId('c');
250 this.attributes = {};
251 if (options.collection) this.collection = options.collection;
252 if (options.parse) attrs = this.parse(attrs, options) || {};
253 attrs = _.defaults({}, attrs, _.result(this, 'defaults'));
254 this.set(attrs, options);
255 this.changed = {};
256 this.initialize.apply(this, arguments);
257 };
258
259 // Attach all inheritable methods to the Model prototype.
260 _.extend(Model.prototype, Events, {
261
262 // A hash of attributes whose current and previous value differ.
263 changed: null,
264
265 // The value returned during the last failed validation.
266 validationError: null,
267
268 // The default name for the JSON `id` attribute is `"id"`. MongoDB and
269 // CouchDB users may want to set this to `"_id"`.
270 idAttribute: 'id',
271
272 // Initialize is an empty function by default. Override it with your own
273 // initialization logic.
274 initialize: function(){},
275
276 // Return a copy of the model's `attributes` object.
277 toJSON: function(options) {
278 return _.clone(this.attributes);
279 },
280
281 // Proxy `Backbone.sync` by default -- but override this if you need
282 // custom syncing semantics for *this* particular model.
283 sync: function() {
284 return Backbone.sync.apply(this, arguments);
285 },
286
287 // Get the value of an attribute.
288 get: function(attr) {
289 return this.attributes[attr];
290 },
291
292 // Get the HTML-escaped value of an attribute.
293 escape: function(attr) {
294 return _.escape(this.get(attr));
295 },
296
297 // Returns `true` if the attribute contains a value that is not null
298 // or undefined.
299 has: function(attr) {
300 return this.get(attr) != null;
301 },
302
303 // Set a hash of model attributes on the object, firing `"change"`. This is
304 // the core primitive operation of a model, updating the data and notifying
305 // anyone who needs to know about the change in state. The heart of the beast.
306 set: function(key, val, options) {
307 var attr, attrs, unset, changes, silent, changing, prev, current;
308 if (key == null) return this;
309
310 // Handle both `"key", value` and `{key: value}` -style arguments.
311 if (typeof key === 'object') {
312 attrs = key;
313 options = val;
314 } else {
315 (attrs = {})[key] = val;
316 }
317
318 options || (options = {});
319
320 // Run validation.
321 if (!this._validate(attrs, options)) return false;
322
323 // Extract attributes and options.
324 unset = options.unset;
325 silent = options.silent;
326 changes = [];
327 changing = this._changing;
328 this._changing = true;
329
330 if (!changing) {
331 this._previousAttributes = _.clone(this.attributes);
332 this.changed = {};
333 }
334 current = this.attributes, prev = this._previousAttributes;
335
336 // Check for changes of `id`.
337 if (this.idAttribute in attrs) this.id = attrs[this.idAttribute];
338
339 // For each `set` attribute, update or delete the current value.
340 for (attr in attrs) {
341 val = attrs[attr];
342 if (!_.isEqual(current[attr], val)) changes.push(attr);
343 if (!_.isEqual(prev[attr], val)) {
344 this.changed[attr] = val;
345 } else {
346 delete this.changed[attr];
347 }
348 unset ? delete current[attr] : current[attr] = val;
349 }
350
351 // Trigger all relevant attribute changes.
352 if (!silent) {
353 if (changes.length) this._pending = true;
354 for (var i = 0, l = changes.length; i < l; i++) {
355 this.trigger('change:' + changes[i], this, current[changes[i]], options);
356 }
357 }
358
359 // You might be wondering why there's a `while` loop here. Changes can
360 // be recursively nested within `"change"` events.
361 if (changing) return this;
362 if (!silent) {
363 while (this._pending) {
364 this._pending = false;
365 this.trigger('change', this, options);
366 }
367 }
368 this._pending = false;
369 this._changing = false;
370 return this;
371 },
372
373 // Remove an attribute from the model, firing `"change"`. `unset` is a noop
374 // if the attribute doesn't exist.
375 unset: function(attr, options) {
376 return this.set(attr, void 0, _.extend({}, options, {unset: true}));
377 },
378
379 // Clear all attributes on the model, firing `"change"`.
380 clear: function(options) {
381 var attrs = {};
382 for (var key in this.attributes) attrs[key] = void 0;
383 return this.set(attrs, _.extend({}, options, {unset: true}));
384 },
385
386 // Determine if the model has changed since the last `"change"` event.
387 // If you specify an attribute name, determine if that attribute has changed.
388 hasChanged: function(attr) {
389 if (attr == null) return !_.isEmpty(this.changed);
390 return _.has(this.changed, attr);
391 },
392
393 // Return an object containing all the attributes that have changed, or
394 // false if there are no changed attributes. Useful for determining what
395 // parts of a view need to be updated and/or what attributes need to be
396 // persisted to the server. Unset attributes will be set to undefined.
397 // You can also pass an attributes object to diff against the model,
398 // determining if there *would be* a change.
399 changedAttributes: function(diff) {
400 if (!diff) return this.hasChanged() ? _.clone(this.changed) : false;
401 var val, changed = false;
402 var old = this._changing ? this._previousAttributes : this.attributes;
403 for (var attr in diff) {
404 if (_.isEqual(old[attr], (val = diff[attr]))) continue;
405 (changed || (changed = {}))[attr] = val;
406 }
407 return changed;
408 },
409
410 // Get the previous value of an attribute, recorded at the time the last
411 // `"change"` event was fired.
412 previous: function(attr) {
413 if (attr == null || !this._previousAttributes) return null;
414 return this._previousAttributes[attr];
415 },
416
417 // Get all of the attributes of the model at the time of the previous
418 // `"change"` event.
419 previousAttributes: function() {
420 return _.clone(this._previousAttributes);
421 },
422
423 // Fetch the model from the server. If the server's representation of the
424 // model differs from its current attributes, they will be overridden,
425 // triggering a `"change"` event.
426 fetch: function(options) {
427 options = options ? _.clone(options) : {};
428 if (options.parse === void 0) options.parse = true;
429 var model = this;
430 var success = options.success;
431 options.success = function(resp) {
432 if (!model.set(model.parse(resp, options), options)) return false;
433 if (success) success(model, resp, options);
434 model.trigger('sync', model, resp, options);
435 };
436 wrapError(this, options);
437 return this.sync('read', this, options);
438 },
439
440 // Set a hash of model attributes, and sync the model to the server.
441 // If the server returns an attributes hash that differs, the model's
442 // state will be `set` again.
443 save: function(key, val, options) {
444 var attrs, method, xhr, attributes = this.attributes;
445
446 // Handle both `"key", value` and `{key: value}` -style arguments.
447 if (key == null || typeof key === 'object') {
448 attrs = key;
449 options = val;
450 } else {
451 (attrs = {})[key] = val;
452 }
453
454 options = _.extend({validate: true}, options);
455
456 // If we're not waiting and attributes exist, save acts as
457 // `set(attr).save(null, opts)` with validation. Otherwise, check if
458 // the model will be valid when the attributes, if any, are set.
459 if (attrs && !options.wait) {
460 if (!this.set(attrs, options)) return false;
461 } else {
462 if (!this._validate(attrs, options)) return false;
463 }
464
465 // Set temporary attributes if `{wait: true}`.
466 if (attrs && options.wait) {
467 this.attributes = _.extend({}, attributes, attrs);
468 }
469
470 // After a successful server-side save, the client is (optionally)
471 // updated with the server-side state.
472 if (options.parse === void 0) options.parse = true;
473 var model = this;
474 var success = options.success;
475 options.success = function(resp) {
476 // Ensure attributes are restored during synchronous saves.
477 model.attributes = attributes;
478 var serverAttrs = model.parse(resp, options);
479 if (options.wait) serverAttrs = _.extend(attrs || {}, serverAttrs);
480 if (_.isObject(serverAttrs) && !model.set(serverAttrs, options)) {
481 return false;
482 }
483 if (success) success(model, resp, options);
484 model.trigger('sync', model, resp, options);
485 };
486 wrapError(this, options);
487
488 method = this.isNew() ? 'create' : (options.patch ? 'patch' : 'update');
489 if (method === 'patch') options.attrs = attrs;
490 xhr = this.sync(method, this, options);
491
492 // Restore attributes.
493 if (attrs && options.wait) this.attributes = attributes;
494
495 return xhr;
496 },
497
498 // Destroy this model on the server if it was already persisted.
499 // Optimistically removes the model from its collection, if it has one.
500 // If `wait: true` is passed, waits for the server to respond before removal.
501 destroy: function(options) {
502 options = options ? _.clone(options) : {};
503 var model = this;
504 var success = options.success;
505
506 var destroy = function() {
507 model.trigger('destroy', model, model.collection, options);
508 };
509
510 options.success = function(resp) {
511 if (options.wait || model.isNew()) destroy();
512 if (success) success(model, resp, options);
513 if (!model.isNew()) model.trigger('sync', model, resp, options);
514 };
515
516 if (this.isNew()) {
517 options.success();
518 return false;
519 }
520 wrapError(this, options);
521
522 var xhr = this.sync('delete', this, options);
523 if (!options.wait) destroy();
524 return xhr;
525 },
526
527 // Default URL for the model's representation on the server -- if you're
528 // using Backbone's restful methods, override this to change the endpoint
529 // that will be called.
530 url: function() {
531 var base = _.result(this, 'urlRoot') || _.result(this.collection, 'url') || urlError();
532 if (this.isNew()) return base;
533 return base + (base.charAt(base.length - 1) === '/' ? '' : '/') + encodeURIComponent(this.id);
534 },
535
536 // **parse** converts a response into the hash of attributes to be `set` on
537 // the model. The default implementation is just to pass the response along.
538 parse: function(resp, options) {
539 return resp;
540 },
541
542 // Create a new model with identical attributes to this one.
543 clone: function() {
544 return new this.constructor(this.attributes);
545 },
546
547 // A model is new if it has never been saved to the server, and lacks an id.
548 isNew: function() {
549 return this.id == null;
550 },
551
552 // Check if the model is currently in a valid state.
553 isValid: function(options) {
554 return this._validate({}, _.extend(options || {}, { validate: true }));
555 },
556
557 // Run validation against the next complete set of model attributes,
558 // returning `true` if all is well. Otherwise, fire an `"invalid"` event.
559 _validate: function(attrs, options) {
560 if (!options.validate || !this.validate) return true;
561 attrs = _.extend({}, this.attributes, attrs);
562 var error = this.validationError = this.validate(attrs, options) || null;
563 if (!error) return true;
564 this.trigger('invalid', this, error, _.extend(options, {validationError: error}));
565 return false;
566 }
567
568 });
569
570 // Underscore methods that we want to implement on the Model.
571 var modelMethods = ['keys', 'values', 'pairs', 'invert', 'pick', 'omit'];
572
573 // Mix in each Underscore method as a proxy to `Model#attributes`.
574 _.each(modelMethods, function(method) {
575 Model.prototype[method] = function() {
576 var args = slice.call(arguments);
577 args.unshift(this.attributes);
578 return _[method].apply(_, args);
579 };
580 });
581
582 // Backbone.Collection
583 // -------------------
584
585 // If models tend to represent a single row of data, a Backbone Collection is
586 // more analagous to a table full of data ... or a small slice or page of that
587 // table, or a collection of rows that belong together for a particular reason
588 // -- all of the messages in this particular folder, all of the documents
589 // belonging to this particular author, and so on. Collections maintain
590 // indexes of their models, both in order, and for lookup by `id`.
591
592 // Create a new **Collection**, perhaps to contain a specific type of `model`.
593 // If a `comparator` is specified, the Collection will maintain
594 // its models in sort order, as they're added and removed.
595 var Collection = Backbone.Collection = function(models, options) {
596 options || (options = {});
597 if (options.model) this.model = options.model;
598 if (options.comparator !== void 0) this.comparator = options.comparator;
599 this._reset();
600 this.initialize.apply(this, arguments);
601 if (models) this.reset(models, _.extend({silent: true}, options));
602 };
603
604 // Default options for `Collection#set`.
605 var setOptions = {add: true, remove: true, merge: true};
606 var addOptions = {add: true, remove: false};
607
608 // Define the Collection's inheritable methods.
609 _.extend(Collection.prototype, Events, {
610
611 // The default model for a collection is just a **Backbone.Model**.
612 // This should be overridden in most cases.
613 model: Model,
614
615 // Initialize is an empty function by default. Override it with your own
616 // initialization logic.
617 initialize: function(){},
618
619 // The JSON representation of a Collection is an array of the
620 // models' attributes.
621 toJSON: function(options) {
622 return this.map(function(model){ return model.toJSON(options); });
623 },
624
625 // Proxy `Backbone.sync` by default.
626 sync: function() {
627 return Backbone.sync.apply(this, arguments);
628 },
629
630 // Add a model, or list of models to the set.
631 add: function(models, options) {
632 return this.set(models, _.extend({merge: false}, options, addOptions));
633 },
634
635 // Remove a model, or a list of models from the set.
636 remove: function(models, options) {
637 var singular = !_.isArray(models);
638 models = singular ? [models] : _.clone(models);
639 options || (options = {});
640 var i, l, index, model;
641 for (i = 0, l = models.length; i < l; i++) {
642 model = models[i] = this.get(models[i]);
643 if (!model) continue;
644 delete this._byId[model.id];
645 delete this._byId[model.cid];
646 index = this.indexOf(model);
647 this.models.splice(index, 1);
648 this.length--;
649 if (!options.silent) {
650 options.index = index;
651 model.trigger('remove', model, this, options);
652 }
653 this._removeReference(model);
654 }
655 return singular ? models[0] : models;
656 },
657
658 // Update a collection by `set`-ing a new list of models, adding new ones,
659 // removing models that are no longer present, and merging models that
660 // already exist in the collection, as necessary. Similar to **Model#set**,
661 // the core operation for updating the data contained by the collection.
662 set: function(models, options) {
663 options = _.defaults({}, options, setOptions);
664 if (options.parse) models = this.parse(models, options);
665 var singular = !_.isArray(models);
666 models = singular ? (models ? [models] : []) : _.clone(models);
667 var i, l, id, model, attrs, existing, sort;
668 var at = options.at;
669 var targetModel = this.model;
670 var sortable = this.comparator && (at == null) && options.sort !== false;
671 var sortAttr = _.isString(this.comparator) ? this.comparator : null;
672 var toAdd = [], toRemove = [], modelMap = {};
673 var add = options.add, merge = options.merge, remove = options.remove;
674 var order = !sortable && add && remove ? [] : false;
675
676 // Turn bare objects into model references, and prevent invalid models
677 // from being added.
678 for (i = 0, l = models.length; i < l; i++) {
679 attrs = models[i];
680 if (attrs instanceof Model) {
681 id = model = attrs;
682 } else {
683 id = attrs[targetModel.prototype.idAttribute];
684 }
685
686 // If a duplicate is found, prevent it from being added and
687 // optionally merge it into the existing model.
688 if (existing = this.get(id)) {
689 if (remove) modelMap[existing.cid] = true;
690 if (merge) {
691 attrs = attrs === model ? model.attributes : attrs;
692 if (options.parse) attrs = existing.parse(attrs, options);
693 existing.set(attrs, options);
694 if (sortable && !sort && existing.hasChanged(sortAttr)) sort = true;
695 }
696 models[i] = existing;
697
698 // If this is a new, valid model, push it to the `toAdd` list.
699 } else if (add) {
700 model = models[i] = this._prepareModel(attrs, options);
701 if (!model) continue;
702 toAdd.push(model);
703
704 // Listen to added models' events, and index models for lookup by
705 // `id` and by `cid`.
706 model.on('all', this._onModelEvent, this);
707 this._byId[model.cid] = model;
708 if (model.id != null) this._byId[model.id] = model;
709 }
710 if (order) order.push(existing || model);
711 }
712
713 // Remove nonexistent models if appropriate.
714 if (remove) {
715 for (i = 0, l = this.length; i < l; ++i) {
716 if (!modelMap[(model = this.models[i]).cid]) toRemove.push(model);
717 }
718 if (toRemove.length) this.remove(toRemove, options);
719 }
720
721 // See if sorting is needed, update `length` and splice in new models.
722 if (toAdd.length || (order && order.length)) {
723 if (sortable) sort = true;
724 this.length += toAdd.length;
725 if (at != null) {
726 for (i = 0, l = toAdd.length; i < l; i++) {
727 this.models.splice(at + i, 0, toAdd[i]);
728 }
729 } else {
730 if (order) this.models.length = 0;
731 var orderedModels = order || toAdd;
732 for (i = 0, l = orderedModels.length; i < l; i++) {
733 this.models.push(orderedModels[i]);
734 }
735 }
736 }
737
738 // Silently sort the collection if appropriate.
739 if (sort) this.sort({silent: true});
740
741 // Unless silenced, it's time to fire all appropriate add/sort events.
742 if (!options.silent) {
743 for (i = 0, l = toAdd.length; i < l; i++) {
744 (model = toAdd[i]).trigger('add', model, this, options);
745 }
746 if (sort || (order && order.length)) this.trigger('sort', this, options);
747 }
748
749 // Return the added (or merged) model (or models).
750 return singular ? models[0] : models;
751 },
752
753 // When you have more items than you want to add or remove individually,
754 // you can reset the entire set with a new list of models, without firing
755 // any granular `add` or `remove` events. Fires `reset` when finished.
756 // Useful for bulk operations and optimizations.
757 reset: function(models, options) {
758 options || (options = {});
759 for (var i = 0, l = this.models.length; i < l; i++) {
760 this._removeReference(this.models[i]);
761 }
762 options.previousModels = this.models;
763 this._reset();
764 models = this.add(models, _.extend({silent: true}, options));
765 if (!options.silent) this.trigger('reset', this, options);
766 return models;
767 },
768
769 // Add a model to the end of the collection.
770 push: function(model, options) {
771 return this.add(model, _.extend({at: this.length}, options));
772 },
773
774 // Remove a model from the end of the collection.
775 pop: function(options) {
776 var model = this.at(this.length - 1);
777 this.remove(model, options);
778 return model;
779 },
780
781 // Add a model to the beginning of the collection.
782 unshift: function(model, options) {
783 return this.add(model, _.extend({at: 0}, options));
784 },
785
786 // Remove a model from the beginning of the collection.
787 shift: function(options) {
788 var model = this.at(0);
789 this.remove(model, options);
790 return model;
791 },
792
793 // Slice out a sub-array of models from the collection.
794 slice: function() {
795 return slice.apply(this.models, arguments);
796 },
797
798 // Get a model from the set by id.
799 get: function(obj) {
800 if (obj == null) return void 0;
801 return this._byId[obj.id] || this._byId[obj.cid] || this._byId[obj];
802 },
803
804 // Get the model at the given index.
805 at: function(index) {
806 return this.models[index];
807 },
808
809 // Return models with matching attributes. Useful for simple cases of
810 // `filter`.
811 where: function(attrs, first) {
812 if (_.isEmpty(attrs)) return first ? void 0 : [];
813 return this[first ? 'find' : 'filter'](function(model) {
814 for (var key in attrs) {
815 if (attrs[key] !== model.get(key)) return false;
816 }
817 return true;
818 });
819 },
820
821 // Return the first model with matching attributes. Useful for simple cases
822 // of `find`.
823 findWhere: function(attrs) {
824 return this.where(attrs, true);
825 },
826
827 // Force the collection to re-sort itself. You don't need to call this under
828 // normal circumstances, as the set will maintain sort order as each item
829 // is added.
830 sort: function(options) {
831 if (!this.comparator) throw new Error('Cannot sort a set without a comparator');
832 options || (options = {});
833
834 // Run sort based on type of `comparator`.
835 if (_.isString(this.comparator) || this.comparator.length === 1) {
836 this.models = this.sortBy(this.comparator, this);
837 } else {
838 this.models.sort(_.bind(this.comparator, this));
839 }
840
841 if (!options.silent) this.trigger('sort', this, options);
842 return this;
843 },
844
845 // Pluck an attribute from each model in the collection.
846 pluck: function(attr) {
847 return _.invoke(this.models, 'get', attr);
848 },
849
850 // Fetch the default set of models for this collection, resetting the
851 // collection when they arrive. If `reset: true` is passed, the response
852 // data will be passed through the `reset` method instead of `set`.
853 fetch: function(options) {
854 options = options ? _.clone(options) : {};
855 if (options.parse === void 0) options.parse = true;
856 var success = options.success;
857 var collection = this;
858 options.success = function(resp) {
859 var method = options.reset ? 'reset' : 'set';
860 collection[method](resp, options);
861 if (success) success(collection, resp, options);
862 collection.trigger('sync', collection, resp, options);
863 };
864 wrapError(this, options);
865 return this.sync('read', this, options);
866 },
867
868 // Create a new instance of a model in this collection. Add the model to the
869 // collection immediately, unless `wait: true` is passed, in which case we
870 // wait for the server to agree.
871 create: function(model, options) {
872 options = options ? _.clone(options) : {};
873 if (!(model = this._prepareModel(model, options))) return false;
874 if (!options.wait) this.add(model, options);
875 var collection = this;
876 var success = options.success;
877 options.success = function(model, resp, options) {
878 if (options.wait) collection.add(model, options);
879 if (success) success(model, resp, options);
880 };
881 model.save(null, options);
882 return model;
883 },
884
885 // **parse** converts a response into a list of models to be added to the
886 // collection. The default implementation is just to pass it through.
887 parse: function(resp, options) {
888 return resp;
889 },
890
891 // Create a new collection with an identical list of models as this one.
892 clone: function() {
893 return new this.constructor(this.models);
894 },
895
896 // Private method to reset all internal state. Called when the collection
897 // is first initialized or reset.
898 _reset: function() {
899 this.length = 0;
900 this.models = [];
901 this._byId = {};
902 },
903
904 // Prepare a hash of attributes (or other model) to be added to this
905 // collection.
906 _prepareModel: function(attrs, options) {
907 if (attrs instanceof Model) {
908 if (!attrs.collection) attrs.collection = this;
909 return attrs;
910 }
911 options = options ? _.clone(options) : {};
912 options.collection = this;
913 var model = new this.model(attrs, options);
914 if (!model.validationError) return model;
915 this.trigger('invalid', this, model.validationError, options);
916 return false;
917 },
918
919 // Internal method to sever a model's ties to a collection.
920 _removeReference: function(model) {
921 if (this === model.collection) delete model.collection;
922 model.off('all', this._onModelEvent, this);
923 },
924
925 // Internal method called every time a model in the set fires an event.
926 // Sets need to update their indexes when models change ids. All other
927 // events simply proxy through. "add" and "remove" events that originate
928 // in other collections are ignored.
929 _onModelEvent: function(event, model, collection, options) {
930 if ((event === 'add' || event === 'remove') && collection !== this) return;
931 if (event === 'destroy') this.remove(model, options);
932 if (model && event === 'change:' + model.idAttribute) {
933 delete this._byId[model.previous(model.idAttribute)];
934 if (model.id != null) this._byId[model.id] = model;
935 }
936 this.trigger.apply(this, arguments);
937 }
938
939 });
940
941 // Underscore methods that we want to implement on the Collection.
942 // 90% of the core usefulness of Backbone Collections is actually implemented
943 // right here:
944 var methods = ['forEach', 'each', 'map', 'collect', 'reduce', 'foldl',
945 'inject', 'reduceRight', 'foldr', 'find', 'detect', 'filter', 'select',
946 'reject', 'every', 'all', 'some', 'any', 'include', 'contains', 'invoke',
947 'max', 'min', 'toArray', 'size', 'first', 'head', 'take', 'initial', 'rest',
948 'tail', 'drop', 'last', 'without', 'difference', 'indexOf', 'shuffle',
949 'lastIndexOf', 'isEmpty', 'chain'];
950
951 // Mix in each Underscore method as a proxy to `Collection#models`.
952 _.each(methods, function(method) {
953 Collection.prototype[method] = function() {
954 var args = slice.call(arguments);
955 args.unshift(this.models);
956 return _[method].apply(_, args);
957 };
958 });
959
960 // Underscore methods that take a property name as an argument.
961 var attributeMethods = ['groupBy', 'countBy', 'sortBy'];
962
963 // Use attributes instead of properties.
964 _.each(attributeMethods, function(method) {
965 Collection.prototype[method] = function(value, context) {
966 var iterator = _.isFunction(value) ? value : function(model) {
967 return model.get(value);
968 };
969 return _[method](this.models, iterator, context);
970 };
971 });
972
973 // Backbone.View
974 // -------------
975
976 // Backbone Views are almost more convention than they are actual code. A View
977 // is simply a JavaScript object that represents a logical chunk of UI in the
978 // DOM. This might be a single item, an entire list, a sidebar or panel, or
979 // even the surrounding frame which wraps your whole app. Defining a chunk of
980 // UI as a **View** allows you to define your DOM events declaratively, without
981 // having to worry about render order ... and makes it easy for the view to
982 // react to specific changes in the state of your models.
983
984 // Creating a Backbone.View creates its initial element outside of the DOM,
985 // if an existing element is not provided...
986 var View = Backbone.View = function(options) {
987 this.cid = _.uniqueId('view');
988 options || (options = {});
989 _.extend(this, _.pick(options, viewOptions));
990 this._ensureElement();
991 this.initialize.apply(this, arguments);
992 this.delegateEvents();
993 };
994
995 // Cached regex to split keys for `delegate`.
996 var delegateEventSplitter = /^(\S+)\s*(.*)$/;
997
998 // List of view options to be merged as properties.
999 var viewOptions = ['model', 'collection', 'el', 'id', 'attributes', 'className', 'tagName', 'events'];
1000
1001 // Set up all inheritable **Backbone.View** properties and methods.
1002 _.extend(View.prototype, Events, {
1003
1004 // The default `tagName` of a View's element is `"div"`.
1005 tagName: 'div',
1006
1007 // jQuery delegate for element lookup, scoped to DOM elements within the
1008 // current view. This should be preferred to global lookups where possible.
1009 $: function(selector) {
1010 return this.$el.find(selector);
1011 },
1012
1013 // Initialize is an empty function by default. Override it with your own
1014 // initialization logic.
1015 initialize: function(){},
1016
1017 // **render** is the core function that your view should override, in order
1018 // to populate its element (`this.el`), with the appropriate HTML. The
1019 // convention is for **render** to always return `this`.
1020 render: function() {
1021 return this;
1022 },
1023
1024 // Remove this view by taking the element out of the DOM, and removing any
1025 // applicable Backbone.Events listeners.
1026 remove: function() {
1027 this.$el.remove();
1028 this.stopListening();
1029 return this;
1030 },
1031
1032 // Change the view's element (`this.el` property), including event
1033 // re-delegation.
1034 setElement: function(element, delegate) {
1035 if (this.$el) this.undelegateEvents();
1036 this.$el = element instanceof Backbone.$ ? element : Backbone.$(element);
1037 this.el = this.$el[0];
1038 if (delegate !== false) this.delegateEvents();
1039 return this;
1040 },
1041
1042 // Set callbacks, where `this.events` is a hash of
1043 //
1044 // *{"event selector": "callback"}*
1045 //
1046 // {
1047 // 'mousedown .title': 'edit',
1048 // 'click .button': 'save',
1049 // 'click .open': function(e) { ... }
1050 // }
1051 //
1052 // pairs. Callbacks will be bound to the view, with `this` set properly.
1053 // Uses event delegation for efficiency.
1054 // Omitting the selector binds the event to `this.el`.
1055 // This only works for delegate-able events: not `focus`, `blur`, and
1056 // not `change`, `submit`, and `reset` in Internet Explorer.
1057 delegateEvents: function(events) {
1058 if (!(events || (events = _.result(this, 'events')))) return this;
1059 this.undelegateEvents();
1060 for (var key in events) {
1061 var method = events[key];
1062 if (!_.isFunction(method)) method = this[events[key]];
1063 if (!method) continue;
1064
1065 var match = key.match(delegateEventSplitter);
1066 var eventName = match[1], selector = match[2];
1067 method = _.bind(method, this);
1068 eventName += '.delegateEvents' + this.cid;
1069 if (selector === '') {
1070 this.$el.on(eventName, method);
1071 } else {
1072 this.$el.on(eventName, selector, method);
1073 }
1074 }
1075 return this;
1076 },
1077
1078 // Clears all callbacks previously bound to the view with `delegateEvents`.
1079 // You usually don't need to use this, but may wish to if you have multiple
1080 // Backbone views attached to the same DOM element.
1081 undelegateEvents: function() {
1082 this.$el.off('.delegateEvents' + this.cid);
1083 return this;
1084 },
1085
1086 // Ensure that the View has a DOM element to render into.
1087 // If `this.el` is a string, pass it through `$()`, take the first
1088 // matching element, and re-assign it to `el`. Otherwise, create
1089 // an element from the `id`, `className` and `tagName` properties.
1090 _ensureElement: function() {
1091 if (!this.el) {
1092 var attrs = _.extend({}, _.result(this, 'attributes'));
1093 if (this.id) attrs.id = _.result(this, 'id');
1094 if (this.className) attrs['class'] = _.result(this, 'className');
1095 var $el = Backbone.$('<' + _.result(this, 'tagName') + '>').attr(attrs);
1096 this.setElement($el, false);
1097 } else {
1098 this.setElement(_.result(this, 'el'), false);
1099 }
1100 }
1101
1102 });
1103
1104 // Backbone.sync
1105 // -------------
1106
1107 // Override this function to change the manner in which Backbone persists
1108 // models to the server. You will be passed the type of request, and the
1109 // model in question. By default, makes a RESTful Ajax request
1110 // to the model's `url()`. Some possible customizations could be:
1111 //
1112 // * Use `setTimeout` to batch rapid-fire updates into a single request.
1113 // * Send up the models as XML instead of JSON.
1114 // * Persist models via WebSockets instead of Ajax.
1115 //
1116 // Turn on `Backbone.emulateHTTP` in order to send `PUT` and `DELETE` requests
1117 // as `POST`, with a `_method` parameter containing the true HTTP method,
1118 // as well as all requests with the body as `application/x-www-form-urlencoded`
1119 // instead of `application/json` with the model in a param named `model`.
1120 // Useful when interfacing with server-side languages like **PHP** that make
1121 // it difficult to read the body of `PUT` requests.
1122 Backbone.sync = function(method, model, options) {
1123 var type = methodMap[method];
1124
1125 // Default options, unless specified.
1126 _.defaults(options || (options = {}), {
1127 emulateHTTP: Backbone.emulateHTTP,
1128 emulateJSON: Backbone.emulateJSON
1129 });
1130
1131 // Default JSON-request options.
1132 var params = {type: type, dataType: 'json'};
1133
1134 // Ensure that we have a URL.
1135 if (!options.url) {
1136 params.url = _.result(model, 'url') || urlError();
1137 }
1138
1139 // Ensure that we have the appropriate request data.
1140 if (options.data == null && model && (method === 'create' || method === 'update' || method === 'patch')) {
1141 params.contentType = 'application/json';
1142 params.data = JSON.stringify(options.attrs || model.toJSON(options));
1143 }
1144
1145 // For older servers, emulate JSON by encoding the request into an HTML-form.
1146 if (options.emulateJSON) {
1147 params.contentType = 'application/x-www-form-urlencoded';
1148 params.data = params.data ? {model: params.data} : {};
1149 }
1150
1151 // For older servers, emulate HTTP by mimicking the HTTP method with `_method`
1152 // And an `X-HTTP-Method-Override` header.
1153 if (options.emulateHTTP && (type === 'PUT' || type === 'DELETE' || type === 'PATCH')) {
1154 params.type = 'POST';
1155 if (options.emulateJSON) params.data._method = type;
1156 var beforeSend = options.beforeSend;
1157 options.beforeSend = function(xhr) {
1158 xhr.setRequestHeader('X-HTTP-Method-Override', type);
1159 if (beforeSend) return beforeSend.apply(this, arguments);
1160 };
1161 }
1162
1163 // Don't process data on a non-GET request.
1164 if (params.type !== 'GET' && !options.emulateJSON) {
1165 params.processData = false;
1166 }
1167
1168 // If we're sending a `PATCH` request, and we're in an old Internet Explorer
1169 // that still has ActiveX enabled by default, override jQuery to use that
1170 // for XHR instead. Remove this line when jQuery supports `PATCH` on IE8.
1171 if (params.type === 'PATCH' && noXhrPatch) {
1172 params.xhr = function() {
1173 return new ActiveXObject("Microsoft.XMLHTTP");
1174 };
1175 }
1176
1177 // Make the request, allowing the user to override any Ajax options.
1178 var xhr = options.xhr = Backbone.ajax(_.extend(params, options));
1179 model.trigger('request', model, xhr, options);
1180 return xhr;
1181 };
1182
1183 var noXhrPatch = typeof window !== 'undefined' && !!window.ActiveXObject && !(window.XMLHttpRequest && (new XMLHttpRequest).dispatchEvent);
1184
1185 // Map from CRUD to HTTP for our default `Backbone.sync` implementation.
1186 var methodMap = {
1187 'create': 'POST',
1188 'update': 'PUT',
1189 'patch': 'PATCH',
1190 'delete': 'DELETE',
1191 'read': 'GET'
1192 };
1193
1194 // Set the default implementation of `Backbone.ajax` to proxy through to `$`.
1195 // Override this if you'd like to use a different library.
1196 Backbone.ajax = function() {
1197 return Backbone.$.ajax.apply(Backbone.$, arguments);
1198 };
1199
1200 // Backbone.Router
1201 // ---------------
1202
1203 // Routers map faux-URLs to actions, and fire events when routes are
1204 // matched. Creating a new one sets its `routes` hash, if not set statically.
1205 var Router = Backbone.Router = function(options) {
1206 options || (options = {});
1207 if (options.routes) this.routes = options.routes;
1208 this._bindRoutes();
1209 this.initialize.apply(this, arguments);
1210 };
1211
1212 // Cached regular expressions for matching named param parts and splatted
1213 // parts of route strings.
1214 var optionalParam = /\((.*?)\)/g;
1215 var namedParam = /(\(\?)?:\w+/g;
1216 var splatParam = /\*\w+/g;
1217 var escapeRegExp = /[\-{}\[\]+?.,\\\^$|#\s]/g;
1218
1219 // Set up all inheritable **Backbone.Router** properties and methods.
1220 _.extend(Router.prototype, Events, {
1221
1222 // Initialize is an empty function by default. Override it with your own
1223 // initialization logic.
1224 initialize: function(){},
1225
1226 // Manually bind a single named route to a callback. For example:
1227 //
1228 // this.route('search/:query/p:num', 'search', function(query, num) {
1229 // ...
1230 // });
1231 //
1232 route: function(route, name, callback) {
1233 if (!_.isRegExp(route)) route = this._routeToRegExp(route);
1234 if (_.isFunction(name)) {
1235 callback = name;
1236 name = '';
1237 }
1238 if (!callback) callback = this[name];
1239 var router = this;
1240 Backbone.history.route(route, function(fragment) {
1241 var args = router._extractParameters(route, fragment);
1242 callback && callback.apply(router, args);
1243 router.trigger.apply(router, ['route:' + name].concat(args));
1244 router.trigger('route', name, args);
1245 Backbone.history.trigger('route', router, name, args);
1246 });
1247 return this;
1248 },
1249
1250 // Simple proxy to `Backbone.history` to save a fragment into the history.
1251 navigate: function(fragment, options) {
1252 Backbone.history.navigate(fragment, options);
1253 return this;
1254 },
1255
1256 // Bind all defined routes to `Backbone.history`. We have to reverse the
1257 // order of the routes here to support behavior where the most general
1258 // routes can be defined at the bottom of the route map.
1259 _bindRoutes: function() {
1260 if (!this.routes) return;
1261 this.routes = _.result(this, 'routes');
1262 var route, routes = _.keys(this.routes);
1263 while ((route = routes.pop()) != null) {
1264 this.route(route, this.routes[route]);
1265 }
1266 },
1267
1268 // Convert a route string into a regular expression, suitable for matching
1269 // against the current location hash.
1270 _routeToRegExp: function(route) {
1271 route = route.replace(escapeRegExp, '\\$&')
1272 .replace(optionalParam, '(?:$1)?')
1273 .replace(namedParam, function(match, optional) {
1274 return optional ? match : '([^\/]+)';
1275 })
1276 .replace(splatParam, '(.*?)');
1277 return new RegExp('^' + route + '$');
1278 },
1279
1280 // Given a route, and a URL fragment that it matches, return the array of
1281 // extracted decoded parameters. Empty or unmatched parameters will be
1282 // treated as `null` to normalize cross-browser behavior.
1283 _extractParameters: function(route, fragment) {
1284 var params = route.exec(fragment).slice(1);
1285 return _.map(params, function(param) {
1286 return param ? decodeURIComponent(param) : null;
1287 });
1288 }
1289
1290 });
1291
1292 // Backbone.History
1293 // ----------------
1294
1295 // Handles cross-browser history management, based on either
1296 // [pushState](http://diveintohtml5.info/history.html) and real URLs, or
1297 // [onhashchange](https://developer.mozilla.org/en-US/docs/DOM/window.onhashchange)
1298 // and URL fragments. If the browser supports neither (old IE, natch),
1299 // falls back to polling.
1300 var History = Backbone.History = function() {
1301 this.handlers = [];
1302 _.bindAll(this, 'checkUrl');
1303
1304 // Ensure that `History` can be used outside of the browser.
1305 if (typeof window !== 'undefined') {
1306 this.location = window.location;
1307 this.history = window.history;
1308 }
1309 };
1310
1311 // Cached regex for stripping a leading hash/slash and trailing space.
1312 var routeStripper = /^[#\/]|\s+$/g;
1313
1314 // Cached regex for stripping leading and trailing slashes.
1315 var rootStripper = /^\/+|\/+$/g;
1316
1317 // Cached regex for detecting MSIE.
1318 var isExplorer = /msie [\w.]+/;
1319
1320 // Cached regex for removing a trailing slash.
1321 var trailingSlash = /\/$/;
1322
1323 // Cached regex for stripping urls of hash and query.
1324 var pathStripper = /[?#].*$/;
1325
1326 // Has the history handling already been started?
1327 History.started = false;
1328
1329 // Set up all inheritable **Backbone.History** properties and methods.
1330 _.extend(History.prototype, Events, {
1331
1332 // The default interval to poll for hash changes, if necessary, is
1333 // twenty times a second.
1334 interval: 50,
1335
1336 // Gets the true hash value. Cannot use location.hash directly due to bug
1337 // in Firefox where location.hash will always be decoded.
1338 getHash: function(window) {
1339 var match = (window || this).location.href.match(/#(.*)$/);
1340 return match ? match[1] : '';
1341 },
1342
1343 // Get the cross-browser normalized URL fragment, either from the URL,
1344 // the hash, or the override.
1345 getFragment: function(fragment, forcePushState) {
1346 if (fragment == null) {
1347 if (this._hasPushState || !this._wantsHashChange || forcePushState) {
1348 fragment = this.location.pathname;
1349 var root = this.root.replace(trailingSlash, '');
1350 if (!fragment.indexOf(root)) fragment = fragment.slice(root.length);
1351 } else {
1352 fragment = this.getHash();
1353 }
1354 }
1355 return fragment.replace(routeStripper, '');
1356 },
1357
1358 // Start the hash change handling, returning `true` if the current URL matches
1359 // an existing route, and `false` otherwise.
1360 start: function(options) {
1361 if (History.started) throw new Error("Backbone.history has already been started");
1362 History.started = true;
1363
1364 // Figure out the initial configuration. Do we need an iframe?
1365 // Is pushState desired ... is it available?
1366 this.options = _.extend({root: '/'}, this.options, options);
1367 this.root = this.options.root;
1368 this._wantsHashChange = this.options.hashChange !== false;
1369 this._wantsPushState = !!this.options.pushState;
1370 this._hasPushState = !!(this.options.pushState && this.history && this.history.pushState);
1371 var fragment = this.getFragment();
1372 var docMode = document.documentMode;
1373 var oldIE = (isExplorer.exec(navigator.userAgent.toLowerCase()) && (!docMode || docMode <= 7));
1374
1375 // Normalize root to always include a leading and trailing slash.
1376 this.root = ('/' + this.root + '/').replace(rootStripper, '/');
1377
1378 if (oldIE && this._wantsHashChange) {
1379 this.iframe = Backbone.$('<iframe src="javascript:0" tabindex="-1" />').hide().appendTo('body')[0].contentWindow;
1380 this.navigate(fragment);
1381 }
1382
1383 // Depending on whether we're using pushState or hashes, and whether
1384 // 'onhashchange' is supported, determine how we check the URL state.
1385 if (this._hasPushState) {
1386 Backbone.$(window).on('popstate', this.checkUrl);
1387 } else if (this._wantsHashChange && ('onhashchange' in window) && !oldIE) {
1388 Backbone.$(window).on('hashchange', this.checkUrl);
1389 } else if (this._wantsHashChange) {
1390 this._checkUrlInterval = setInterval(this.checkUrl, this.interval);
1391 }
1392
1393 // Determine if we need to change the base url, for a pushState link
1394 // opened by a non-pushState browser.
1395 this.fragment = fragment;
1396 var loc = this.location;
1397 var atRoot = loc.pathname.replace(/[^\/]$/, '$&/') === this.root;
1398
1399 // Transition from hashChange to pushState or vice versa if both are
1400 // requested.
1401 if (this._wantsHashChange && this._wantsPushState) {
1402
1403 // If we've started off with a route from a `pushState`-enabled
1404 // browser, but we're currently in a browser that doesn't support it...
1405 if (!this._hasPushState && !atRoot) {
1406 this.fragment = this.getFragment(null, true);
1407 this.location.replace(this.root + this.location.search + '#' + this.fragment);
1408 // Return immediately as browser will do redirect to new url
1409 return true;
1410
1411 // Or if we've started out with a hash-based route, but we're currently
1412 // in a browser where it could be `pushState`-based instead...
1413 } else if (this._hasPushState && atRoot && loc.hash) {
1414 this.fragment = this.getHash().replace(routeStripper, '');
1415 this.history.replaceState({}, document.title, this.root + this.fragment + loc.search);
1416 }
1417
1418 }
1419
1420 if (!this.options.silent) return this.loadUrl();
1421 },
1422
1423 // Disable Backbone.history, perhaps temporarily. Not useful in a real app,
1424 // but possibly useful for unit testing Routers.
1425 stop: function() {
1426 Backbone.$(window).off('popstate', this.checkUrl).off('hashchange', this.checkUrl);
1427 clearInterval(this._checkUrlInterval);
1428 History.started = false;
1429 },
1430
1431 // Add a route to be tested when the fragment changes. Routes added later
1432 // may override previous routes.
1433 route: function(route, callback) {
1434 this.handlers.unshift({route: route, callback: callback});
1435 },
1436
1437 // Checks the current URL to see if it has changed, and if it has,
1438 // calls `loadUrl`, normalizing across the hidden iframe.
1439 checkUrl: function(e) {
1440 var current = this.getFragment();
1441 if (current === this.fragment && this.iframe) {
1442 current = this.getFragment(this.getHash(this.iframe));
1443 }
1444 if (current === this.fragment) return false;
1445 if (this.iframe) this.navigate(current);
1446 this.loadUrl();
1447 },
1448
1449 // Attempt to load the current URL fragment. If a route succeeds with a
1450 // match, returns `true`. If no defined routes matches the fragment,
1451 // returns `false`.
1452 loadUrl: function(fragment) {
1453 fragment = this.fragment = this.getFragment(fragment);
1454 return _.any(this.handlers, function(handler) {
1455 if (handler.route.test(fragment)) {
1456 handler.callback(fragment);
1457 return true;
1458 }
1459 });
1460 },
1461
1462 // Save a fragment into the hash history, or replace the URL state if the
1463 // 'replace' option is passed. You are responsible for properly URL-encoding
1464 // the fragment in advance.
1465 //
1466 // The options object can contain `trigger: true` if you wish to have the
1467 // route callback be fired (not usually desirable), or `replace: true`, if
1468 // you wish to modify the current URL without adding an entry to the history.
1469 navigate: function(fragment, options) {
1470 if (!History.started) return false;
1471 if (!options || options === true) options = {trigger: !!options};
1472
1473 var url = this.root + (fragment = this.getFragment(fragment || ''));
1474
1475 // Strip the fragment of the query and hash for matching.
1476 fragment = fragment.replace(pathStripper, '');
1477
1478 if (this.fragment === fragment) return;
1479 this.fragment = fragment;
1480
1481 // Don't include a trailing slash on the root.
1482 if (fragment === '' && url !== '/') url = url.slice(0, -1);
1483
1484 // If pushState is available, we use it to set the fragment as a real URL.
1485 if (this._hasPushState) {
1486 this.history[options.replace ? 'replaceState' : 'pushState']({}, document.title, url);
1487
1488 // If hash changes haven't been explicitly disabled, update the hash
1489 // fragment to store history.
1490 } else if (this._wantsHashChange) {
1491 this._updateHash(this.location, fragment, options.replace);
1492 if (this.iframe && (fragment !== this.getFragment(this.getHash(this.iframe)))) {
1493 // Opening and closing the iframe tricks IE7 and earlier to push a
1494 // history entry on hash-tag change. When replace is true, we don't
1495 // want this.
1496 if(!options.replace) this.iframe.document.open().close();
1497 this._updateHash(this.iframe.location, fragment, options.replace);
1498 }
1499
1500 // If you've told us that you explicitly don't want fallback hashchange-
1501 // based history, then `navigate` becomes a page refresh.
1502 } else {
1503 return this.location.assign(url);
1504 }
1505 if (options.trigger) return this.loadUrl(fragment);
1506 },
1507
1508 // Update the hash location, either replacing the current entry, or adding
1509 // a new one to the browser history.
1510 _updateHash: function(location, fragment, replace) {
1511 if (replace) {
1512 var href = location.href.replace(/(javascript:|#).*$/, '');
1513 location.replace(href + '#' + fragment);
1514 } else {
1515 // Some browsers require that `hash` contains a leading #.
1516 location.hash = '#' + fragment;
1517 }
1518 }
1519
1520 });
1521
1522 // Create the default Backbone.history.
1523 Backbone.history = new History;
1524
1525 // Helpers
1526 // -------
1527
1528 // Helper function to correctly set up the prototype chain, for subclasses.
1529 // Similar to `goog.inherits`, but uses a hash of prototype properties and
1530 // class properties to be extended.
1531 var extend = function(protoProps, staticProps) {
1532 var parent = this;
1533 var child;
1534
1535 // The constructor function for the new subclass is either defined by you
1536 // (the "constructor" property in your `extend` definition), or defaulted
1537 // by us to simply call the parent's constructor.
1538 if (protoProps && _.has(protoProps, 'constructor')) {
1539 child = protoProps.constructor;
1540 } else {
1541 child = function(){ return parent.apply(this, arguments); };
1542 }
1543
1544 // Add static properties to the constructor function, if supplied.
1545 _.extend(child, parent, staticProps);
1546
1547 // Set the prototype chain to inherit from `parent`, without calling
1548 // `parent`'s constructor function.
1549 var Surrogate = function(){ this.constructor = child; };
1550 Surrogate.prototype = parent.prototype;
1551 child.prototype = new Surrogate;
1552
1553 // Add prototype properties (instance properties) to the subclass,
1554 // if supplied.
1555 if (protoProps) _.extend(child.prototype, protoProps);
1556
1557 // Set a convenience property in case the parent's prototype is needed
1558 // later.
1559 child.__super__ = parent.prototype;
1560
1561 return child;
1562 };
1563
1564 // Set up inheritance for the model, collection, router, view and history.
1565 Model.extend = Collection.extend = Router.extend = View.extend = History.extend = extend;
1566
1567 // Throw an error when a URL is needed, and none is supplied.
1568 var urlError = function() {
1569 throw new Error('A "url" property or function must be specified');
1570 };
1571
1572 // Wrap an optional error callback with a fallback error event.
1573 var wrapError = function(model, options) {
1574 var error = options.error;
1575 options.error = function(resp) {
1576 if (error) error(model, resp, options);
1577 model.trigger('error', model, resp, options);
1578 };
1579 };
1580
1581}).call(this);
0\ No newline at end of file1582\ No newline at end of file
11583
=== added directory 'web_unleashed/static/lib/jquery-addons'
=== added file 'web_unleashed/static/lib/jquery-addons/jquery.font_size.js'
--- web_unleashed/static/lib/jquery-addons/jquery.font_size.js 1970-01-01 00:00:00 +0000
+++ web_unleashed/static/lib/jquery-addons/jquery.font_size.js 2014-01-28 13:12:54 +0000
@@ -0,0 +1,11 @@
1(function(jQuery){
2
3 /*
4 * get the font size value
5 */
6 jQuery.fn.fontSize = function() {
7 return parseInt(this.css('fontSize').replace('px', ''));
8 };
9
10
11})($ || jQuery)
012
=== added file 'web_unleashed/static/lib/jquery-addons/jquery.form_reset.js'
--- web_unleashed/static/lib/jquery-addons/jquery.form_reset.js 1970-01-01 00:00:00 +0000
+++ web_unleashed/static/lib/jquery-addons/jquery.form_reset.js 2014-01-28 13:12:54 +0000
@@ -0,0 +1,13 @@
1(function(jQuery){
2
3 /*
4 * reset from values.
5 */
6 jQuery.fn.reset = function() {
7 this.find('input,textarea,select').each(function(index, el){
8 $(el).val('');
9 });
10 };
11
12
13})($ || jQuery)
014
=== added file 'web_unleashed/static/lib/jquery-addons/jquery.serialize_object.js'
--- web_unleashed/static/lib/jquery-addons/jquery.serialize_object.js 1970-01-01 00:00:00 +0000
+++ web_unleashed/static/lib/jquery-addons/jquery.serialize_object.js 2014-01-28 13:12:54 +0000
@@ -0,0 +1,17 @@
1(function(jQuery){
2
3 /*
4 * serialize a form into an Object.
5 */
6 $.fn.serializeObject = function(){
7 var data = this.serializeArray(),
8 obj = {}, i;
9
10 for(i=0 ; i < data.length ; i++){
11 obj[data[i]['name']] = $.isNumeric(data[i]['value']) ? parseInt(data[i]['value']) : data[i]['value'];
12 }
13
14 return obj;
15 };
16
17})($ || jQuery)
0\ No newline at end of file18\ No newline at end of file
119
=== added file 'web_unleashed/static/lib/jquery-addons/jquery.when_all.js'
--- web_unleashed/static/lib/jquery-addons/jquery.when_all.js 1970-01-01 00:00:00 +0000
+++ web_unleashed/static/lib/jquery-addons/jquery.when_all.js 2014-01-28 13:12:54 +0000
@@ -0,0 +1,56 @@
1(function(jQuery){
2
3 /*
4 * add a helper for $.Deferred, to keep waiting until all deferrer are executed before firing the global deferrer, keep info of each deferrer.
5 */
6 jQuery.whenAll = function( firstParam ) {
7 var args = $.makeArray(arguments),
8 i = 0,
9 length = args.length,
10 pValues = new Array( length ),
11 count = length,
12 pCount = length,
13 deferred = length <= 1 && firstParam && jQuery.isFunction( firstParam.promise ) ?
14 firstParam :
15 jQuery.Deferred(),
16 promise = deferred.promise(),
17 state = 'resolved';
18
19 function alwaysFunc( i ) {
20 return function( value ) {
21 args[ i ] = arguments.length > 1 ? $.makeArray(arguments) : value;
22 state = this.state() === "rejected" ? "rejected" : state;
23 if ( !( --count ) ) {
24 var method = (state === "rejected"? "reject": "resolve") + "With";
25 deferred[method]( deferred, args );
26 }
27 };
28 }
29
30 function progressFunc( i ) {
31 return function( value ) {
32 pValues[ i ] = arguments.length > 1 ? $.makeArray(arguments) : value;
33 deferred.notifyWith( promise, pValues );
34 };
35 }
36
37 if ( length > 1 ) {
38 for ( ; i < length; i++ ) {
39 if ( args[ i ] && args[ i ].promise && jQuery.isFunction( args[ i ].promise ) ) {
40 args[ i ].promise().always( alwaysFunc(i))
41 .progress(progressFunc(i));
42 } else {
43 --count;
44 }
45 }
46 if ( !count ) {
47 deferred.resolveWith( deferred, args );
48 }
49 } else if ( deferred !== firstParam ) {
50 deferred.resolveWith( deferred, length ? [ firstParam ] : [] );
51 }
52 return promise;
53 };
54
55
56})($ || jQuery)
057
=== added directory 'web_unleashed/static/lib/marionette'
=== added file 'web_unleashed/static/lib/marionette/marionette.js'
--- web_unleashed/static/lib/marionette/marionette.js 1970-01-01 00:00:00 +0000
+++ web_unleashed/static/lib/marionette/marionette.js 2014-01-28 13:12:54 +0000
@@ -0,0 +1,2363 @@
1// MarionetteJS (Backbone.Marionette)
2// ----------------------------------
3// v1.1.0
4//
5// Copyright (c)2013 Derick Bailey, Muted Solutions, LLC.
6// Distributed under MIT license
7//
8// http://marionettejs.com
9
10
11
12/*!
13 * Includes BabySitter
14 * https://github.com/marionettejs/backbone.babysitter/
15 *
16 * Includes Wreqr
17 * https://github.com/marionettejs/backbone.wreqr/
18 */
19
20// Backbone.BabySitter
21// -------------------
22// v0.0.6
23//
24// Copyright (c)2013 Derick Bailey, Muted Solutions, LLC.
25// Distributed under MIT license
26//
27// http://github.com/babysitterjs/backbone.babysitter
28
29// Backbone.ChildViewContainer
30// ---------------------------
31//
32// Provide a container to store, retrieve and
33// shut down child views.
34
35Backbone.ChildViewContainer = (function(Backbone, _){
36
37 // Container Constructor
38 // ---------------------
39
40 var Container = function(views){
41 this._views = {};
42 this._indexByModel = {};
43 this._indexByCustom = {};
44 this._updateLength();
45
46 _.each(views, this.add, this);
47 };
48
49 // Container Methods
50 // -----------------
51
52 _.extend(Container.prototype, {
53
54 // Add a view to this container. Stores the view
55 // by `cid` and makes it searchable by the model
56 // cid (and model itself). Optionally specify
57 // a custom key to store an retrieve the view.
58 add: function(view, customIndex){
59 var viewCid = view.cid;
60
61 // store the view
62 this._views[viewCid] = view;
63
64 // index it by model
65 if (view.model){
66 this._indexByModel[view.model.cid] = viewCid;
67 }
68
69 // index by custom
70 if (customIndex){
71 this._indexByCustom[customIndex] = viewCid;
72 }
73
74 this._updateLength();
75 },
76
77 // Find a view by the model that was attached to
78 // it. Uses the model's `cid` to find it.
79 findByModel: function(model){
80 return this.findByModelCid(model.cid);
81 },
82
83 // Find a view by the `cid` of the model that was attached to
84 // it. Uses the model's `cid` to find the view `cid` and
85 // retrieve the view using it.
86 findByModelCid: function(modelCid){
87 var viewCid = this._indexByModel[modelCid];
88 return this.findByCid(viewCid);
89 },
90
91 // Find a view by a custom indexer.
92 findByCustom: function(index){
93 var viewCid = this._indexByCustom[index];
94 return this.findByCid(viewCid);
95 },
96
97 // Find by index. This is not guaranteed to be a
98 // stable index.
99 findByIndex: function(index){
100 return _.values(this._views)[index];
101 },
102
103 // retrieve a view by it's `cid` directly
104 findByCid: function(cid){
105 return this._views[cid];
106 },
107
108 // Remove a view
109 remove: function(view){
110 var viewCid = view.cid;
111
112 // delete model index
113 if (view.model){
114 delete this._indexByModel[view.model.cid];
115 }
116
117 // delete custom index
118 _.any(this._indexByCustom, function(cid, key) {
119 if (cid === viewCid) {
120 delete this._indexByCustom[key];
121 return true;
122 }
123 }, this);
124
125 // remove the view from the container
126 delete this._views[viewCid];
127
128 // update the length
129 this._updateLength();
130 },
131
132 // Call a method on every view in the container,
133 // passing parameters to the call method one at a
134 // time, like `function.call`.
135 call: function(method){
136 this.apply(method, _.tail(arguments));
137 },
138
139 // Apply a method on every view in the container,
140 // passing parameters to the call method one at a
141 // time, like `function.apply`.
142 apply: function(method, args){
143 _.each(this._views, function(view){
144 if (_.isFunction(view[method])){
145 view[method].apply(view, args || []);
146 }
147 });
148 },
149
150 // Update the `.length` attribute on this container
151 _updateLength: function(){
152 this.length = _.size(this._views);
153 }
154 });
155
156 // Borrowing this code from Backbone.Collection:
157 // http://backbonejs.org/docs/backbone.html#section-106
158 //
159 // Mix in methods from Underscore, for iteration, and other
160 // collection related features.
161 var methods = ['forEach', 'each', 'map', 'find', 'detect', 'filter',
162 'select', 'reject', 'every', 'all', 'some', 'any', 'include',
163 'contains', 'invoke', 'toArray', 'first', 'initial', 'rest',
164 'last', 'without', 'isEmpty', 'pluck'];
165
166 _.each(methods, function(method) {
167 Container.prototype[method] = function() {
168 var views = _.values(this._views);
169 var args = [views].concat(_.toArray(arguments));
170 return _[method].apply(_, args);
171 };
172 });
173
174 // return the public API
175 return Container;
176})(Backbone, _);
177
178// Backbone.Wreqr (Backbone.Marionette)
179// ----------------------------------
180// v0.2.0
181//
182// Copyright (c)2013 Derick Bailey, Muted Solutions, LLC.
183// Distributed under MIT license
184//
185// http://github.com/marionettejs/backbone.wreqr
186
187
188Backbone.Wreqr = (function(Backbone, Marionette, _){
189 "use strict";
190 var Wreqr = {};
191
192 // Handlers
193// --------
194// A registry of functions to call, given a name
195
196Wreqr.Handlers = (function(Backbone, _){
197 "use strict";
198
199 // Constructor
200 // -----------
201
202 var Handlers = function(options){
203 this.options = options;
204 this._wreqrHandlers = {};
205
206 if (_.isFunction(this.initialize)){
207 this.initialize(options);
208 }
209 };
210
211 Handlers.extend = Backbone.Model.extend;
212
213 // Instance Members
214 // ----------------
215
216 _.extend(Handlers.prototype, Backbone.Events, {
217
218 // Add multiple handlers using an object literal configuration
219 setHandlers: function(handlers){
220 _.each(handlers, function(handler, name){
221 var context = null;
222
223 if (_.isObject(handler) && !_.isFunction(handler)){
224 context = handler.context;
225 handler = handler.callback;
226 }
227
228 this.setHandler(name, handler, context);
229 }, this);
230 },
231
232 // Add a handler for the given name, with an
233 // optional context to run the handler within
234 setHandler: function(name, handler, context){
235 var config = {
236 callback: handler,
237 context: context
238 };
239
240 this._wreqrHandlers[name] = config;
241
242 this.trigger("handler:add", name, handler, context);
243 },
244
245 // Determine whether or not a handler is registered
246 hasHandler: function(name){
247 return !! this._wreqrHandlers[name];
248 },
249
250 // Get the currently registered handler for
251 // the specified name. Throws an exception if
252 // no handler is found.
253 getHandler: function(name){
254 var config = this._wreqrHandlers[name];
255
256 if (!config){
257 throw new Error("Handler not found for '" + name + "'");
258 }
259
260 return function(){
261 var args = Array.prototype.slice.apply(arguments);
262 return config.callback.apply(config.context, args);
263 };
264 },
265
266 // Remove a handler for the specified name
267 removeHandler: function(name){
268 delete this._wreqrHandlers[name];
269 },
270
271 // Remove all handlers from this registry
272 removeAllHandlers: function(){
273 this._wreqrHandlers = {};
274 }
275 });
276
277 return Handlers;
278})(Backbone, _);
279
280 // Wreqr.CommandStorage
281// --------------------
282//
283// Store and retrieve commands for execution.
284Wreqr.CommandStorage = (function(){
285 "use strict";
286
287 // Constructor function
288 var CommandStorage = function(options){
289 this.options = options;
290 this._commands = {};
291
292 if (_.isFunction(this.initialize)){
293 this.initialize(options);
294 }
295 };
296
297 // Instance methods
298 _.extend(CommandStorage.prototype, Backbone.Events, {
299
300 // Get an object literal by command name, that contains
301 // the `commandName` and the `instances` of all commands
302 // represented as an array of arguments to process
303 getCommands: function(commandName){
304 var commands = this._commands[commandName];
305
306 // we don't have it, so add it
307 if (!commands){
308
309 // build the configuration
310 commands = {
311 command: commandName,
312 instances: []
313 };
314
315 // store it
316 this._commands[commandName] = commands;
317 }
318
319 return commands;
320 },
321
322 // Add a command by name, to the storage and store the
323 // args for the command
324 addCommand: function(commandName, args){
325 var command = this.getCommands(commandName);
326 command.instances.push(args);
327 },
328
329 // Clear all commands for the given `commandName`
330 clearCommands: function(commandName){
331 var command = this.getCommands(commandName);
332 command.instances = [];
333 }
334 });
335
336 return CommandStorage;
337})();
338
339 // Wreqr.Commands
340// --------------
341//
342// A simple command pattern implementation. Register a command
343// handler and execute it.
344Wreqr.Commands = (function(Wreqr){
345 "use strict";
346
347 return Wreqr.Handlers.extend({
348 // default storage type
349 storageType: Wreqr.CommandStorage,
350
351 constructor: function(options){
352 this.options = options || {};
353
354 this._initializeStorage(this.options);
355 this.on("handler:add", this._executeCommands, this);
356
357 var args = Array.prototype.slice.call(arguments);
358 Wreqr.Handlers.prototype.constructor.apply(this, args);
359 },
360
361 // Execute a named command with the supplied args
362 execute: function(name, args){
363 name = arguments[0];
364 args = Array.prototype.slice.call(arguments, 1);
365
366 if (this.hasHandler(name)){
367 this.getHandler(name).apply(this, args);
368 } else {
369 this.storage.addCommand(name, args);
370 }
371
372 },
373
374 // Internal method to handle bulk execution of stored commands
375 _executeCommands: function(name, handler, context){
376 var command = this.storage.getCommands(name);
377
378 // loop through and execute all the stored command instances
379 _.each(command.instances, function(args){
380 handler.apply(context, args);
381 });
382
383 this.storage.clearCommands(name);
384 },
385
386 // Internal method to initialize storage either from the type's
387 // `storageType` or the instance `options.storageType`.
388 _initializeStorage: function(options){
389 var storage;
390
391 var StorageType = options.storageType || this.storageType;
392 if (_.isFunction(StorageType)){
393 storage = new StorageType();
394 } else {
395 storage = StorageType;
396 }
397
398 this.storage = storage;
399 }
400 });
401
402})(Wreqr);
403
404 // Wreqr.RequestResponse
405// ---------------------
406//
407// A simple request/response implementation. Register a
408// request handler, and return a response from it
409Wreqr.RequestResponse = (function(Wreqr){
410 "use strict";
411
412 return Wreqr.Handlers.extend({
413 request: function(){
414 var name = arguments[0];
415 var args = Array.prototype.slice.call(arguments, 1);
416
417 return this.getHandler(name).apply(this, args);
418 }
419 });
420
421})(Wreqr);
422
423 // Event Aggregator
424// ----------------
425// A pub-sub object that can be used to decouple various parts
426// of an application through event-driven architecture.
427
428Wreqr.EventAggregator = (function(Backbone, _){
429 "use strict";
430 var EA = function(){};
431
432 // Copy the `extend` function used by Backbone's classes
433 EA.extend = Backbone.Model.extend;
434
435 // Copy the basic Backbone.Events on to the event aggregator
436 _.extend(EA.prototype, Backbone.Events);
437
438 return EA;
439})(Backbone, _);
440
441
442 return Wreqr;
443})(Backbone, Backbone.Marionette, _);
444
445var Marionette = (function(global, Backbone, _){
446 "use strict";
447
448 // Define and export the Marionette namespace
449 var Marionette = {};
450 Backbone.Marionette = Marionette;
451
452 // Get the DOM manipulator for later use
453 Marionette.$ = Backbone.$;
454
455// Helpers
456// -------
457
458// For slicing `arguments` in functions
459var protoSlice = Array.prototype.slice;
460function slice(args) {
461 return protoSlice.call(args);
462}
463
464function throwError(message, name) {
465 var error = new Error(message);
466 error.name = name || 'Error';
467 throw error;
468}
469
470// Marionette.extend
471// -----------------
472
473// Borrow the Backbone `extend` method so we can use it as needed
474Marionette.extend = Backbone.Model.extend;
475
476// Marionette.getOption
477// --------------------
478
479// Retrieve an object, function or other value from a target
480// object or its `options`, with `options` taking precedence.
481Marionette.getOption = function(target, optionName){
482 if (!target || !optionName){ return; }
483 var value;
484
485 if (target.options && (optionName in target.options) && (target.options[optionName] !== undefined)){
486 value = target.options[optionName];
487 } else {
488 value = target[optionName];
489 }
490
491 return value;
492};
493
494// Trigger an event and/or a corresponding method name. Examples:
495//
496// `this.triggerMethod("foo")` will trigger the "foo" event and
497// call the "onFoo" method.
498//
499// `this.triggerMethod("foo:bar") will trigger the "foo:bar" event and
500// call the "onFooBar" method.
501Marionette.triggerMethod = (function(){
502
503 // split the event name on the :
504 var splitter = /(^|:)(\w)/gi;
505
506 // take the event section ("section1:section2:section3")
507 // and turn it in to uppercase name
508 function getEventName(match, prefix, eventName) {
509 return eventName.toUpperCase();
510 }
511
512 // actual triggerMethod name
513 var triggerMethod = function(event) {
514 // get the method name from the event name
515 var methodName = 'on' + event.replace(splitter, getEventName);
516 var method = this[methodName];
517
518 // trigger the event, if a trigger method exists
519 if(_.isFunction(this.trigger)) {
520 this.trigger.apply(this, arguments);
521 }
522
523 // call the onMethodName if it exists
524 if (_.isFunction(method)) {
525 // pass all arguments, except the event name
526 return method.apply(this, _.tail(arguments));
527 }
528 };
529
530 return triggerMethod;
531})();
532
533// DOMRefresh
534// ----------
535//
536// Monitor a view's state, and after it has been rendered and shown
537// in the DOM, trigger a "dom:refresh" event every time it is
538// re-rendered.
539
540Marionette.MonitorDOMRefresh = (function(){
541 // track when the view has been shown in the DOM,
542 // using a Marionette.Region (or by other means of triggering "show")
543 function handleShow(view){
544 view._isShown = true;
545 triggerDOMRefresh(view);
546 }
547
548 // track when the view has been rendered
549 function handleRender(view){
550 view._isRendered = true;
551 triggerDOMRefresh(view);
552 }
553
554 // Trigger the "dom:refresh" event and corresponding "onDomRefresh" method
555 function triggerDOMRefresh(view){
556 if (view._isShown && view._isRendered){
557 if (_.isFunction(view.triggerMethod)){
558 view.triggerMethod("dom:refresh");
559 }
560 }
561 }
562
563 // Export public API
564 return function(view){
565 view.listenTo(view, "show", function(){
566 handleShow(view);
567 });
568
569 view.listenTo(view, "render", function(){
570 handleRender(view);
571 });
572 };
573})();
574
575
576// Marionette.bindEntityEvents & unbindEntityEvents
577// ---------------------------
578//
579// These methods are used to bind/unbind a backbone "entity" (collection/model)
580// to methods on a target object.
581//
582// The first parameter, `target`, must have a `listenTo` method from the
583// EventBinder object.
584//
585// The second parameter is the entity (Backbone.Model or Backbone.Collection)
586// to bind the events from.
587//
588// The third parameter is a hash of { "event:name": "eventHandler" }
589// configuration. Multiple handlers can be separated by a space. A
590// function can be supplied instead of a string handler name.
591
592(function(Marionette){
593 "use strict";
594
595 // Bind the event to handlers specified as a string of
596 // handler names on the target object
597 function bindFromStrings(target, entity, evt, methods){
598 var methodNames = methods.split(/\s+/);
599
600 _.each(methodNames,function(methodName) {
601
602 var method = target[methodName];
603 if(!method) {
604 throwError("Method '"+ methodName +"' was configured as an event handler, but does not exist.");
605 }
606
607 target.listenTo(entity, evt, method, target);
608 });
609 }
610
611 // Bind the event to a supplied callback function
612 function bindToFunction(target, entity, evt, method){
613 target.listenTo(entity, evt, method, target);
614 }
615
616 // Bind the event to handlers specified as a string of
617 // handler names on the target object
618 function unbindFromStrings(target, entity, evt, methods){
619 var methodNames = methods.split(/\s+/);
620
621 _.each(methodNames,function(methodName) {
622 var method = target[methodName];
623 target.stopListening(entity, evt, method, target);
624 });
625 }
626
627 // Bind the event to a supplied callback function
628 function unbindToFunction(target, entity, evt, method){
629 target.stopListening(entity, evt, method, target);
630 }
631
632
633 // generic looping function
634 function iterateEvents(target, entity, bindings, functionCallback, stringCallback){
635 if (!entity || !bindings) { return; }
636
637 // allow the bindings to be a function
638 if (_.isFunction(bindings)){
639 bindings = bindings.call(target);
640 }
641
642 // iterate the bindings and bind them
643 _.each(bindings, function(methods, evt){
644
645 // allow for a function as the handler,
646 // or a list of event names as a string
647 if (_.isFunction(methods)){
648 functionCallback(target, entity, evt, methods);
649 } else {
650 stringCallback(target, entity, evt, methods);
651 }
652
653 });
654 }
655
656 // Export Public API
657 Marionette.bindEntityEvents = function(target, entity, bindings){
658 iterateEvents(target, entity, bindings, bindToFunction, bindFromStrings);
659 };
660
661 Marionette.unbindEntityEvents = function(target, entity, bindings){
662 iterateEvents(target, entity, bindings, unbindToFunction, unbindFromStrings);
663 };
664
665})(Marionette);
666
667
668// Callbacks
669// ---------
670
671// A simple way of managing a collection of callbacks
672// and executing them at a later point in time, using jQuery's
673// `Deferred` object.
674Marionette.Callbacks = function(){
675 this._deferred = Marionette.$.Deferred();
676 this._callbacks = [];
677};
678
679_.extend(Marionette.Callbacks.prototype, {
680
681 // Add a callback to be executed. Callbacks added here are
682 // guaranteed to execute, even if they are added after the
683 // `run` method is called.
684 add: function(callback, contextOverride){
685 this._callbacks.push({cb: callback, ctx: contextOverride});
686
687 this._deferred.done(function(context, options){
688 if (contextOverride){ context = contextOverride; }
689 callback.call(context, options);
690 });
691 },
692
693 // Run all registered callbacks with the context specified.
694 // Additional callbacks can be added after this has been run
695 // and they will still be executed.
696 run: function(options, context){
697 this._deferred.resolve(context, options);
698 },
699
700 // Resets the list of callbacks to be run, allowing the same list
701 // to be run multiple times - whenever the `run` method is called.
702 reset: function(){
703 var callbacks = this._callbacks;
704 this._deferred = Marionette.$.Deferred();
705 this._callbacks = [];
706
707 _.each(callbacks, function(cb){
708 this.add(cb.cb, cb.ctx);
709 }, this);
710 }
711});
712
713
714// Marionette Controller
715// ---------------------
716//
717// A multi-purpose object to use as a controller for
718// modules and routers, and as a mediator for workflow
719// and coordination of other objects, views, and more.
720Marionette.Controller = function(options){
721 this.triggerMethod = Marionette.triggerMethod;
722 this.options = options || {};
723
724 if (_.isFunction(this.initialize)){
725 this.initialize(this.options);
726 }
727};
728
729Marionette.Controller.extend = Marionette.extend;
730
731// Controller Methods
732// --------------
733
734// Ensure it can trigger events with Backbone.Events
735_.extend(Marionette.Controller.prototype, Backbone.Events, {
736 close: function(){
737 this.stopListening();
738 this.triggerMethod("close");
739 this.unbind();
740 }
741});
742
743// Region
744// ------
745//
746// Manage the visual regions of your composite application. See
747// http://lostechies.com/derickbailey/2011/12/12/composite-js-apps-regions-and-region-managers/
748
749Marionette.Region = function(options){
750 this.options = options || {};
751
752 this.el = Marionette.getOption(this, "el");
753
754 if (!this.el){
755 var err = new Error("An 'el' must be specified for a region.");
756 err.name = "NoElError";
757 throw err;
758 }
759
760 if (this.initialize){
761 var args = Array.prototype.slice.apply(arguments);
762 this.initialize.apply(this, args);
763 }
764};
765
766
767// Region Type methods
768// -------------------
769
770_.extend(Marionette.Region, {
771
772 // Build an instance of a region by passing in a configuration object
773 // and a default region type to use if none is specified in the config.
774 //
775 // The config object should either be a string as a jQuery DOM selector,
776 // a Region type directly, or an object literal that specifies both
777 // a selector and regionType:
778 //
779 // ```js
780 // {
781 // selector: "#foo",
782 // regionType: MyCustomRegion
783 // }
784 // ```
785 //
786 buildRegion: function(regionConfig, defaultRegionType){
787
788 var regionIsString = (typeof regionConfig === "string");
789 var regionSelectorIsString = (typeof regionConfig.selector === "string");
790 var regionTypeIsUndefined = (typeof regionConfig.regionType === "undefined");
791 var regionIsType = (typeof regionConfig === "function");
792
793 if (!regionIsType && !regionIsString && !regionSelectorIsString) {
794 throw new Error("Region must be specified as a Region type, a selector string or an object with selector property");
795 }
796
797 var selector, RegionType;
798
799 // get the selector for the region
800
801 if (regionIsString) {
802 selector = regionConfig;
803 }
804
805 if (regionConfig.selector) {
806 selector = regionConfig.selector;
807 }
808
809 // get the type for the region
810
811 if (regionIsType){
812 RegionType = regionConfig;
813 }
814
815 if (!regionIsType && regionTypeIsUndefined) {
816 RegionType = defaultRegionType;
817 }
818
819 if (regionConfig.regionType) {
820 RegionType = regionConfig.regionType;
821 }
822
823 // build the region instance
824 var region = new RegionType({
825 el: selector
826 });
827
828 // override the `getEl` function if we have a parentEl
829 // this must be overridden to ensure the selector is found
830 // on the first use of the region. if we try to assign the
831 // region's `el` to `parentEl.find(selector)` in the object
832 // literal to build the region, the element will not be
833 // guaranteed to be in the DOM already, and will cause problems
834 if (regionConfig.parentEl){
835
836 region.getEl = function(selector) {
837 var parentEl = regionConfig.parentEl;
838 if (_.isFunction(parentEl)){
839 parentEl = parentEl();
840 }
841 return parentEl.find(selector);
842 };
843 }
844
845 return region;
846 }
847
848});
849
850// Region Instance Methods
851// -----------------------
852
853_.extend(Marionette.Region.prototype, Backbone.Events, {
854
855 // Displays a backbone view instance inside of the region.
856 // Handles calling the `render` method for you. Reads content
857 // directly from the `el` attribute. Also calls an optional
858 // `onShow` and `close` method on your view, just after showing
859 // or just before closing the view, respectively.
860 show: function(view){
861
862 this.ensureEl();
863
864 var isViewClosed = view.isClosed || _.isUndefined(view.$el);
865
866 var isDifferentView = view !== this.currentView;
867
868 if (isDifferentView) {
869 this.close();
870 }
871
872 view.render();
873
874 if (isDifferentView || isViewClosed) {
875 this.open(view);
876 }
877
878 this.currentView = view;
879
880 Marionette.triggerMethod.call(this, "show", view);
881 Marionette.triggerMethod.call(view, "show");
882 },
883
884 ensureEl: function(){
885 if (!this.$el || this.$el.length === 0){
886 this.$el = this.getEl(this.el);
887 }
888 },
889
890 // Override this method to change how the region finds the
891 // DOM element that it manages. Return a jQuery selector object.
892 getEl: function(selector){
893 return Marionette.$(selector);
894 },
895
896 // Override this method to change how the new view is
897 // appended to the `$el` that the region is managing
898 open: function(view){
899 this.$el.empty().append(view.el);
900 },
901
902 // Close the current view, if there is one. If there is no
903 // current view, it does nothing and returns immediately.
904 close: function(){
905 var view = this.currentView;
906 if (!view || view.isClosed){ return; }
907
908 // call 'close' or 'remove', depending on which is found
909 if (view.close) { view.close(); }
910 else if (view.remove) { view.remove(); }
911
912 Marionette.triggerMethod.call(this, "close");
913
914 delete this.currentView;
915 },
916
917 // Attach an existing view to the region. This
918 // will not call `render` or `onShow` for the new view,
919 // and will not replace the current HTML for the `el`
920 // of the region.
921 attachView: function(view){
922 this.currentView = view;
923 },
924
925 // Reset the region by closing any existing view and
926 // clearing out the cached `$el`. The next time a view
927 // is shown via this region, the region will re-query the
928 // DOM for the region's `el`.
929 reset: function(){
930 this.close();
931 delete this.$el;
932 }
933});
934
935// Copy the `extend` function used by Backbone's classes
936Marionette.Region.extend = Marionette.extend;
937
938// Marionette.RegionManager
939// ------------------------
940//
941// Manage one or more related `Marionette.Region` objects.
942Marionette.RegionManager = (function(Marionette){
943
944 var RegionManager = Marionette.Controller.extend({
945 constructor: function(options){
946 this._regions = {};
947 Marionette.Controller.prototype.constructor.call(this, options);
948 },
949
950 // Add multiple regions using an object literal, where
951 // each key becomes the region name, and each value is
952 // the region definition.
953 addRegions: function(regionDefinitions, defaults){
954 var regions = {};
955
956 _.each(regionDefinitions, function(definition, name){
957 if (typeof definition === "string"){
958 definition = { selector: definition };
959 }
960
961 if (definition.selector){
962 definition = _.defaults({}, definition, defaults);
963 }
964
965 var region = this.addRegion(name, definition);
966 regions[name] = region;
967 }, this);
968
969 return regions;
970 },
971
972 // Add an individual region to the region manager,
973 // and return the region instance
974 addRegion: function(name, definition){
975 var region;
976
977 var isObject = _.isObject(definition);
978 var isString = _.isString(definition);
979 var hasSelector = !!definition.selector;
980
981 if (isString || (isObject && hasSelector)){
982 region = Marionette.Region.buildRegion(definition, Marionette.Region);
983 } else if (_.isFunction(definition)){
984 region = Marionette.Region.buildRegion(definition, Marionette.Region);
985 } else {
986 region = definition;
987 }
988
989 this._store(name, region);
990 this.triggerMethod("region:add", name, region);
991 return region;
992 },
993
994 // Get a region by name
995 get: function(name){
996 return this._regions[name];
997 },
998
999 // Remove a region by name
1000 removeRegion: function(name){
1001 var region = this._regions[name];
1002 this._remove(name, region);
1003 },
1004
1005 // Close all regions in the region manager, and
1006 // remove them
1007 removeRegions: function(){
1008 _.each(this._regions, function(region, name){
1009 this._remove(name, region);
1010 }, this);
1011 },
1012
1013 // Close all regions in the region manager, but
1014 // leave them attached
1015 closeRegions: function(){
1016 _.each(this._regions, function(region, name){
1017 region.close();
1018 }, this);
1019 },
1020
1021 // Close all regions and shut down the region
1022 // manager entirely
1023 close: function(){
1024 this.removeRegions();
1025 var args = Array.prototype.slice.call(arguments);
1026 Marionette.Controller.prototype.close.apply(this, args);
1027 },
1028
1029 // internal method to store regions
1030 _store: function(name, region){
1031 this._regions[name] = region;
1032 this._setLength();
1033 },
1034
1035 // internal method to remove a region
1036 _remove: function(name, region){
1037 region.close();
1038 delete this._regions[name];
1039 this._setLength();
1040 this.triggerMethod("region:remove", name, region);
1041 },
1042
1043 // set the number of regions current held
1044 _setLength: function(){
1045 this.length = _.size(this._regions);
1046 }
1047
1048 });
1049
1050 // Borrowing this code from Backbone.Collection:
1051 // http://backbonejs.org/docs/backbone.html#section-106
1052 //
1053 // Mix in methods from Underscore, for iteration, and other
1054 // collection related features.
1055 var methods = ['forEach', 'each', 'map', 'find', 'detect', 'filter',
1056 'select', 'reject', 'every', 'all', 'some', 'any', 'include',
1057 'contains', 'invoke', 'toArray', 'first', 'initial', 'rest',
1058 'last', 'without', 'isEmpty', 'pluck'];
1059
1060 _.each(methods, function(method) {
1061 RegionManager.prototype[method] = function() {
1062 var regions = _.values(this._regions);
1063 var args = [regions].concat(_.toArray(arguments));
1064 return _[method].apply(_, args);
1065 };
1066 });
1067
1068 return RegionManager;
1069})(Marionette);
1070
1071
1072// Template Cache
1073// --------------
1074
1075// Manage templates stored in `<script>` blocks,
1076// caching them for faster access.
1077Marionette.TemplateCache = function(templateId){
1078 this.templateId = templateId;
1079};
1080
1081// TemplateCache object-level methods. Manage the template
1082// caches from these method calls instead of creating
1083// your own TemplateCache instances
1084_.extend(Marionette.TemplateCache, {
1085 templateCaches: {},
1086
1087 // Get the specified template by id. Either
1088 // retrieves the cached version, or loads it
1089 // from the DOM.
1090 get: function(templateId){
1091 var cachedTemplate = this.templateCaches[templateId];
1092
1093 if (!cachedTemplate){
1094 cachedTemplate = new Marionette.TemplateCache(templateId);
1095 this.templateCaches[templateId] = cachedTemplate;
1096 }
1097
1098 return cachedTemplate.load();
1099 },
1100
1101 // Clear templates from the cache. If no arguments
1102 // are specified, clears all templates:
1103 // `clear()`
1104 //
1105 // If arguments are specified, clears each of the
1106 // specified templates from the cache:
1107 // `clear("#t1", "#t2", "...")`
1108 clear: function(){
1109 var i;
1110 var args = slice(arguments);
1111 var length = args.length;
1112
1113 if (length > 0){
1114 for(i=0; i<length; i++){
1115 delete this.templateCaches[args[i]];
1116 }
1117 } else {
1118 this.templateCaches = {};
1119 }
1120 }
1121});
1122
1123// TemplateCache instance methods, allowing each
1124// template cache object to manage its own state
1125// and know whether or not it has been loaded
1126_.extend(Marionette.TemplateCache.prototype, {
1127
1128 // Internal method to load the template
1129 load: function(){
1130 // Guard clause to prevent loading this template more than once
1131 if (this.compiledTemplate){
1132 return this.compiledTemplate;
1133 }
1134
1135 // Load the template and compile it
1136 var template = this.loadTemplate(this.templateId);
1137 this.compiledTemplate = this.compileTemplate(template);
1138
1139 return this.compiledTemplate;
1140 },
1141
1142 // Load a template from the DOM, by default. Override
1143 // this method to provide your own template retrieval
1144 // For asynchronous loading with AMD/RequireJS, consider
1145 // using a template-loader plugin as described here:
1146 // https://github.com/marionettejs/backbone.marionette/wiki/Using-marionette-with-requirejs
1147 loadTemplate: function(templateId){
1148 var template = Marionette.$(templateId).html();
1149
1150 if (!template || template.length === 0){
1151 throwError("Could not find template: '" + templateId + "'", "NoTemplateError");
1152 }
1153
1154 return template;
1155 },
1156
1157 // Pre-compile the template before caching it. Override
1158 // this method if you do not need to pre-compile a template
1159 // (JST / RequireJS for example) or if you want to change
1160 // the template engine used (Handebars, etc).
1161 compileTemplate: function(rawTemplate){
1162 return _.template(rawTemplate);
1163 }
1164});
1165
1166
1167// Renderer
1168// --------
1169
1170// Render a template with data by passing in the template
1171// selector and the data to render.
1172Marionette.Renderer = {
1173
1174 // Render a template with data. The `template` parameter is
1175 // passed to the `TemplateCache` object to retrieve the
1176 // template function. Override this method to provide your own
1177 // custom rendering and template handling for all of Marionette.
1178 render: function(template, data){
1179
1180 if (!template) {
1181 var error = new Error("Cannot render the template since it's false, null or undefined.");
1182 error.name = "TemplateNotFoundError";
1183 throw error;
1184 }
1185
1186 var templateFunc;
1187 if (typeof template === "function"){
1188 templateFunc = template;
1189 } else {
1190 templateFunc = Marionette.TemplateCache.get(template);
1191 }
1192
1193 return templateFunc(data);
1194 }
1195};
1196
1197
1198
1199// Marionette.View
1200// ---------------
1201
1202// The core view type that other Marionette views extend from.
1203Marionette.View = Backbone.View.extend({
1204
1205 constructor: function(){
1206 _.bindAll(this, "render");
1207
1208 var args = Array.prototype.slice.apply(arguments);
1209 Backbone.View.prototype.constructor.apply(this, args);
1210
1211 Marionette.MonitorDOMRefresh(this);
1212 this.listenTo(this, "show", this.onShowCalled, this);
1213 },
1214
1215 // import the "triggerMethod" to trigger events with corresponding
1216 // methods if the method exists
1217 triggerMethod: Marionette.triggerMethod,
1218
1219 // Get the template for this view
1220 // instance. You can set a `template` attribute in the view
1221 // definition or pass a `template: "whatever"` parameter in
1222 // to the constructor options.
1223 getTemplate: function(){
1224 return Marionette.getOption(this, "template");
1225 },
1226
1227 // Mix in template helper methods. Looks for a
1228 // `templateHelpers` attribute, which can either be an
1229 // object literal, or a function that returns an object
1230 // literal. All methods and attributes from this object
1231 // are copies to the object passed in.
1232 mixinTemplateHelpers: function(target){
1233 target = target || {};
1234 var templateHelpers = Marionette.getOption(this, "templateHelpers");
1235 if (_.isFunction(templateHelpers)){
1236 templateHelpers = templateHelpers.call(this);
1237 }
1238 return _.extend(target, templateHelpers);
1239 },
1240
1241 // Configure `triggers` to forward DOM events to view
1242 // events. `triggers: {"click .foo": "do:foo"}`
1243 configureTriggers: function(){
1244 if (!this.triggers) { return; }
1245
1246 var triggerEvents = {};
1247
1248 // Allow `triggers` to be configured as a function
1249 var triggers = _.result(this, "triggers");
1250
1251 // Configure the triggers, prevent default
1252 // action and stop propagation of DOM events
1253 _.each(triggers, function(value, key){
1254
1255 // build the event handler function for the DOM event
1256 triggerEvents[key] = function(e){
1257
1258 // stop the event in its tracks
1259 if (e && e.preventDefault){ e.preventDefault(); }
1260 if (e && e.stopPropagation){ e.stopPropagation(); }
1261
1262 // build the args for the event
1263 var args = {
1264 view: this,
1265 model: this.model,
1266 collection: this.collection
1267 };
1268
1269 // trigger the event
1270 this.triggerMethod(value, args);
1271 };
1272
1273 }, this);
1274
1275 return triggerEvents;
1276 },
1277
1278 // Overriding Backbone.View's delegateEvents to handle
1279 // the `triggers`, `modelEvents`, and `collectionEvents` configuration
1280 delegateEvents: function(events){
1281 this._delegateDOMEvents(events);
1282 Marionette.bindEntityEvents(this, this.model, Marionette.getOption(this, "modelEvents"));
1283 Marionette.bindEntityEvents(this, this.collection, Marionette.getOption(this, "collectionEvents"));
1284 },
1285
1286 // internal method to delegate DOM events and triggers
1287 _delegateDOMEvents: function(events){
1288 events = events || this.events;
1289 if (_.isFunction(events)){ events = events.call(this); }
1290
1291 var combinedEvents = {};
1292 var triggers = this.configureTriggers();
1293 _.extend(combinedEvents, events, triggers);
1294
1295 Backbone.View.prototype.delegateEvents.call(this, combinedEvents);
1296 },
1297
1298 // Overriding Backbone.View's undelegateEvents to handle unbinding
1299 // the `triggers`, `modelEvents`, and `collectionEvents` config
1300 undelegateEvents: function(){
1301 var args = Array.prototype.slice.call(arguments);
1302 Backbone.View.prototype.undelegateEvents.apply(this, args);
1303
1304 Marionette.unbindEntityEvents(this, this.model, Marionette.getOption(this, "modelEvents"));
1305 Marionette.unbindEntityEvents(this, this.collection, Marionette.getOption(this, "collectionEvents"));
1306 },
1307
1308 // Internal method, handles the `show` event.
1309 onShowCalled: function(){},
1310
1311 // Default `close` implementation, for removing a view from the
1312 // DOM and unbinding it. Regions will call this method
1313 // for you. You can specify an `onClose` method in your view to
1314 // add custom code that is called after the view is closed.
1315 close: function(){
1316 if (this.isClosed) { return; }
1317
1318 // allow the close to be stopped by returning `false`
1319 // from the `onBeforeClose` method
1320 var shouldClose = this.triggerMethod("before:close");
1321 if (shouldClose === false){
1322 return;
1323 }
1324
1325 // mark as closed before doing the actual close, to
1326 // prevent infinite loops within "close" event handlers
1327 // that are trying to close other views
1328 this.isClosed = true;
1329 this.triggerMethod("close");
1330
1331 // unbind UI elements
1332 this.unbindUIElements();
1333
1334 // remove the view from the DOM
1335 this.remove();
1336 },
1337
1338 // This method binds the elements specified in the "ui" hash inside the view's code with
1339 // the associated jQuery selectors.
1340 bindUIElements: function(){
1341 if (!this.ui) { return; }
1342
1343 // store the ui hash in _uiBindings so they can be reset later
1344 // and so re-rendering the view will be able to find the bindings
1345 if (!this._uiBindings){
1346 this._uiBindings = this.ui;
1347 }
1348
1349 // get the bindings result, as a function or otherwise
1350 var bindings = _.result(this, "_uiBindings");
1351
1352 // empty the ui so we don't have anything to start with
1353 this.ui = {};
1354
1355 // bind each of the selectors
1356 _.each(_.keys(bindings), function(key) {
1357 var selector = bindings[key];
1358 this.ui[key] = this.$(selector);
1359 }, this);
1360 },
1361
1362 // This method unbinds the elements specified in the "ui" hash
1363 unbindUIElements: function(){
1364 if (!this.ui || !this._uiBindings){ return; }
1365
1366 // delete all of the existing ui bindings
1367 _.each(this.ui, function($el, name){
1368 delete this.ui[name];
1369 }, this);
1370
1371 // reset the ui element to the original bindings configuration
1372 this.ui = this._uiBindings;
1373 delete this._uiBindings;
1374 }
1375});
1376
1377// Item View
1378// ---------
1379
1380// A single item view implementation that contains code for rendering
1381// with underscore.js templates, serializing the view's model or collection,
1382// and calling several methods on extended views, such as `onRender`.
1383Marionette.ItemView = Marionette.View.extend({
1384
1385 // Setting up the inheritance chain which allows changes to
1386 // Marionette.View.prototype.constructor which allows overriding
1387 constructor: function(){
1388 Marionette.View.prototype.constructor.apply(this, slice(arguments));
1389 },
1390
1391 // Serialize the model or collection for the view. If a model is
1392 // found, `.toJSON()` is called. If a collection is found, `.toJSON()`
1393 // is also called, but is used to populate an `items` array in the
1394 // resulting data. If both are found, defaults to the model.
1395 // You can override the `serializeData` method in your own view
1396 // definition, to provide custom serialization for your view's data.
1397 serializeData: function(){
1398 var data = {};
1399
1400 if (this.model) {
1401 data = this.model.toJSON();
1402 }
1403 else if (this.collection) {
1404 data = { items: this.collection.toJSON() };
1405 }
1406
1407 return data;
1408 },
1409
1410 // Render the view, defaulting to underscore.js templates.
1411 // You can override this in your view definition to provide
1412 // a very specific rendering for your view. In general, though,
1413 // you should override the `Marionette.Renderer` object to
1414 // change how Marionette renders views.
1415 render: function(){
1416 this.isClosed = false;
1417
1418 this.triggerMethod("before:render", this);
1419 this.triggerMethod("item:before:render", this);
1420
1421 var data = this.serializeData();
1422 data = this.mixinTemplateHelpers(data);
1423
1424 var template = this.getTemplate();
1425 var html = Marionette.Renderer.render(template, data);
1426
1427 this.$el.html(html);
1428 this.bindUIElements();
1429
1430 this.triggerMethod("render", this);
1431 this.triggerMethod("item:rendered", this);
1432
1433 return this;
1434 },
1435
1436 // Override the default close event to add a few
1437 // more events that are triggered.
1438 close: function(){
1439 if (this.isClosed){ return; }
1440
1441 this.triggerMethod('item:before:close');
1442
1443 Marionette.View.prototype.close.apply(this, slice(arguments));
1444
1445 this.triggerMethod('item:closed');
1446 }
1447});
1448
1449// Collection View
1450// ---------------
1451
1452// A view that iterates over a Backbone.Collection
1453// and renders an individual ItemView for each model.
1454Marionette.CollectionView = Marionette.View.extend({
1455 // used as the prefix for item view events
1456 // that are forwarded through the collectionview
1457 itemViewEventPrefix: "itemview",
1458
1459 // constructor
1460 constructor: function(options){
1461 this._initChildViewStorage();
1462
1463 Marionette.View.prototype.constructor.apply(this, slice(arguments));
1464
1465 this._initialEvents();
1466 },
1467
1468 // Configured the initial events that the collection view
1469 // binds to. Override this method to prevent the initial
1470 // events, or to add your own initial events.
1471 _initialEvents: function(){
1472 if (this.collection){
1473 this.listenTo(this.collection, "add", this.addChildView, this);
1474 this.listenTo(this.collection, "remove", this.removeItemView, this);
1475 this.listenTo(this.collection, "reset", this.render, this);
1476 }
1477 },
1478
1479 // Handle a child item added to the collection
1480 addChildView: function(item, collection, options){
1481 this.closeEmptyView();
1482 var ItemView = this.getItemView(item);
1483 var index = this.collection.indexOf(item);
1484 this.addItemView(item, ItemView, index);
1485 },
1486
1487 // Override from `Marionette.View` to guarantee the `onShow` method
1488 // of child views is called.
1489 onShowCalled: function(){
1490 this.children.each(function(child){
1491 Marionette.triggerMethod.call(child, "show");
1492 });
1493 },
1494
1495 // Internal method to trigger the before render callbacks
1496 // and events
1497 triggerBeforeRender: function(){
1498 this.triggerMethod("before:render", this);
1499 this.triggerMethod("collection:before:render", this);
1500 },
1501
1502 // Internal method to trigger the rendered callbacks and
1503 // events
1504 triggerRendered: function(){
1505 this.triggerMethod("render", this);
1506 this.triggerMethod("collection:rendered", this);
1507 },
1508
1509 // Render the collection of items. Override this method to
1510 // provide your own implementation of a render function for
1511 // the collection view.
1512 render: function(){
1513 this.isClosed = false;
1514 this.triggerBeforeRender();
1515 this._renderChildren();
1516 this.triggerRendered();
1517 return this;
1518 },
1519
1520 // Internal method. Separated so that CompositeView can have
1521 // more control over events being triggered, around the rendering
1522 // process
1523 _renderChildren: function(){
1524 this.closeEmptyView();
1525 this.closeChildren();
1526
1527 if (this.collection && this.collection.length > 0) {
1528 this.showCollection();
1529 } else {
1530 this.showEmptyView();
1531 }
1532 },
1533
1534 // Internal method to loop through each item in the
1535 // collection view and show it
1536 showCollection: function(){
1537 var ItemView;
1538 this.collection.each(function(item, index){
1539 ItemView = this.getItemView(item);
1540 this.addItemView(item, ItemView, index);
1541 }, this);
1542 },
1543
1544 // Internal method to show an empty view in place of
1545 // a collection of item views, when the collection is
1546 // empty
1547 showEmptyView: function(){
1548 var EmptyView = Marionette.getOption(this, "emptyView");
1549
1550 if (EmptyView && !this._showingEmptyView){
1551 this._showingEmptyView = true;
1552 var model = new Backbone.Model();
1553 this.addItemView(model, EmptyView, 0);
1554 }
1555 },
1556
1557 // Internal method to close an existing emptyView instance
1558 // if one exists. Called when a collection view has been
1559 // rendered empty, and then an item is added to the collection.
1560 closeEmptyView: function(){
1561 if (this._showingEmptyView){
1562 this.closeChildren();
1563 delete this._showingEmptyView;
1564 }
1565 },
1566
1567 // Retrieve the itemView type, either from `this.options.itemView`
1568 // or from the `itemView` in the object definition. The "options"
1569 // takes precedence.
1570 getItemView: function(item){
1571 var itemView = Marionette.getOption(this, "itemView");
1572
1573 if (!itemView){
1574 throwError("An `itemView` must be specified", "NoItemViewError");
1575 }
1576
1577 return itemView;
1578 },
1579
1580 // Render the child item's view and add it to the
1581 // HTML for the collection view.
1582 addItemView: function(item, ItemView, index){
1583 // get the itemViewOptions if any were specified
1584 var itemViewOptions = Marionette.getOption(this, "itemViewOptions");
1585 if (_.isFunction(itemViewOptions)){
1586 itemViewOptions = itemViewOptions.call(this, item, index);
1587 }
1588
1589 // build the view
1590 var view = this.buildItemView(item, ItemView, itemViewOptions);
1591
1592 // set up the child view event forwarding
1593 this.addChildViewEventForwarding(view);
1594
1595 // this view is about to be added
1596 this.triggerMethod("before:item:added", view);
1597
1598 // Store the child view itself so we can properly
1599 // remove and/or close it later
1600 this.children.add(view);
1601
1602 // Render it and show it
1603 this.renderItemView(view, index);
1604
1605 // call the "show" method if the collection view
1606 // has already been shown
1607 if (this._isShown){
1608 Marionette.triggerMethod.call(view, "show");
1609 }
1610
1611 // this view was added
1612 this.triggerMethod("after:item:added", view);
1613 },
1614
1615 // Set up the child view event forwarding. Uses an "itemview:"
1616 // prefix in front of all forwarded events.
1617 addChildViewEventForwarding: function(view){
1618 var prefix = Marionette.getOption(this, "itemViewEventPrefix");
1619
1620 // Forward all child item view events through the parent,
1621 // prepending "itemview:" to the event name
1622 this.listenTo(view, "all", function(){
1623 var args = slice(arguments);
1624 args[0] = prefix + ":" + args[0];
1625 args.splice(1, 0, view);
1626
1627 Marionette.triggerMethod.apply(this, args);
1628 }, this);
1629 },
1630
1631 // render the item view
1632 renderItemView: function(view, index) {
1633 view.render();
1634 this.appendHtml(this, view, index);
1635 },
1636
1637 // Build an `itemView` for every model in the collection.
1638 buildItemView: function(item, ItemViewType, itemViewOptions){
1639 var options = _.extend({model: item}, itemViewOptions);
1640 return new ItemViewType(options);
1641 },
1642
1643 // get the child view by item it holds, and remove it
1644 removeItemView: function(item){
1645 var view = this.children.findByModel(item);
1646 this.removeChildView(view);
1647 this.checkEmpty();
1648 },
1649
1650 // Remove the child view and close it
1651 removeChildView: function(view){
1652
1653 // shut down the child view properly,
1654 // including events that the collection has from it
1655 if (view){
1656 this.stopListening(view);
1657
1658 // call 'close' or 'remove', depending on which is found
1659 if (view.close) { view.close(); }
1660 else if (view.remove) { view.remove(); }
1661
1662 this.children.remove(view);
1663 }
1664
1665 this.triggerMethod("item:removed", view);
1666 },
1667
1668 // helper to show the empty view if the collection is empty
1669 checkEmpty: function() {
1670 // check if we're empty now, and if we are, show the
1671 // empty view
1672 if (!this.collection || this.collection.length === 0){
1673 this.showEmptyView();
1674 }
1675 },
1676
1677 // Append the HTML to the collection's `el`.
1678 // Override this method to do something other
1679 // then `.append`.
1680 appendHtml: function(collectionView, itemView, index){
1681 collectionView.$el.append(itemView.el);
1682 },
1683
1684 // Internal method to set up the `children` object for
1685 // storing all of the child views
1686 _initChildViewStorage: function(){
1687 this.children = new Backbone.ChildViewContainer();
1688 },
1689
1690 // Handle cleanup and other closing needs for
1691 // the collection of views.
1692 close: function(){
1693 if (this.isClosed){ return; }
1694
1695 this.triggerMethod("collection:before:close");
1696 this.closeChildren();
1697 this.triggerMethod("collection:closed");
1698
1699 Marionette.View.prototype.close.apply(this, slice(arguments));
1700 },
1701
1702 // Close the child views that this collection view
1703 // is holding on to, if any
1704 closeChildren: function(){
1705 this.children.each(function(child){
1706 this.removeChildView(child);
1707 }, this);
1708 this.checkEmpty();
1709 }
1710});
1711
1712
1713// Composite View
1714// --------------
1715
1716// Used for rendering a branch-leaf, hierarchical structure.
1717// Extends directly from CollectionView and also renders an
1718// an item view as `modelView`, for the top leaf
1719Marionette.CompositeView = Marionette.CollectionView.extend({
1720
1721 // Setting up the inheritance chain which allows changes to
1722 // Marionette.CollectionView.prototype.constructor which allows overriding
1723 constructor: function(){
1724 Marionette.CollectionView.prototype.constructor.apply(this, slice(arguments));
1725 },
1726
1727 // Configured the initial events that the composite view
1728 // binds to. Override this method to prevent the initial
1729 // events, or to add your own initial events.
1730 _initialEvents: function(){
1731 if (this.collection){
1732 this.listenTo(this.collection, "add", this.addChildView, this);
1733 this.listenTo(this.collection, "remove", this.removeItemView, this);
1734 this.listenTo(this.collection, "reset", this._renderChildren, this);
1735 }
1736 },
1737
1738 // Retrieve the `itemView` to be used when rendering each of
1739 // the items in the collection. The default is to return
1740 // `this.itemView` or Marionette.CompositeView if no `itemView`
1741 // has been defined
1742 getItemView: function(item){
1743 var itemView = Marionette.getOption(this, "itemView") || this.constructor;
1744
1745 if (!itemView){
1746 throwError("An `itemView` must be specified", "NoItemViewError");
1747 }
1748
1749 return itemView;
1750 },
1751
1752 // Serialize the collection for the view.
1753 // You can override the `serializeData` method in your own view
1754 // definition, to provide custom serialization for your view's data.
1755 serializeData: function(){
1756 var data = {};
1757
1758 if (this.model){
1759 data = this.model.toJSON();
1760 }
1761
1762 return data;
1763 },
1764
1765 // Renders the model once, and the collection once. Calling
1766 // this again will tell the model's view to re-render itself
1767 // but the collection will not re-render.
1768 render: function(){
1769 this.isRendered = true;
1770 this.isClosed = false;
1771 this.resetItemViewContainer();
1772
1773 this.triggerBeforeRender();
1774 var html = this.renderModel();
1775 this.$el.html(html);
1776 // the ui bindings is done here and not at the end of render since they
1777 // will not be available until after the model is rendered, but should be
1778 // available before the collection is rendered.
1779 this.bindUIElements();
1780 this.triggerMethod("composite:model:rendered");
1781
1782 this._renderChildren();
1783
1784 this.triggerMethod("composite:rendered");
1785 this.triggerRendered();
1786 return this;
1787 },
1788
1789 _renderChildren: function(){
1790 if (this.isRendered){
1791 Marionette.CollectionView.prototype._renderChildren.call(this);
1792 this.triggerMethod("composite:collection:rendered");
1793 }
1794 },
1795
1796 // Render an individual model, if we have one, as
1797 // part of a composite view (branch / leaf). For example:
1798 // a treeview.
1799 renderModel: function(){
1800 var data = {};
1801 data = this.serializeData();
1802 data = this.mixinTemplateHelpers(data);
1803
1804 var template = this.getTemplate();
1805 return Marionette.Renderer.render(template, data);
1806 },
1807
1808 // Appends the `el` of itemView instances to the specified
1809 // `itemViewContainer` (a jQuery selector). Override this method to
1810 // provide custom logic of how the child item view instances have their
1811 // HTML appended to the composite view instance.
1812 appendHtml: function(cv, iv, index){
1813 var $container = this.getItemViewContainer(cv);
1814 $container.append(iv.el);
1815 },
1816
1817 // Internal method to ensure an `$itemViewContainer` exists, for the
1818 // `appendHtml` method to use.
1819 getItemViewContainer: function(containerView){
1820 if ("$itemViewContainer" in containerView){
1821 return containerView.$itemViewContainer;
1822 }
1823
1824 var container;
1825 var itemViewContainer = Marionette.getOption(containerView, "itemViewContainer");
1826 if (itemViewContainer){
1827
1828 var selector = _.isFunction(itemViewContainer) ? itemViewContainer() : itemViewContainer;
1829 container = containerView.$(selector);
1830 if (container.length <= 0) {
1831 throwError("The specified `itemViewContainer` was not found: " + containerView.itemViewContainer, "ItemViewContainerMissingError");
1832 }
1833
1834 } else {
1835 container = containerView.$el;
1836 }
1837
1838 containerView.$itemViewContainer = container;
1839 return container;
1840 },
1841
1842 // Internal method to reset the `$itemViewContainer` on render
1843 resetItemViewContainer: function(){
1844 if (this.$itemViewContainer){
1845 delete this.$itemViewContainer;
1846 }
1847 }
1848});
1849
1850
1851// Layout
1852// ------
1853
1854// Used for managing application layouts, nested layouts and
1855// multiple regions within an application or sub-application.
1856//
1857// A specialized view type that renders an area of HTML and then
1858// attaches `Region` instances to the specified `regions`.
1859// Used for composite view management and sub-application areas.
1860Marionette.Layout = Marionette.ItemView.extend({
1861 regionType: Marionette.Region,
1862
1863 // Ensure the regions are available when the `initialize` method
1864 // is called.
1865 constructor: function (options) {
1866 options = options || {};
1867
1868 this._firstRender = true;
1869 this._initializeRegions(options);
1870
1871 Marionette.ItemView.prototype.constructor.call(this, options);
1872 },
1873
1874 // Layout's render will use the existing region objects the
1875 // first time it is called. Subsequent calls will close the
1876 // views that the regions are showing and then reset the `el`
1877 // for the regions to the newly rendered DOM elements.
1878 render: function(){
1879
1880 if (this.isClosed){
1881 // a previously closed layout means we need to
1882 // completely re-initialize the regions
1883 this._initializeRegions();
1884 }
1885 if (this._firstRender) {
1886 // if this is the first render, don't do anything to
1887 // reset the regions
1888 this._firstRender = false;
1889 } else if (!this.isClosed){
1890 // If this is not the first render call, then we need to
1891 // re-initializing the `el` for each region
1892 this._reInitializeRegions();
1893 }
1894
1895 var args = Array.prototype.slice.apply(arguments);
1896 var result = Marionette.ItemView.prototype.render.apply(this, args);
1897
1898 return result;
1899 },
1900
1901 // Handle closing regions, and then close the view itself.
1902 close: function () {
1903 if (this.isClosed){ return; }
1904 this.regionManager.close();
1905 var args = Array.prototype.slice.apply(arguments);
1906 Marionette.ItemView.prototype.close.apply(this, args);
1907 },
1908
1909 // Add a single region, by name, to the layout
1910 addRegion: function(name, definition){
1911 var regions = {};
1912 regions[name] = definition;
1913 return this._buildRegions(regions)[name];
1914 },
1915
1916 // Add multiple regions as a {name: definition, name2: def2} object literal
1917 addRegions: function(regions){
1918 this.regions = _.extend({}, this.regions, regions);
1919 return this._buildRegions(regions);
1920 },
1921
1922 // Remove a single region from the Layout, by name
1923 removeRegion: function(name){
1924 delete this.regions[name];
1925 return this.regionManager.removeRegion(name);
1926 },
1927
1928 // internal method to build regions
1929 _buildRegions: function(regions){
1930 var that = this;
1931
1932 var defaults = {
1933 regionType: Marionette.getOption(this, "regionType"),
1934 parentEl: function(){ return that.$el; }
1935 };
1936
1937 return this.regionManager.addRegions(regions, defaults);
1938 },
1939
1940 // Internal method to initialize the regions that have been defined in a
1941 // `regions` attribute on this layout.
1942 _initializeRegions: function (options) {
1943 var regions;
1944 this._initRegionManager();
1945
1946 if (_.isFunction(this.regions)) {
1947 regions = this.regions(options);
1948 } else {
1949 regions = this.regions || {};
1950 }
1951
1952 this.addRegions(regions);
1953 },
1954
1955 // Internal method to re-initialize all of the regions by updating the `el` that
1956 // they point to
1957 _reInitializeRegions: function(){
1958 this.regionManager.closeRegions();
1959 this.regionManager.each(function(region){
1960 region.reset();
1961 });
1962 },
1963
1964 // Internal method to initialize the region manager
1965 // and all regions in it
1966 _initRegionManager: function(){
1967 this.regionManager = new Marionette.RegionManager();
1968
1969 this.listenTo(this.regionManager, "region:add", function(name, region){
1970 this[name] = region;
1971 this.trigger("region:add", name, region);
1972 });
1973
1974 this.listenTo(this.regionManager, "region:remove", function(name, region){
1975 delete this[name];
1976 this.trigger("region:remove", name, region);
1977 });
1978 }
1979});
1980
1981
1982// AppRouter
1983// ---------
1984
1985// Reduce the boilerplate code of handling route events
1986// and then calling a single method on another object.
1987// Have your routers configured to call the method on
1988// your object, directly.
1989//
1990// Configure an AppRouter with `appRoutes`.
1991//
1992// App routers can only take one `controller` object.
1993// It is recommended that you divide your controller
1994// objects in to smaller pieces of related functionality
1995// and have multiple routers / controllers, instead of
1996// just one giant router and controller.
1997//
1998// You can also add standard routes to an AppRouter.
1999
2000Marionette.AppRouter = Backbone.Router.extend({
2001
2002 constructor: function(options){
2003 Backbone.Router.prototype.constructor.apply(this, slice(arguments));
2004
2005 this.options = options || {};
2006
2007 var appRoutes = Marionette.getOption(this, "appRoutes");
2008 var controller = this._getController();
2009 this.processAppRoutes(controller, appRoutes);
2010 },
2011
2012 // Similar to route method on a Backbone Router but
2013 // method is called on the controller
2014 appRoute: function(route, methodName) {
2015 var controller = this._getController();
2016 this._addAppRoute(controller, route, methodName);
2017 },
2018
2019 // Internal method to process the `appRoutes` for the
2020 // router, and turn them in to routes that trigger the
2021 // specified method on the specified `controller`.
2022 processAppRoutes: function(controller, appRoutes) {
2023 if (!appRoutes){ return; }
2024
2025 var routeNames = _.keys(appRoutes).reverse(); // Backbone requires reverted order of routes
2026
2027 _.each(routeNames, function(route) {
2028 this._addAppRoute(controller, route, appRoutes[route]);
2029 }, this);
2030 },
2031
2032 _getController: function(){
2033 return Marionette.getOption(this, "controller");
2034 },
2035
2036 _addAppRoute: function(controller, route, methodName){
2037 var method = controller[methodName];
2038
2039 if (!method) {
2040 throw new Error("Method '" + methodName + "' was not found on the controller");
2041 }
2042
2043 this.route(route, methodName, _.bind(method, controller));
2044 }
2045});
2046
2047
2048// Application
2049// -----------
2050
2051// Contain and manage the composite application as a whole.
2052// Stores and starts up `Region` objects, includes an
2053// event aggregator as `app.vent`
2054Marionette.Application = function(options){
2055 this._initRegionManager();
2056 this._initCallbacks = new Marionette.Callbacks();
2057 this.vent = new Backbone.Wreqr.EventAggregator();
2058 this.commands = new Backbone.Wreqr.Commands();
2059 this.reqres = new Backbone.Wreqr.RequestResponse();
2060 this.submodules = {};
2061
2062 _.extend(this, options);
2063
2064 this.triggerMethod = Marionette.triggerMethod;
2065};
2066
2067_.extend(Marionette.Application.prototype, Backbone.Events, {
2068 // Command execution, facilitated by Backbone.Wreqr.Commands
2069 execute: function(){
2070 var args = Array.prototype.slice.apply(arguments);
2071 this.commands.execute.apply(this.commands, args);
2072 },
2073
2074 // Request/response, facilitated by Backbone.Wreqr.RequestResponse
2075 request: function(){
2076 var args = Array.prototype.slice.apply(arguments);
2077 return this.reqres.request.apply(this.reqres, args);
2078 },
2079
2080 // Add an initializer that is either run at when the `start`
2081 // method is called, or run immediately if added after `start`
2082 // has already been called.
2083 addInitializer: function(initializer){
2084 this._initCallbacks.add(initializer);
2085 },
2086
2087 // kick off all of the application's processes.
2088 // initializes all of the regions that have been added
2089 // to the app, and runs all of the initializer functions
2090 start: function(options){
2091 this.triggerMethod("initialize:before", options);
2092 this._initCallbacks.run(options, this);
2093 this.triggerMethod("initialize:after", options);
2094
2095 this.triggerMethod("start", options);
2096 },
2097
2098 // Add regions to your app.
2099 // Accepts a hash of named strings or Region objects
2100 // addRegions({something: "#someRegion"})
2101 // addRegions({something: Region.extend({el: "#someRegion"}) });
2102 addRegions: function(regions){
2103 return this._regionManager.addRegions(regions);
2104 },
2105
2106 // Close all regions in the app, without removing them
2107 closeRegions: function(){
2108 this._regionManager.closeRegions();
2109 },
2110
2111 // Removes a region from your app, by name
2112 // Accepts the regions name
2113 // removeRegion('myRegion')
2114 removeRegion: function(region) {
2115 this._regionManager.removeRegion(region);
2116 },
2117
2118 // Provides alternative access to regions
2119 // Accepts the region name
2120 // getRegion('main')
2121 getRegion: function(region) {
2122 return this._regionManager.get(region);
2123 },
2124
2125 // Create a module, attached to the application
2126 module: function(moduleNames, moduleDefinition){
2127 // slice the args, and add this application object as the
2128 // first argument of the array
2129 var args = slice(arguments);
2130 args.unshift(this);
2131
2132 // see the Marionette.Module object for more information
2133 return Marionette.Module.create.apply(Marionette.Module, args);
2134 },
2135
2136 // Internal method to set up the region manager
2137 _initRegionManager: function(){
2138 this._regionManager = new Marionette.RegionManager();
2139
2140 this.listenTo(this._regionManager, "region:add", function(name, region){
2141 this[name] = region;
2142 });
2143
2144 this.listenTo(this._regionManager, "region:remove", function(name, region){
2145 delete this[name];
2146 });
2147 }
2148});
2149
2150// Copy the `extend` function used by Backbone's classes
2151Marionette.Application.extend = Marionette.extend;
2152
2153// Module
2154// ------
2155
2156// A simple module system, used to create privacy and encapsulation in
2157// Marionette applications
2158Marionette.Module = function(moduleName, app){
2159 this.moduleName = moduleName;
2160
2161 // store sub-modules
2162 this.submodules = {};
2163
2164 this._setupInitializersAndFinalizers();
2165
2166 // store the configuration for this module
2167 this.app = app;
2168 this.startWithParent = true;
2169
2170 this.triggerMethod = Marionette.triggerMethod;
2171};
2172
2173// Extend the Module prototype with events / listenTo, so that the module
2174// can be used as an event aggregator or pub/sub.
2175_.extend(Marionette.Module.prototype, Backbone.Events, {
2176
2177 // Initializer for a specific module. Initializers are run when the
2178 // module's `start` method is called.
2179 addInitializer: function(callback){
2180 this._initializerCallbacks.add(callback);
2181 },
2182
2183 // Finalizers are run when a module is stopped. They are used to teardown
2184 // and finalize any variables, references, events and other code that the
2185 // module had set up.
2186 addFinalizer: function(callback){
2187 this._finalizerCallbacks.add(callback);
2188 },
2189
2190 // Start the module, and run all of its initializers
2191 start: function(options){
2192 // Prevent re-starting a module that is already started
2193 if (this._isInitialized){ return; }
2194
2195 // start the sub-modules (depth-first hierarchy)
2196 _.each(this.submodules, function(mod){
2197 // check to see if we should start the sub-module with this parent
2198 if (mod.startWithParent){
2199 mod.start(options);
2200 }
2201 });
2202
2203 // run the callbacks to "start" the current module
2204 this.triggerMethod("before:start", options);
2205
2206 this._initializerCallbacks.run(options, this);
2207 this._isInitialized = true;
2208
2209 this.triggerMethod("start", options);
2210 },
2211
2212 // Stop this module by running its finalizers and then stop all of
2213 // the sub-modules for this module
2214 stop: function(){
2215 // if we are not initialized, don't bother finalizing
2216 if (!this._isInitialized){ return; }
2217 this._isInitialized = false;
2218
2219 Marionette.triggerMethod.call(this, "before:stop");
2220
2221 // stop the sub-modules; depth-first, to make sure the
2222 // sub-modules are stopped / finalized before parents
2223 _.each(this.submodules, function(mod){ mod.stop(); });
2224
2225 // run the finalizers
2226 this._finalizerCallbacks.run(undefined,this);
2227
2228 // reset the initializers and finalizers
2229 this._initializerCallbacks.reset();
2230 this._finalizerCallbacks.reset();
2231
2232 Marionette.triggerMethod.call(this, "stop");
2233 },
2234
2235 // Configure the module with a definition function and any custom args
2236 // that are to be passed in to the definition function
2237 addDefinition: function(moduleDefinition, customArgs){
2238 this._runModuleDefinition(moduleDefinition, customArgs);
2239 },
2240
2241 // Internal method: run the module definition function with the correct
2242 // arguments
2243 _runModuleDefinition: function(definition, customArgs){
2244 if (!definition){ return; }
2245
2246 // build the correct list of arguments for the module definition
2247 var args = _.flatten([
2248 this,
2249 this.app,
2250 Backbone,
2251 Marionette,
2252 Marionette.$, _,
2253 customArgs
2254 ]);
2255
2256 definition.apply(this, args);
2257 },
2258
2259 // Internal method: set up new copies of initializers and finalizers.
2260 // Calling this method will wipe out all existing initializers and
2261 // finalizers.
2262 _setupInitializersAndFinalizers: function(){
2263 this._initializerCallbacks = new Marionette.Callbacks();
2264 this._finalizerCallbacks = new Marionette.Callbacks();
2265 }
2266});
2267
2268// Type methods to create modules
2269_.extend(Marionette.Module, {
2270
2271 // Create a module, hanging off the app parameter as the parent object.
2272 create: function(app, moduleNames, moduleDefinition){
2273 var module = app;
2274
2275 // get the custom args passed in after the module definition and
2276 // get rid of the module name and definition function
2277 var customArgs = slice(arguments);
2278 customArgs.splice(0, 3);
2279
2280 // split the module names and get the length
2281 moduleNames = moduleNames.split(".");
2282 var length = moduleNames.length;
2283
2284 // store the module definition for the last module in the chain
2285 var moduleDefinitions = [];
2286 moduleDefinitions[length-1] = moduleDefinition;
2287
2288 // Loop through all the parts of the module definition
2289 _.each(moduleNames, function(moduleName, i){
2290 var parentModule = module;
2291 module = this._getModule(parentModule, moduleName, app);
2292 this._addModuleDefinition(parentModule, module, moduleDefinitions[i], customArgs);
2293 }, this);
2294
2295 // Return the last module in the definition chain
2296 return module;
2297 },
2298
2299 _getModule: function(parentModule, moduleName, app, def, args){
2300 // Get an existing module of this name if we have one
2301 var module = parentModule[moduleName];
2302
2303 if (!module){
2304 // Create a new module if we don't have one
2305 module = new Marionette.Module(moduleName, app);
2306 parentModule[moduleName] = module;
2307 // store the module on the parent
2308 parentModule.submodules[moduleName] = module;
2309 }
2310
2311 return module;
2312 },
2313
2314 _addModuleDefinition: function(parentModule, module, def, args){
2315 var fn;
2316 var startWithParent;
2317
2318 if (_.isFunction(def)){
2319 // if a function is supplied for the module definition
2320 fn = def;
2321 startWithParent = true;
2322
2323 } else if (_.isObject(def)){
2324 // if an object is supplied
2325 fn = def.define;
2326 startWithParent = def.startWithParent;
2327
2328 } else {
2329 // if nothing is supplied
2330 startWithParent = true;
2331 }
2332
2333 // add module definition if needed
2334 if (fn){
2335 module.addDefinition(fn, args);
2336 }
2337
2338 // `and` the two together, ensuring a single `false` will prevent it
2339 // from starting with the parent
2340 module.startWithParent = module.startWithParent && startWithParent;
2341
2342 // setup auto-start if needed
2343 if (module.startWithParent && !module.startWithParentIsConfigured){
2344
2345 // only configure this once
2346 module.startWithParentIsConfigured = true;
2347
2348 // add the module initializer config
2349 parentModule.addInitializer(function(options){
2350 if (module.startWithParent){
2351 module.start(options);
2352 }
2353 });
2354
2355 }
2356
2357 }
2358});
2359
2360
2361
2362 return Marionette;
2363})(this, Backbone, _);
0\ No newline at end of file2364\ No newline at end of file
12365
=== added directory 'web_unleashed/static/lib/underscore'
=== added directory 'web_unleashed/static/lib/underscore-addons'
=== added file 'web_unleashed/static/lib/underscore-addons/underscore.deep_extend.js'
--- web_unleashed/static/lib/underscore-addons/underscore.deep_extend.js 1970-01-01 00:00:00 +0000
+++ web_unleashed/static/lib/underscore-addons/underscore.deep_extend.js 2014-01-28 13:12:54 +0000
@@ -0,0 +1,52 @@
1/* Copyright (C) 2012-2013 Kurt Milam - http://xioup.com | Source: https://gist.github.com/1868955
2 *
3 * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4 * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
5 *
6 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
7**/
8
9// Based conceptually on the _.extend() function in underscore.js ( see http://documentcloud.github.com/underscore/#extend for more details )
10
11// source: https://gist.github.com/kurtmilam/1868955
12
13(function(Underscore){
14 var _ = Underscore;
15 var deepExtend = function(obj) {
16 var parentRE = /#{\s*?_\s*?}/, slice = Array.prototype.slice, hasOwnProperty = Object.prototype.hasOwnProperty;
17
18 _.each(slice.call(arguments, 1), function(source) {
19 for (var prop in source) {
20 if (hasOwnProperty.call(source, prop)) {
21 if (_.isUndefined(obj[prop]) || _.isFunction(obj[prop]) || _.isNull(source[prop])) {
22 obj[prop] = source[prop];
23 } else if (_.isString(source[prop]) && parentRE.test(source[prop])) {
24 if (_.isString(obj[prop])) {
25 obj[prop] = source[prop].replace(parentRE, obj[prop]);
26 }
27 } else if (_.isArray(obj[prop]) || _.isArray(source[prop])) {
28 if (!_.isArray(obj[prop]) || !_.isArray(source[prop])) {
29 throw 'Error: Trying to combine an array with a non-array (' + prop + ')';
30 } else {
31 obj[prop] = _.reject(_.deepExtend(obj[prop], source[prop]), function(item) {
32 return _.isNull(item);
33 });
34 }
35 } else if (_.isObject(obj[prop]) || _.isObject(source[prop])) {
36 if (!_.isObject(obj[prop]) || !_.isObject(source[prop])) {
37 throw 'Error: Trying to combine an object with a non-object (' + prop + ')';
38 } else {
39 obj[prop] = _.deepExtend(obj[prop], source[prop]);
40 }
41 } else {
42 obj[prop] = source[prop];
43 }
44 }
45 }
46 });
47 return obj;
48 };
49
50 Underscore.mixin({deepExtend: deepExtend});
51
52})(_ || Underscore)
0\ No newline at end of file53\ No newline at end of file
154
=== added file 'web_unleashed/static/lib/underscore-addons/underscore.find_index_where.js'
--- web_unleashed/static/lib/underscore-addons/underscore.find_index_where.js 1970-01-01 00:00:00 +0000
+++ web_unleashed/static/lib/underscore-addons/underscore.find_index_where.js 2014-01-28 13:12:54 +0000
@@ -0,0 +1,25 @@
1(function(Underscore) {
2 var _ = Underscore;
3 var findIndexWhere = function(obj, find) {
4
5 var index = null, name;
6 _.each(obj, function(item, i) {
7 var found = true;
8 for(name in find){
9 if(item[name] !== find[name]){
10 found = false;
11 }
12 }
13 if(found){
14 index = i;
15 }
16 });
17
18 return obj;
19 };
20
21 Underscore.mixin({
22 findIndexWhere : findIndexWhere
23 });
24
25})(_ || Underscore)
0\ No newline at end of file26\ No newline at end of file
127
=== added file 'web_unleashed/static/lib/underscore/underscore.js'
--- web_unleashed/static/lib/underscore/underscore.js 1970-01-01 00:00:00 +0000
+++ web_unleashed/static/lib/underscore/underscore.js 2014-01-28 13:12:54 +0000
@@ -0,0 +1,1276 @@
1// Underscore.js 1.5.2
2// http://underscorejs.org
3// (c) 2009-2013 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors
4// Underscore may be freely distributed under the MIT license.
5
6(function() {
7
8 // Baseline setup
9 // --------------
10
11 // Establish the root object, `window` in the browser, or `exports` on the server.
12 var root = this;
13
14 // Save the previous value of the `_` variable.
15 var previousUnderscore = root._;
16
17 // Establish the object that gets returned to break out of a loop iteration.
18 var breaker = {};
19
20 // Save bytes in the minified (but not gzipped) version:
21 var ArrayProto = Array.prototype, ObjProto = Object.prototype, FuncProto = Function.prototype;
22
23 // Create quick reference variables for speed access to core prototypes.
24 var
25 push = ArrayProto.push,
26 slice = ArrayProto.slice,
27 concat = ArrayProto.concat,
28 toString = ObjProto.toString,
29 hasOwnProperty = ObjProto.hasOwnProperty;
30
31 // All **ECMAScript 5** native function implementations that we hope to use
32 // are declared here.
33 var
34 nativeForEach = ArrayProto.forEach,
35 nativeMap = ArrayProto.map,
36 nativeReduce = ArrayProto.reduce,
37 nativeReduceRight = ArrayProto.reduceRight,
38 nativeFilter = ArrayProto.filter,
39 nativeEvery = ArrayProto.every,
40 nativeSome = ArrayProto.some,
41 nativeIndexOf = ArrayProto.indexOf,
42 nativeLastIndexOf = ArrayProto.lastIndexOf,
43 nativeIsArray = Array.isArray,
44 nativeKeys = Object.keys,
45 nativeBind = FuncProto.bind;
46
47 // Create a safe reference to the Underscore object for use below.
48 var _ = function(obj) {
49 if (obj instanceof _) return obj;
50 if (!(this instanceof _)) return new _(obj);
51 this._wrapped = obj;
52 };
53
54 // Export the Underscore object for **Node.js**, with
55 // backwards-compatibility for the old `require()` API. If we're in
56 // the browser, add `_` as a global object via a string identifier,
57 // for Closure Compiler "advanced" mode.
58 if (typeof exports !== 'undefined') {
59 if (typeof module !== 'undefined' && module.exports) {
60 exports = module.exports = _;
61 }
62 exports._ = _;
63 } else {
64 root._ = _;
65 }
66
67 // Current version.
68 _.VERSION = '1.5.2';
69
70 // Collection Functions
71 // --------------------
72
73 // The cornerstone, an `each` implementation, aka `forEach`.
74 // Handles objects with the built-in `forEach`, arrays, and raw objects.
75 // Delegates to **ECMAScript 5**'s native `forEach` if available.
76 var each = _.each = _.forEach = function(obj, iterator, context) {
77 if (obj == null) return;
78 if (nativeForEach && obj.forEach === nativeForEach) {
79 obj.forEach(iterator, context);
80 } else if (obj.length === +obj.length) {
81 for (var i = 0, length = obj.length; i < length; i++) {
82 if (iterator.call(context, obj[i], i, obj) === breaker) return;
83 }
84 } else {
85 var keys = _.keys(obj);
86 for (var i = 0, length = keys.length; i < length; i++) {
87 if (iterator.call(context, obj[keys[i]], keys[i], obj) === breaker) return;
88 }
89 }
90 };
91
92 // Return the results of applying the iterator to each element.
93 // Delegates to **ECMAScript 5**'s native `map` if available.
94 _.map = _.collect = function(obj, iterator, context) {
95 var results = [];
96 if (obj == null) return results;
97 if (nativeMap && obj.map === nativeMap) return obj.map(iterator, context);
98 each(obj, function(value, index, list) {
99 results.push(iterator.call(context, value, index, list));
100 });
101 return results;
102 };
103
104 var reduceError = 'Reduce of empty array with no initial value';
105
106 // **Reduce** builds up a single result from a list of values, aka `inject`,
107 // or `foldl`. Delegates to **ECMAScript 5**'s native `reduce` if available.
108 _.reduce = _.foldl = _.inject = function(obj, iterator, memo, context) {
109 var initial = arguments.length > 2;
110 if (obj == null) obj = [];
111 if (nativeReduce && obj.reduce === nativeReduce) {
112 if (context) iterator = _.bind(iterator, context);
113 return initial ? obj.reduce(iterator, memo) : obj.reduce(iterator);
114 }
115 each(obj, function(value, index, list) {
116 if (!initial) {
117 memo = value;
118 initial = true;
119 } else {
120 memo = iterator.call(context, memo, value, index, list);
121 }
122 });
123 if (!initial) throw new TypeError(reduceError);
124 return memo;
125 };
126
127 // The right-associative version of reduce, also known as `foldr`.
128 // Delegates to **ECMAScript 5**'s native `reduceRight` if available.
129 _.reduceRight = _.foldr = function(obj, iterator, memo, context) {
130 var initial = arguments.length > 2;
131 if (obj == null) obj = [];
132 if (nativeReduceRight && obj.reduceRight === nativeReduceRight) {
133 if (context) iterator = _.bind(iterator, context);
134 return initial ? obj.reduceRight(iterator, memo) : obj.reduceRight(iterator);
135 }
136 var length = obj.length;
137 if (length !== +length) {
138 var keys = _.keys(obj);
139 length = keys.length;
140 }
141 each(obj, function(value, index, list) {
142 index = keys ? keys[--length] : --length;
143 if (!initial) {
144 memo = obj[index];
145 initial = true;
146 } else {
147 memo = iterator.call(context, memo, obj[index], index, list);
148 }
149 });
150 if (!initial) throw new TypeError(reduceError);
151 return memo;
152 };
153
154 // Return the first value which passes a truth test. Aliased as `detect`.
155 _.find = _.detect = function(obj, iterator, context) {
156 var result;
157 any(obj, function(value, index, list) {
158 if (iterator.call(context, value, index, list)) {
159 result = value;
160 return true;
161 }
162 });
163 return result;
164 };
165
166 // Return all the elements that pass a truth test.
167 // Delegates to **ECMAScript 5**'s native `filter` if available.
168 // Aliased as `select`.
169 _.filter = _.select = function(obj, iterator, context) {
170 var results = [];
171 if (obj == null) return results;
172 if (nativeFilter && obj.filter === nativeFilter) return obj.filter(iterator, context);
173 each(obj, function(value, index, list) {
174 if (iterator.call(context, value, index, list)) results.push(value);
175 });
176 return results;
177 };
178
179 // Return all the elements for which a truth test fails.
180 _.reject = function(obj, iterator, context) {
181 return _.filter(obj, function(value, index, list) {
182 return !iterator.call(context, value, index, list);
183 }, context);
184 };
185
186 // Determine whether all of the elements match a truth test.
187 // Delegates to **ECMAScript 5**'s native `every` if available.
188 // Aliased as `all`.
189 _.every = _.all = function(obj, iterator, context) {
190 iterator || (iterator = _.identity);
191 var result = true;
192 if (obj == null) return result;
193 if (nativeEvery && obj.every === nativeEvery) return obj.every(iterator, context);
194 each(obj, function(value, index, list) {
195 if (!(result = result && iterator.call(context, value, index, list))) return breaker;
196 });
197 return !!result;
198 };
199
200 // Determine if at least one element in the object matches a truth test.
201 // Delegates to **ECMAScript 5**'s native `some` if available.
202 // Aliased as `any`.
203 var any = _.some = _.any = function(obj, iterator, context) {
204 iterator || (iterator = _.identity);
205 var result = false;
206 if (obj == null) return result;
207 if (nativeSome && obj.some === nativeSome) return obj.some(iterator, context);
208 each(obj, function(value, index, list) {
209 if (result || (result = iterator.call(context, value, index, list))) return breaker;
210 });
211 return !!result;
212 };
213
214 // Determine if the array or object contains a given value (using `===`).
215 // Aliased as `include`.
216 _.contains = _.include = function(obj, target) {
217 if (obj == null) return false;
218 if (nativeIndexOf && obj.indexOf === nativeIndexOf) return obj.indexOf(target) != -1;
219 return any(obj, function(value) {
220 return value === target;
221 });
222 };
223
224 // Invoke a method (with arguments) on every item in a collection.
225 _.invoke = function(obj, method) {
226 var args = slice.call(arguments, 2);
227 var isFunc = _.isFunction(method);
228 return _.map(obj, function(value) {
229 return (isFunc ? method : value[method]).apply(value, args);
230 });
231 };
232
233 // Convenience version of a common use case of `map`: fetching a property.
234 _.pluck = function(obj, key) {
235 return _.map(obj, function(value){ return value[key]; });
236 };
237
238 // Convenience version of a common use case of `filter`: selecting only objects
239 // containing specific `key:value` pairs.
240 _.where = function(obj, attrs, first) {
241 if (_.isEmpty(attrs)) return first ? void 0 : [];
242 return _[first ? 'find' : 'filter'](obj, function(value) {
243 for (var key in attrs) {
244 if (attrs[key] !== value[key]) return false;
245 }
246 return true;
247 });
248 };
249
250 // Convenience version of a common use case of `find`: getting the first object
251 // containing specific `key:value` pairs.
252 _.findWhere = function(obj, attrs) {
253 return _.where(obj, attrs, true);
254 };
255
256 // Return the maximum element or (element-based computation).
257 // Can't optimize arrays of integers longer than 65,535 elements.
258 // See [WebKit Bug 80797](https://bugs.webkit.org/show_bug.cgi?id=80797)
259 _.max = function(obj, iterator, context) {
260 if (!iterator && _.isArray(obj) && obj[0] === +obj[0] && obj.length < 65535) {
261 return Math.max.apply(Math, obj);
262 }
263 if (!iterator && _.isEmpty(obj)) return -Infinity;
264 var result = {computed : -Infinity, value: -Infinity};
265 each(obj, function(value, index, list) {
266 var computed = iterator ? iterator.call(context, value, index, list) : value;
267 computed > result.computed && (result = {value : value, computed : computed});
268 });
269 return result.value;
270 };
271
272 // Return the minimum element (or element-based computation).
273 _.min = function(obj, iterator, context) {
274 if (!iterator && _.isArray(obj) && obj[0] === +obj[0] && obj.length < 65535) {
275 return Math.min.apply(Math, obj);
276 }
277 if (!iterator && _.isEmpty(obj)) return Infinity;
278 var result = {computed : Infinity, value: Infinity};
279 each(obj, function(value, index, list) {
280 var computed = iterator ? iterator.call(context, value, index, list) : value;
281 computed < result.computed && (result = {value : value, computed : computed});
282 });
283 return result.value;
284 };
285
286 // Shuffle an array, using the modern version of the
287 // [Fisher-Yates shuffle](http://en.wikipedia.org/wiki/Fisher–Yates_shuffle).
288 _.shuffle = function(obj) {
289 var rand;
290 var index = 0;
291 var shuffled = [];
292 each(obj, function(value) {
293 rand = _.random(index++);
294 shuffled[index - 1] = shuffled[rand];
295 shuffled[rand] = value;
296 });
297 return shuffled;
298 };
299
300 // Sample **n** random values from an array.
301 // If **n** is not specified, returns a single random element from the array.
302 // The internal `guard` argument allows it to work with `map`.
303 _.sample = function(obj, n, guard) {
304 if (arguments.length < 2 || guard) {
305 return obj[_.random(obj.length - 1)];
306 }
307 return _.shuffle(obj).slice(0, Math.max(0, n));
308 };
309
310 // An internal function to generate lookup iterators.
311 var lookupIterator = function(value) {
312 return _.isFunction(value) ? value : function(obj){ return obj[value]; };
313 };
314
315 // Sort the object's values by a criterion produced by an iterator.
316 _.sortBy = function(obj, value, context) {
317 var iterator = lookupIterator(value);
318 return _.pluck(_.map(obj, function(value, index, list) {
319 return {
320 value: value,
321 index: index,
322 criteria: iterator.call(context, value, index, list)
323 };
324 }).sort(function(left, right) {
325 var a = left.criteria;
326 var b = right.criteria;
327 if (a !== b) {
328 if (a > b || a === void 0) return 1;
329 if (a < b || b === void 0) return -1;
330 }
331 return left.index - right.index;
332 }), 'value');
333 };
334
335 // An internal function used for aggregate "group by" operations.
336 var group = function(behavior) {
337 return function(obj, value, context) {
338 var result = {};
339 var iterator = value == null ? _.identity : lookupIterator(value);
340 each(obj, function(value, index) {
341 var key = iterator.call(context, value, index, obj);
342 behavior(result, key, value);
343 });
344 return result;
345 };
346 };
347
348 // Groups the object's values by a criterion. Pass either a string attribute
349 // to group by, or a function that returns the criterion.
350 _.groupBy = group(function(result, key, value) {
351 (_.has(result, key) ? result[key] : (result[key] = [])).push(value);
352 });
353
354 // Indexes the object's values by a criterion, similar to `groupBy`, but for
355 // when you know that your index values will be unique.
356 _.indexBy = group(function(result, key, value) {
357 result[key] = value;
358 });
359
360 // Counts instances of an object that group by a certain criterion. Pass
361 // either a string attribute to count by, or a function that returns the
362 // criterion.
363 _.countBy = group(function(result, key) {
364 _.has(result, key) ? result[key]++ : result[key] = 1;
365 });
366
367 // Use a comparator function to figure out the smallest index at which
368 // an object should be inserted so as to maintain order. Uses binary search.
369 _.sortedIndex = function(array, obj, iterator, context) {
370 iterator = iterator == null ? _.identity : lookupIterator(iterator);
371 var value = iterator.call(context, obj);
372 var low = 0, high = array.length;
373 while (low < high) {
374 var mid = (low + high) >>> 1;
375 iterator.call(context, array[mid]) < value ? low = mid + 1 : high = mid;
376 }
377 return low;
378 };
379
380 // Safely create a real, live array from anything iterable.
381 _.toArray = function(obj) {
382 if (!obj) return [];
383 if (_.isArray(obj)) return slice.call(obj);
384 if (obj.length === +obj.length) return _.map(obj, _.identity);
385 return _.values(obj);
386 };
387
388 // Return the number of elements in an object.
389 _.size = function(obj) {
390 if (obj == null) return 0;
391 return (obj.length === +obj.length) ? obj.length : _.keys(obj).length;
392 };
393
394 // Array Functions
395 // ---------------
396
397 // Get the first element of an array. Passing **n** will return the first N
398 // values in the array. Aliased as `head` and `take`. The **guard** check
399 // allows it to work with `_.map`.
400 _.first = _.head = _.take = function(array, n, guard) {
401 if (array == null) return void 0;
402 return (n == null) || guard ? array[0] : slice.call(array, 0, n);
403 };
404
405 // Returns everything but the last entry of the array. Especially useful on
406 // the arguments object. Passing **n** will return all the values in
407 // the array, excluding the last N. The **guard** check allows it to work with
408 // `_.map`.
409 _.initial = function(array, n, guard) {
410 return slice.call(array, 0, array.length - ((n == null) || guard ? 1 : n));
411 };
412
413 // Get the last element of an array. Passing **n** will return the last N
414 // values in the array. The **guard** check allows it to work with `_.map`.
415 _.last = function(array, n, guard) {
416 if (array == null) return void 0;
417 if ((n == null) || guard) {
418 return array[array.length - 1];
419 } else {
420 return slice.call(array, Math.max(array.length - n, 0));
421 }
422 };
423
424 // Returns everything but the first entry of the array. Aliased as `tail` and `drop`.
425 // Especially useful on the arguments object. Passing an **n** will return
426 // the rest N values in the array. The **guard**
427 // check allows it to work with `_.map`.
428 _.rest = _.tail = _.drop = function(array, n, guard) {
429 return slice.call(array, (n == null) || guard ? 1 : n);
430 };
431
432 // Trim out all falsy values from an array.
433 _.compact = function(array) {
434 return _.filter(array, _.identity);
435 };
436
437 // Internal implementation of a recursive `flatten` function.
438 var flatten = function(input, shallow, output) {
439 if (shallow && _.every(input, _.isArray)) {
440 return concat.apply(output, input);
441 }
442 each(input, function(value) {
443 if (_.isArray(value) || _.isArguments(value)) {
444 shallow ? push.apply(output, value) : flatten(value, shallow, output);
445 } else {
446 output.push(value);
447 }
448 });
449 return output;
450 };
451
452 // Flatten out an array, either recursively (by default), or just one level.
453 _.flatten = function(array, shallow) {
454 return flatten(array, shallow, []);
455 };
456
457 // Return a version of the array that does not contain the specified value(s).
458 _.without = function(array) {
459 return _.difference(array, slice.call(arguments, 1));
460 };
461
462 // Produce a duplicate-free version of the array. If the array has already
463 // been sorted, you have the option of using a faster algorithm.
464 // Aliased as `unique`.
465 _.uniq = _.unique = function(array, isSorted, iterator, context) {
466 if (_.isFunction(isSorted)) {
467 context = iterator;
468 iterator = isSorted;
469 isSorted = false;
470 }
471 var initial = iterator ? _.map(array, iterator, context) : array;
472 var results = [];
473 var seen = [];
474 each(initial, function(value, index) {
475 if (isSorted ? (!index || seen[seen.length - 1] !== value) : !_.contains(seen, value)) {
476 seen.push(value);
477 results.push(array[index]);
478 }
479 });
480 return results;
481 };
482
483 // Produce an array that contains the union: each distinct element from all of
484 // the passed-in arrays.
485 _.union = function() {
486 return _.uniq(_.flatten(arguments, true));
487 };
488
489 // Produce an array that contains every item shared between all the
490 // passed-in arrays.
491 _.intersection = function(array) {
492 var rest = slice.call(arguments, 1);
493 return _.filter(_.uniq(array), function(item) {
494 return _.every(rest, function(other) {
495 return _.indexOf(other, item) >= 0;
496 });
497 });
498 };
499
500 // Take the difference between one array and a number of other arrays.
501 // Only the elements present in just the first array will remain.
502 _.difference = function(array) {
503 var rest = concat.apply(ArrayProto, slice.call(arguments, 1));
504 return _.filter(array, function(value){ return !_.contains(rest, value); });
505 };
506
507 // Zip together multiple lists into a single array -- elements that share
508 // an index go together.
509 _.zip = function() {
510 var length = _.max(_.pluck(arguments, "length").concat(0));
511 var results = new Array(length);
512 for (var i = 0; i < length; i++) {
513 results[i] = _.pluck(arguments, '' + i);
514 }
515 return results;
516 };
517
518 // Converts lists into objects. Pass either a single array of `[key, value]`
519 // pairs, or two parallel arrays of the same length -- one of keys, and one of
520 // the corresponding values.
521 _.object = function(list, values) {
522 if (list == null) return {};
523 var result = {};
524 for (var i = 0, length = list.length; i < length; i++) {
525 if (values) {
526 result[list[i]] = values[i];
527 } else {
528 result[list[i][0]] = list[i][1];
529 }
530 }
531 return result;
532 };
533
534 // If the browser doesn't supply us with indexOf (I'm looking at you, **MSIE**),
535 // we need this function. Return the position of the first occurrence of an
536 // item in an array, or -1 if the item is not included in the array.
537 // Delegates to **ECMAScript 5**'s native `indexOf` if available.
538 // If the array is large and already in sort order, pass `true`
539 // for **isSorted** to use binary search.
540 _.indexOf = function(array, item, isSorted) {
541 if (array == null) return -1;
542 var i = 0, length = array.length;
543 if (isSorted) {
544 if (typeof isSorted == 'number') {
545 i = (isSorted < 0 ? Math.max(0, length + isSorted) : isSorted);
546 } else {
547 i = _.sortedIndex(array, item);
548 return array[i] === item ? i : -1;
549 }
550 }
551 if (nativeIndexOf && array.indexOf === nativeIndexOf) return array.indexOf(item, isSorted);
552 for (; i < length; i++) if (array[i] === item) return i;
553 return -1;
554 };
555
556 // Delegates to **ECMAScript 5**'s native `lastIndexOf` if available.
557 _.lastIndexOf = function(array, item, from) {
558 if (array == null) return -1;
559 var hasIndex = from != null;
560 if (nativeLastIndexOf && array.lastIndexOf === nativeLastIndexOf) {
561 return hasIndex ? array.lastIndexOf(item, from) : array.lastIndexOf(item);
562 }
563 var i = (hasIndex ? from : array.length);
564 while (i--) if (array[i] === item) return i;
565 return -1;
566 };
567
568 // Generate an integer Array containing an arithmetic progression. A port of
569 // the native Python `range()` function. See
570 // [the Python documentation](http://docs.python.org/library/functions.html#range).
571 _.range = function(start, stop, step) {
572 if (arguments.length <= 1) {
573 stop = start || 0;
574 start = 0;
575 }
576 step = arguments[2] || 1;
577
578 var length = Math.max(Math.ceil((stop - start) / step), 0);
579 var idx = 0;
580 var range = new Array(length);
581
582 while(idx < length) {
583 range[idx++] = start;
584 start += step;
585 }
586
587 return range;
588 };
589
590 // Function (ahem) Functions
591 // ------------------
592
593 // Reusable constructor function for prototype setting.
594 var ctor = function(){};
595
596 // Create a function bound to a given object (assigning `this`, and arguments,
597 // optionally). Delegates to **ECMAScript 5**'s native `Function.bind` if
598 // available.
599 _.bind = function(func, context) {
600 var args, bound;
601 if (nativeBind && func.bind === nativeBind) return nativeBind.apply(func, slice.call(arguments, 1));
602 if (!_.isFunction(func)) throw new TypeError;
603 args = slice.call(arguments, 2);
604 return bound = function() {
605 if (!(this instanceof bound)) return func.apply(context, args.concat(slice.call(arguments)));
606 ctor.prototype = func.prototype;
607 var self = new ctor;
608 ctor.prototype = null;
609 var result = func.apply(self, args.concat(slice.call(arguments)));
610 if (Object(result) === result) return result;
611 return self;
612 };
613 };
614
615 // Partially apply a function by creating a version that has had some of its
616 // arguments pre-filled, without changing its dynamic `this` context.
617 _.partial = function(func) {
618 var args = slice.call(arguments, 1);
619 return function() {
620 return func.apply(this, args.concat(slice.call(arguments)));
621 };
622 };
623
624 // Bind all of an object's methods to that object. Useful for ensuring that
625 // all callbacks defined on an object belong to it.
626 _.bindAll = function(obj) {
627 var funcs = slice.call(arguments, 1);
628 if (funcs.length === 0) throw new Error("bindAll must be passed function names");
629 each(funcs, function(f) { obj[f] = _.bind(obj[f], obj); });
630 return obj;
631 };
632
633 // Memoize an expensive function by storing its results.
634 _.memoize = function(func, hasher) {
635 var memo = {};
636 hasher || (hasher = _.identity);
637 return function() {
638 var key = hasher.apply(this, arguments);
639 return _.has(memo, key) ? memo[key] : (memo[key] = func.apply(this, arguments));
640 };
641 };
642
643 // Delays a function for the given number of milliseconds, and then calls
644 // it with the arguments supplied.
645 _.delay = function(func, wait) {
646 var args = slice.call(arguments, 2);
647 return setTimeout(function(){ return func.apply(null, args); }, wait);
648 };
649
650 // Defers a function, scheduling it to run after the current call stack has
651 // cleared.
652 _.defer = function(func) {
653 return _.delay.apply(_, [func, 1].concat(slice.call(arguments, 1)));
654 };
655
656 // Returns a function, that, when invoked, will only be triggered at most once
657 // during a given window of time. Normally, the throttled function will run
658 // as much as it can, without ever going more than once per `wait` duration;
659 // but if you'd like to disable the execution on the leading edge, pass
660 // `{leading: false}`. To disable execution on the trailing edge, ditto.
661 _.throttle = function(func, wait, options) {
662 var context, args, result;
663 var timeout = null;
664 var previous = 0;
665 options || (options = {});
666 var later = function() {
667 previous = options.leading === false ? 0 : new Date;
668 timeout = null;
669 result = func.apply(context, args);
670 };
671 return function() {
672 var now = new Date;
673 if (!previous && options.leading === false) previous = now;
674 var remaining = wait - (now - previous);
675 context = this;
676 args = arguments;
677 if (remaining <= 0) {
678 clearTimeout(timeout);
679 timeout = null;
680 previous = now;
681 result = func.apply(context, args);
682 } else if (!timeout && options.trailing !== false) {
683 timeout = setTimeout(later, remaining);
684 }
685 return result;
686 };
687 };
688
689 // Returns a function, that, as long as it continues to be invoked, will not
690 // be triggered. The function will be called after it stops being called for
691 // N milliseconds. If `immediate` is passed, trigger the function on the
692 // leading edge, instead of the trailing.
693 _.debounce = function(func, wait, immediate) {
694 var timeout, args, context, timestamp, result;
695 return function() {
696 context = this;
697 args = arguments;
698 timestamp = new Date();
699 var later = function() {
700 var last = (new Date()) - timestamp;
701 if (last < wait) {
702 timeout = setTimeout(later, wait - last);
703 } else {
704 timeout = null;
705 if (!immediate) result = func.apply(context, args);
706 }
707 };
708 var callNow = immediate && !timeout;
709 if (!timeout) {
710 timeout = setTimeout(later, wait);
711 }
712 if (callNow) result = func.apply(context, args);
713 return result;
714 };
715 };
716
717 // Returns a function that will be executed at most one time, no matter how
718 // often you call it. Useful for lazy initialization.
719 _.once = function(func) {
720 var ran = false, memo;
721 return function() {
722 if (ran) return memo;
723 ran = true;
724 memo = func.apply(this, arguments);
725 func = null;
726 return memo;
727 };
728 };
729
730 // Returns the first function passed as an argument to the second,
731 // allowing you to adjust arguments, run code before and after, and
The diff has been truncated for viewing.

Subscribers

People subscribed via source and target branches