pbw 0.0.10 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. checksums.yaml +8 -8
  2. data/Rakefile +1 -10
  3. data/app/controllers/pbw/{application_controller.rb → base_controller.rb} +1 -3
  4. data/app/controllers/pbw/base_models_controller.rb +13 -19
  5. data/app/controllers/pbw/item_containers_controller.rb +36 -0
  6. data/app/controllers/pbw/passwords_controller.rb +2 -2
  7. data/app/controllers/pbw/registrations_controller.rb +5 -3
  8. data/app/controllers/pbw/sessions_controller.rb +6 -3
  9. data/app/controllers/pbw/tokens_controller.rb +4 -0
  10. data/app/models/pbw/ability.rb +3 -3
  11. data/app/models/pbw/attached_process.rb +2 -0
  12. data/app/models/pbw/capability.rb +1 -1
  13. data/app/models/pbw/constraint.rb +1 -1
  14. data/app/models/pbw/trigger.rb +1 -1
  15. data/app/models/pbw/user.rb +3 -3
  16. data/config/initializers/mongoid_accessible_attributes.rb +14 -0
  17. data/config/routes.rb +15 -6
  18. data/lib/generators/pbw/area/area_generator.rb +4 -0
  19. data/lib/generators/pbw/install/install_generator.rb +1 -1
  20. data/lib/generators/pbw/item/item_generator.rb +4 -0
  21. data/lib/generators/pbw/resource_helpers.rb +4 -0
  22. data/lib/generators/pbw/rules/command/command_generator.rb +10 -0
  23. data/lib/generators/pbw/scaffold_generator.rb +15 -12
  24. data/lib/generators/pbw/templates/index.erb +1 -0
  25. data/lib/generators/pbw/templates/model.coffee +5 -5
  26. data/lib/generators/pbw/templates/pbw.coffee +18 -9
  27. data/lib/generators/pbw/templates/router.coffee +9 -0
  28. data/lib/generators/pbw/templates/templates/edit.jst +19 -4
  29. data/lib/generators/pbw/templates/templates/home.jst +2 -1
  30. data/lib/generators/pbw/templates/templates/index.jst +21 -10
  31. data/lib/generators/pbw/templates/templates/model.jst +15 -2
  32. data/lib/generators/pbw/templates/templates/new.jst +20 -4
  33. data/lib/generators/pbw/templates/templates/show.jst +12 -3
  34. data/lib/generators/pbw/templates/user_recovery.coffee +0 -1
  35. data/lib/generators/pbw/templates/user_registration.coffee +0 -1
  36. data/lib/generators/pbw/templates/user_session.coffee +0 -1
  37. data/lib/generators/pbw/templates/views/edit_view.coffee +19 -8
  38. data/lib/generators/pbw/templates/views/index_view.coffee +10 -2
  39. data/lib/generators/pbw/templates/views/login_view.coffee +20 -8
  40. data/lib/generators/pbw/templates/views/new_view.coffee +13 -5
  41. data/lib/generators/pbw/templates/views/recovery_view.coffee +8 -3
  42. data/lib/generators/pbw/templates/views/show_view.coffee +11 -1
  43. data/lib/generators/pbw/templates/views/signup_view.coffee +19 -5
  44. data/lib/generators/pbw/token/token_generator.rb +4 -0
  45. data/lib/pbw/engine.rb +1 -0
  46. data/lib/pbw/version.rb +1 -1
  47. data/vendor/assets/javascripts/Backbone.CollectionBinder.js +281 -0
  48. data/vendor/assets/javascripts/Backbone.ModelBinder.js +576 -0
  49. metadata +6 -6
  50. data/app/controllers/pbw/item_conversions_controller.rb +0 -7
  51. data/vendor/assets/javascripts/backbone_datalink.js +0 -21
  52. data/vendor/assets/javascripts/backbone_rails_sync.js +0 -81
@@ -9,15 +9,20 @@ class <%= user_view_namespace %>.RecoveryView extends Backbone.View
9
9
  constructor: (options) ->
10
10
  super(options)
11
11
  @model = new <%= js_user_model_namespace %>Recovery
12
-
12
+
13
13
  @model.bind("change:errors", () =>
14
14
  this.render()
15
15
  )
16
16
 
17
17
  @model.bind("error", (model, xhr, options) =>
18
- form_errors 'There was a problem recovering your password', xhr
18
+ display_errors 'There was a problem recovering your password', xhr
19
19
  )
20
20
 
21
+ initialize: ->
22
+ @_modelBinder = new Backbone.ModelBinder
23
+ @bindings =
24
+ email: '[name=email]'
25
+
21
26
  save: (e) ->
22
27
  e.preventDefault()
23
28
  e.stopPropagation()
@@ -33,6 +38,6 @@ class <%= user_view_namespace %>.RecoveryView extends Backbone.View
33
38
  render: ->
34
39
  @$el.html(@template(@model.toJSON() ))
35
40
 
36
- this.$("form").backboneLink(@model)
41
+ @_modelBinder.bind(@model, @el, @bindings)
37
42
 
38
43
  return this
@@ -3,6 +3,16 @@
3
3
  class <%= view_namespace %>.ShowView extends Backbone.View
4
4
  template: JST["<%= jst 'show' %>"]
5
5
 
6
+ initialize: () ->
7
+ @model = @options.model
8
+ @model.bind("error", (model, xhr, options) =>
9
+ display_errors 'There was a problem displaying <%= singular_name %>', xhr, "/<%=plural_model_name%>/#{@model.id}"
10
+ )
11
+
6
12
  render: ->
7
- @$el.html(@template(@model.toJSON() ))
13
+ @model.fetch
14
+ success: (model) =>
15
+ @$el.html(@template(model.toJSON() ))
16
+ error: (model, response) ->
17
+ debug response
8
18
  return this
@@ -9,15 +9,27 @@ class <%= user_view_namespace %>.SignupView extends Backbone.View
9
9
  constructor: (options) ->
10
10
  super(options)
11
11
  @model = new <%= js_user_model_namespace %>Registration
12
-
12
+
13
13
  @model.bind("change:errors", () =>
14
14
  this.render()
15
15
  )
16
16
 
17
17
  @model.bind("error", (model, xhr, options) =>
18
- form_errors 'There was a problem signing up', xhr
18
+ display_errors 'There was a problem signing up', xhr
19
+ )
20
+
21
+ @model.bind("sync", (model, xhr, options) =>
22
+ window.<%=js_app_name%>.User = xhr
19
23
  )
20
24
 
25
+ initialize: ->
26
+ @_modelBinder = new Backbone.ModelBinder
27
+ @bindings =
28
+ name: '[name=name]'
29
+ email: '[name=email]'
30
+ password: '[name=password]'
31
+ password_confirmation: '[name=password_confirmation]'
32
+
21
33
  save: (e) ->
22
34
  e.preventDefault()
23
35
  e.stopPropagation()
@@ -27,13 +39,15 @@ class <%= user_view_namespace %>.SignupView extends Backbone.View
27
39
  @model.save(@model.attributes,
28
40
  success: (user, response, options) =>
29
41
  @model = user
30
- window.<%= js_app_name %>.User = @model
31
- window.location.hash = "/"
42
+ if window.<%=js_app_name%>.backlink
43
+ window.location.hash = window.<%=js_app_name%>.backlink
44
+ else
45
+ window.location.hash = "/"
32
46
  )
33
47
 
34
48
  render: ->
35
49
  @$el.html(@template(@model.toJSON() ))
36
50
 
37
- this.$("form").backboneLink(@model)
51
+ @_modelBinder.bind(@model, @el, @bindings)
38
52
 
39
53
  return this
@@ -10,4 +10,8 @@ class Pbw::TokenGenerator < Pbw::Generators::ScaffoldGenerator
10
10
  def model_namespace
11
11
  "Tokens"
12
12
  end
13
+
14
+ def param_root
15
+ 'token'
16
+ end
13
17
  end
data/lib/pbw/engine.rb CHANGED
@@ -1,6 +1,7 @@
1
1
  require 'pbw/version'
2
2
  require 'mongoid'
3
3
  require 'devise'
4
+ require 'cancan'
4
5
 
5
6
  module Pbw
6
7
  class Engine < ::Rails::Engine
data/lib/pbw/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Pbw
2
- VERSION = "0.0.10"
2
+ VERSION = "0.1.0"
3
3
  end
@@ -0,0 +1,281 @@
1
+ // Backbone.CollectionBinder v1.0.2
2
+ // (c) 2013 Bart Wood
3
+ // Distributed Under MIT License
4
+
5
+ (function(){
6
+
7
+ if(!Backbone){
8
+ throw 'Please include Backbone.js before Backbone.ModelBinder.js';
9
+ }
10
+
11
+ if(!Backbone.ModelBinder){
12
+ throw 'Please include Backbone.ModelBinder.js before Backbone.CollectionBinder.js';
13
+ }
14
+
15
+ Backbone.CollectionBinder = function(elManagerFactory, options){
16
+ _.bindAll.apply(_, [this].concat(_.functions(this)));
17
+ this._elManagers = {};
18
+
19
+ this._elManagerFactory = elManagerFactory;
20
+ if(!this._elManagerFactory) throw 'elManagerFactory must be defined.';
21
+
22
+ // Let the factory just use the trigger function on the view binder
23
+ this._elManagerFactory.trigger = this.trigger;
24
+
25
+ this._options = options || {};
26
+ };
27
+
28
+ Backbone.CollectionBinder.VERSION = '1.0.1';
29
+
30
+ _.extend(Backbone.CollectionBinder.prototype, Backbone.Events, {
31
+ bind: function(collection, parentEl){
32
+ this.unbind();
33
+
34
+ if(!collection) throw 'collection must be defined';
35
+ if(!parentEl) throw 'parentEl must be defined';
36
+
37
+ this._collection = collection;
38
+ this._elManagerFactory.setParentEl(parentEl);
39
+
40
+ this._onCollectionReset();
41
+
42
+ this._collection.on('add', this._onCollectionAdd, this);
43
+ this._collection.on('remove', this._onCollectionRemove, this);
44
+ this._collection.on('reset', this._onCollectionReset, this);
45
+ this._collection.on('sort', this._onCollectionSort, this);
46
+ },
47
+
48
+ unbind: function(){
49
+ if(this._collection !== undefined){
50
+ this._collection.off('add', this._onCollectionAdd);
51
+ this._collection.off('remove', this._onCollectionRemove);
52
+ this._collection.off('reset', this._onCollectionReset);
53
+ this._collection.off('sort', this._onCollectionSort);
54
+ }
55
+
56
+ this._removeAllElManagers();
57
+ },
58
+
59
+ getManagerForEl: function(el){
60
+ var i, elManager, elManagers = _.values(this._elManagers);
61
+
62
+ for(i = 0; i < elManagers.length; i++){
63
+ elManager = elManagers[i];
64
+
65
+ if(elManager.isElContained(el)){
66
+ return elManager;
67
+ }
68
+ }
69
+
70
+ return undefined;
71
+ },
72
+
73
+ getManagerForModel: function(model){
74
+ return this._elManagers[_.isObject(model)? model.cid : model];
75
+ },
76
+
77
+ _onCollectionAdd: function(model){
78
+ this._elManagers[model.cid] = this._elManagerFactory.makeElManager(model);
79
+ this._elManagers[model.cid].createEl();
80
+
81
+ if(this._options['autoSort']){
82
+ this.sortRootEls();
83
+ }
84
+ },
85
+
86
+ _onCollectionRemove: function(model){
87
+ this._removeElManager(model);
88
+ },
89
+
90
+ _onCollectionReset: function(){
91
+ this._removeAllElManagers();
92
+
93
+ this._collection.each(function(model){
94
+ this._onCollectionAdd(model);
95
+ }, this);
96
+
97
+ this.trigger('elsReset', this._collection);
98
+ },
99
+
100
+ _onCollectionSort: function() {
101
+ if(this._options['autoSort']){
102
+ this.sortRootEls();
103
+ }
104
+ },
105
+
106
+ _removeAllElManagers: function(){
107
+ _.each(this._elManagers, function(elManager){
108
+ elManager.removeEl();
109
+ delete this._elManagers[elManager._model.cid];
110
+ }, this);
111
+
112
+ delete this._elManagers;
113
+ this._elManagers = {};
114
+ },
115
+
116
+ _removeElManager: function(model){
117
+ if(this._elManagers[model.cid] !== undefined){
118
+ this._elManagers[model.cid].removeEl();
119
+ delete this._elManagers[model.cid];
120
+ }
121
+ },
122
+
123
+ sortRootEls: function(){
124
+ this._collection.each(function(model, modelIndex){
125
+ var modelElManager = this.getManagerForModel(model);
126
+ if(modelElManager){
127
+ var modelEl = modelElManager.getEl();
128
+ var currentRootEls = $(this._elManagerFactory.getParentEl()).children();
129
+
130
+ if(currentRootEls[modelIndex] !== modelEl[0]){
131
+ modelEl.detach();
132
+ modelEl.insertBefore(currentRootEls[modelIndex]);
133
+ }
134
+ }
135
+ }, this);
136
+ }
137
+ });
138
+
139
+ // The ElManagerFactory is used for els that are just html templates
140
+ // elHtml - how the model's html will be rendered. Must have a single root element (div,span).
141
+ // bindings (optional) - either a string which is the binding attribute (name, id, data-name, etc.) or a normal bindings hash
142
+ Backbone.CollectionBinder.ElManagerFactory = function(elHtml, bindings){
143
+ _.bindAll.apply(_, [this].concat(_.functions(this)));
144
+
145
+ this._elHtml = elHtml;
146
+ this._bindings = bindings;
147
+
148
+ if(! _.isString(this._elHtml)) throw 'elHtml must be a valid html string';
149
+ };
150
+
151
+ _.extend(Backbone.CollectionBinder.ElManagerFactory.prototype, {
152
+ setParentEl: function(parentEl){
153
+ this._parentEl = parentEl;
154
+ },
155
+
156
+ getParentEl: function(){
157
+ return this._parentEl;
158
+ },
159
+
160
+ makeElManager: function(model){
161
+
162
+ var elManager = {
163
+ _model: model,
164
+
165
+ createEl: function(){
166
+
167
+ this._el = $(this._elHtml);
168
+ $(this._parentEl).append(this._el);
169
+
170
+ if(this._bindings){
171
+ if(_.isString(this._bindings)){
172
+ this._modelBinder = new Backbone.ModelBinder();
173
+ this._modelBinder.bind(this._model, this._el, Backbone.ModelBinder.createDefaultBindings(this._el, this._bindings));
174
+ }
175
+ else if(_.isObject(this._bindings)){
176
+ this._modelBinder = new Backbone.ModelBinder();
177
+ this._modelBinder.bind(this._model, this._el, this._bindings);
178
+ }
179
+ else {
180
+ throw 'Unsupported bindings type, please use a boolean or a bindings hash';
181
+ }
182
+ }
183
+
184
+ this.trigger('elCreated', this._model, this._el);
185
+ },
186
+
187
+ removeEl: function(){
188
+ if(this._modelBinder !== undefined){
189
+ this._modelBinder.unbind();
190
+ }
191
+
192
+ this._el.remove();
193
+ this.trigger('elRemoved', this._model, this._el);
194
+ },
195
+
196
+ isElContained: function(findEl){
197
+ return this._el === findEl || $(this._el).has(findEl).length > 0;
198
+ },
199
+
200
+ getModel: function(){
201
+ return this._model;
202
+ },
203
+
204
+ getEl: function(){
205
+ return this._el;
206
+ }
207
+ };
208
+
209
+ _.extend(elManager, this);
210
+ return elManager;
211
+ }
212
+ });
213
+
214
+
215
+ // The ViewManagerFactory is used for els that are created and owned by backbone views.
216
+ // There is no bindings option because the view made by the viewCreator should take care of any binding
217
+ // viewCreator - a callback that will create backbone view instances for a model passed to the callback
218
+ Backbone.CollectionBinder.ViewManagerFactory = function(viewCreator){
219
+ _.bindAll.apply(_, [this].concat(_.functions(this)));
220
+ this._viewCreator = viewCreator;
221
+
222
+ if(!_.isFunction(this._viewCreator)) throw 'viewCreator must be a valid function that accepts a model and returns a backbone view';
223
+ };
224
+
225
+ _.extend(Backbone.CollectionBinder.ViewManagerFactory.prototype, {
226
+ setParentEl: function(parentEl){
227
+ this._parentEl = parentEl;
228
+ },
229
+
230
+ getParentEl: function(){
231
+ return this._parentEl;
232
+ },
233
+
234
+ makeElManager: function(model){
235
+ var elManager = {
236
+
237
+ _model: model,
238
+
239
+ createEl: function(){
240
+ this._view = this._viewCreator(model);
241
+ $(this._parentEl).append(this._view.render(this._model).el);
242
+
243
+ this.trigger('elCreated', this._model, this._view);
244
+ },
245
+
246
+ removeEl: function(){
247
+ if(this._view.close !== undefined){
248
+ this._view.close();
249
+ }
250
+ else {
251
+ this._view.$el.remove();
252
+ console.log('warning, you should implement a close() function for your view, you might end up with zombies');
253
+ }
254
+
255
+ this.trigger('elRemoved', this._model, this._view);
256
+ },
257
+
258
+ isElContained: function(findEl){
259
+ return this._view.el === findEl || this._view.$el.has(findEl).length > 0;
260
+ },
261
+
262
+ getModel: function(){
263
+ return this._model;
264
+ },
265
+
266
+ getView: function(){
267
+ return this._view;
268
+ },
269
+
270
+ getEl: function(){
271
+ return this._view.$el;
272
+ }
273
+ };
274
+
275
+ _.extend(elManager, this);
276
+
277
+ return elManager;
278
+ }
279
+ });
280
+
281
+ }).call(this);
@@ -0,0 +1,576 @@
1
+ // Backbone.ModelBinder v1.0.2
2
+ // (c) 2013 Bart Wood
3
+ // Distributed Under MIT License
4
+
5
+ (function (factory) {
6
+ if (typeof define === 'function' && define.amd) {
7
+ // AMD. Register as an anonymous module.
8
+ define(['underscore', 'jquery', 'backbone'], factory);
9
+ } else {
10
+ // Browser globals
11
+ factory(_, $, Backbone);
12
+ }
13
+ }(function(_, $, Backbone){
14
+
15
+ if(!Backbone){
16
+ throw 'Please include Backbone.js before Backbone.ModelBinder.js';
17
+ }
18
+
19
+ Backbone.ModelBinder = function(){
20
+ _.bindAll.apply(_, [this].concat(_.functions(this)));
21
+ };
22
+
23
+ // Static setter for class level options
24
+ Backbone.ModelBinder.SetOptions = function(options){
25
+ Backbone.ModelBinder.options = options;
26
+ };
27
+
28
+ // Current version of the library.
29
+ Backbone.ModelBinder.VERSION = '1.0.2';
30
+ Backbone.ModelBinder.Constants = {};
31
+ Backbone.ModelBinder.Constants.ModelToView = 'ModelToView';
32
+ Backbone.ModelBinder.Constants.ViewToModel = 'ViewToModel';
33
+
34
+ _.extend(Backbone.ModelBinder.prototype, {
35
+
36
+ bind:function (model, rootEl, attributeBindings, options) {
37
+ this.unbind();
38
+
39
+ this._model = model;
40
+ this._rootEl = rootEl;
41
+ this._setOptions(options);
42
+
43
+ if (!this._model) this._throwException('model must be specified');
44
+ if (!this._rootEl) this._throwException('rootEl must be specified');
45
+
46
+ if(attributeBindings){
47
+ // Create a deep clone of the attribute bindings
48
+ this._attributeBindings = $.extend(true, {}, attributeBindings);
49
+
50
+ this._initializeAttributeBindings();
51
+ this._initializeElBindings();
52
+ }
53
+ else {
54
+ this._initializeDefaultBindings();
55
+ }
56
+
57
+ this._bindModelToView();
58
+ this._bindViewToModel();
59
+ },
60
+
61
+ bindCustomTriggers: function (model, rootEl, triggers, attributeBindings, modelSetOptions) {
62
+ this._triggers = triggers;
63
+ this.bind(model, rootEl, attributeBindings, modelSetOptions)
64
+ },
65
+
66
+ unbind:function () {
67
+ this._unbindModelToView();
68
+ this._unbindViewToModel();
69
+
70
+ if(this._attributeBindings){
71
+ delete this._attributeBindings;
72
+ this._attributeBindings = undefined;
73
+ }
74
+ },
75
+
76
+ _setOptions: function(options){
77
+ this._options = _.extend({
78
+ boundAttribute: 'name'
79
+ }, Backbone.ModelBinder.options, options);
80
+
81
+ // initialize default options
82
+ if(!this._options['modelSetOptions']){
83
+ this._options['modelSetOptions'] = {};
84
+ }
85
+ this._options['modelSetOptions'].changeSource = 'ModelBinder';
86
+
87
+ if(!this._options['changeTriggers']){
88
+ this._options['changeTriggers'] = {'': 'change', '[contenteditable]': 'blur'};
89
+ }
90
+
91
+ if(!this._options['initialCopyDirection']){
92
+ this._options['initialCopyDirection'] = Backbone.ModelBinder.Constants.ModelToView;
93
+ }
94
+ },
95
+
96
+ // Converts the input bindings, which might just be empty or strings, to binding objects
97
+ _initializeAttributeBindings:function () {
98
+ var attributeBindingKey, inputBinding, attributeBinding, elementBindingCount, elementBinding;
99
+
100
+ for (attributeBindingKey in this._attributeBindings) {
101
+ inputBinding = this._attributeBindings[attributeBindingKey];
102
+
103
+ if (_.isString(inputBinding)) {
104
+ attributeBinding = {elementBindings: [{selector: inputBinding}]};
105
+ }
106
+ else if (_.isArray(inputBinding)) {
107
+ attributeBinding = {elementBindings: inputBinding};
108
+ }
109
+ else if(_.isObject(inputBinding)){
110
+ attributeBinding = {elementBindings: [inputBinding]};
111
+ }
112
+ else {
113
+ this._throwException('Unsupported type passed to Model Binder ' + attributeBinding);
114
+ }
115
+
116
+ // Add a linkage from the element binding back to the attribute binding
117
+ for(elementBindingCount = 0; elementBindingCount < attributeBinding.elementBindings.length; elementBindingCount++){
118
+ elementBinding = attributeBinding.elementBindings[elementBindingCount];
119
+ elementBinding.attributeBinding = attributeBinding;
120
+ }
121
+
122
+ attributeBinding.attributeName = attributeBindingKey;
123
+ this._attributeBindings[attributeBindingKey] = attributeBinding;
124
+ }
125
+ },
126
+
127
+ // If the bindings are not specified, the default binding is performed on the specified attribute, name by default
128
+ _initializeDefaultBindings: function(){
129
+ var elCount, elsWithAttribute, matchedEl, name, attributeBinding;
130
+
131
+ this._attributeBindings = {};
132
+ elsWithAttribute = $('[' + this._options['boundAttribute'] + ']', this._rootEl);
133
+
134
+ for(elCount = 0; elCount < elsWithAttribute.length; elCount++){
135
+ matchedEl = elsWithAttribute[elCount];
136
+ name = $(matchedEl).attr(this._options['boundAttribute']);
137
+
138
+ // For elements like radio buttons we only want a single attribute binding with possibly multiple element bindings
139
+ if(!this._attributeBindings[name]){
140
+ attributeBinding = {attributeName: name};
141
+ attributeBinding.elementBindings = [{attributeBinding: attributeBinding, boundEls: [matchedEl]}];
142
+ this._attributeBindings[name] = attributeBinding;
143
+ }
144
+ else{
145
+ this._attributeBindings[name].elementBindings.push({attributeBinding: this._attributeBindings[name], boundEls: [matchedEl]});
146
+ }
147
+ }
148
+ },
149
+
150
+ _initializeElBindings:function () {
151
+ var bindingKey, attributeBinding, bindingCount, elementBinding, foundEls, elCount, el;
152
+ for (bindingKey in this._attributeBindings) {
153
+ attributeBinding = this._attributeBindings[bindingKey];
154
+
155
+ for (bindingCount = 0; bindingCount < attributeBinding.elementBindings.length; bindingCount++) {
156
+ elementBinding = attributeBinding.elementBindings[bindingCount];
157
+ if (elementBinding.selector === '') {
158
+ foundEls = $(this._rootEl);
159
+ }
160
+ else {
161
+ foundEls = $(elementBinding.selector, this._rootEl);
162
+ }
163
+
164
+ if (foundEls.length === 0) {
165
+ this._throwException('Bad binding found. No elements returned for binding selector ' + elementBinding.selector);
166
+ }
167
+ else {
168
+ elementBinding.boundEls = [];
169
+ for (elCount = 0; elCount < foundEls.length; elCount++) {
170
+ el = foundEls[elCount];
171
+ elementBinding.boundEls.push(el);
172
+ }
173
+ }
174
+ }
175
+ }
176
+ },
177
+
178
+ _bindModelToView: function () {
179
+ this._model.on('change', this._onModelChange, this);
180
+
181
+ if(this._options['initialCopyDirection'] === Backbone.ModelBinder.Constants.ModelToView){
182
+ this.copyModelAttributesToView();
183
+ }
184
+ },
185
+
186
+ // attributesToCopy is an optional parameter - if empty, all attributes
187
+ // that are bound will be copied. Otherwise, only attributeBindings specified
188
+ // in the attributesToCopy are copied.
189
+ copyModelAttributesToView: function(attributesToCopy){
190
+ var attributeName, attributeBinding;
191
+
192
+ for (attributeName in this._attributeBindings) {
193
+ if(attributesToCopy === undefined || _.indexOf(attributesToCopy, attributeName) !== -1){
194
+ attributeBinding = this._attributeBindings[attributeName];
195
+ this._copyModelToView(attributeBinding);
196
+ }
197
+ }
198
+ },
199
+
200
+ copyViewValuesToModel: function(){
201
+ var bindingKey, attributeBinding, bindingCount, elementBinding, elCount, el;
202
+ for (bindingKey in this._attributeBindings) {
203
+ attributeBinding = this._attributeBindings[bindingKey];
204
+
205
+ for (bindingCount = 0; bindingCount < attributeBinding.elementBindings.length; bindingCount++) {
206
+ elementBinding = attributeBinding.elementBindings[bindingCount];
207
+
208
+ if(this._isBindingUserEditable(elementBinding)){
209
+ if(this._isBindingRadioGroup(elementBinding)){
210
+ el = this._getRadioButtonGroupCheckedEl(elementBinding);
211
+ if(el){
212
+ this._copyViewToModel(elementBinding, el);
213
+ }
214
+ }
215
+ else {
216
+ for(elCount = 0; elCount < elementBinding.boundEls.length; elCount++){
217
+ el = $(elementBinding.boundEls[elCount]);
218
+ if(this._isElUserEditable(el)){
219
+ this._copyViewToModel(elementBinding, el);
220
+ }
221
+ }
222
+ }
223
+ }
224
+ }
225
+ }
226
+ },
227
+
228
+ _unbindModelToView: function(){
229
+ if(this._model){
230
+ this._model.off('change', this._onModelChange);
231
+ this._model = undefined;
232
+ }
233
+ },
234
+
235
+ _bindViewToModel: function () {
236
+ _.each(this._options['changeTriggers'], function (event, selector) {
237
+ $(this._rootEl).delegate(selector, event, this._onElChanged);
238
+ }, this);
239
+
240
+ if(this._options['initialCopyDirection'] === Backbone.ModelBinder.Constants.ViewToModel){
241
+ this.copyViewValuesToModel();
242
+ }
243
+ },
244
+
245
+ _unbindViewToModel: function () {
246
+ if(this._options && this._options['changeTriggers']){
247
+ _.each(this._options['changeTriggers'], function (event, selector) {
248
+ $(this._rootEl).undelegate(selector, event, this._onElChanged);
249
+ }, this);
250
+ }
251
+ },
252
+
253
+ _onElChanged:function (event) {
254
+ var el, elBindings, elBindingCount, elBinding;
255
+
256
+ el = $(event.target)[0];
257
+ elBindings = this._getElBindings(el);
258
+
259
+ for(elBindingCount = 0; elBindingCount < elBindings.length; elBindingCount++){
260
+ elBinding = elBindings[elBindingCount];
261
+ if (this._isBindingUserEditable(elBinding)) {
262
+ this._copyViewToModel(elBinding, el);
263
+ }
264
+ }
265
+ },
266
+
267
+ _isBindingUserEditable: function(elBinding){
268
+ return elBinding.elAttribute === undefined ||
269
+ elBinding.elAttribute === 'text' ||
270
+ elBinding.elAttribute === 'html';
271
+ },
272
+
273
+ _isElUserEditable: function(el){
274
+ var isContentEditable = el.attr('contenteditable');
275
+ return isContentEditable || el.is('input') || el.is('select') || el.is('textarea');
276
+ },
277
+
278
+ _isBindingRadioGroup: function(elBinding){
279
+ var elCount, el;
280
+ var isAllRadioButtons = elBinding.boundEls.length > 0;
281
+ for(elCount = 0; elCount < elBinding.boundEls.length; elCount++){
282
+ el = $(elBinding.boundEls[elCount]);
283
+ if(el.attr('type') !== 'radio'){
284
+ isAllRadioButtons = false;
285
+ break;
286
+ }
287
+ }
288
+
289
+ return isAllRadioButtons;
290
+ },
291
+
292
+ _getRadioButtonGroupCheckedEl: function(elBinding){
293
+ var elCount, el;
294
+ for(elCount = 0; elCount < elBinding.boundEls.length; elCount++){
295
+ el = $(elBinding.boundEls[elCount]);
296
+ if(el.attr('type') === 'radio' && el.attr('checked')){
297
+ return el;
298
+ }
299
+ }
300
+
301
+ return undefined;
302
+ },
303
+
304
+ _getElBindings:function (findEl) {
305
+ var attributeName, attributeBinding, elementBindingCount, elementBinding, boundElCount, boundEl;
306
+ var elBindings = [];
307
+
308
+ for (attributeName in this._attributeBindings) {
309
+ attributeBinding = this._attributeBindings[attributeName];
310
+
311
+ for (elementBindingCount = 0; elementBindingCount < attributeBinding.elementBindings.length; elementBindingCount++) {
312
+ elementBinding = attributeBinding.elementBindings[elementBindingCount];
313
+
314
+ for (boundElCount = 0; boundElCount < elementBinding.boundEls.length; boundElCount++) {
315
+ boundEl = elementBinding.boundEls[boundElCount];
316
+
317
+ if (boundEl === findEl) {
318
+ elBindings.push(elementBinding);
319
+ }
320
+ }
321
+ }
322
+ }
323
+
324
+ return elBindings;
325
+ },
326
+
327
+ _onModelChange:function () {
328
+ var changedAttribute, attributeBinding;
329
+
330
+ for (changedAttribute in this._model.changedAttributes()) {
331
+ attributeBinding = this._attributeBindings[changedAttribute];
332
+
333
+ if (attributeBinding) {
334
+ this._copyModelToView(attributeBinding);
335
+ }
336
+ }
337
+ },
338
+
339
+ _copyModelToView:function (attributeBinding) {
340
+ var elementBindingCount, elementBinding, boundElCount, boundEl, value, convertedValue;
341
+
342
+ value = this._model.get(attributeBinding.attributeName);
343
+
344
+ for (elementBindingCount = 0; elementBindingCount < attributeBinding.elementBindings.length; elementBindingCount++) {
345
+ elementBinding = attributeBinding.elementBindings[elementBindingCount];
346
+
347
+ for (boundElCount = 0; boundElCount < elementBinding.boundEls.length; boundElCount++) {
348
+ boundEl = elementBinding.boundEls[boundElCount];
349
+
350
+ if(!boundEl._isSetting){
351
+ convertedValue = this._getConvertedValue(Backbone.ModelBinder.Constants.ModelToView, elementBinding, value);
352
+ this._setEl($(boundEl), elementBinding, convertedValue);
353
+ }
354
+ }
355
+ }
356
+ },
357
+
358
+ _setEl: function (el, elementBinding, convertedValue) {
359
+ if (elementBinding.elAttribute) {
360
+ this._setElAttribute(el, elementBinding, convertedValue);
361
+ }
362
+ else {
363
+ this._setElValue(el, convertedValue);
364
+ }
365
+ },
366
+
367
+ _setElAttribute:function (el, elementBinding, convertedValue) {
368
+ switch (elementBinding.elAttribute) {
369
+ case 'html':
370
+ el.html(convertedValue);
371
+ break;
372
+ case 'text':
373
+ el.text(convertedValue);
374
+ break;
375
+ case 'enabled':
376
+ el.prop('disabled', !convertedValue);
377
+ break;
378
+ case 'displayed':
379
+ el[convertedValue ? 'show' : 'hide']();
380
+ break;
381
+ case 'hidden':
382
+ el[convertedValue ? 'hide' : 'show']();
383
+ break;
384
+ case 'css':
385
+ el.css(elementBinding.cssAttribute, convertedValue);
386
+ break;
387
+ case 'class':
388
+ var previousValue = this._model.previous(elementBinding.attributeBinding.attributeName);
389
+ var currentValue = this._model.get(elementBinding.attributeBinding.attributeName);
390
+ // is current value is now defined then remove the class the may have been set for the undefined value
391
+ if(!_.isUndefined(previousValue) || !_.isUndefined(currentValue)){
392
+ previousValue = this._getConvertedValue(Backbone.ModelBinder.Constants.ModelToView, elementBinding, previousValue);
393
+ el.removeClass(previousValue);
394
+ }
395
+
396
+ if(convertedValue){
397
+ el.addClass(convertedValue);
398
+ }
399
+ break;
400
+ default:
401
+ el.attr(elementBinding.elAttribute, convertedValue);
402
+ }
403
+ },
404
+
405
+ _setElValue:function (el, convertedValue) {
406
+ if(el.attr('type')){
407
+ switch (el.attr('type')) {
408
+ case 'radio':
409
+ if (el.val() === convertedValue) {
410
+ // must defer the change trigger or the change will actually fire with the old value
411
+ el.prop('checked') || _.defer(function() { el.trigger('change'); });
412
+ el.prop('checked', true);
413
+ }
414
+ else {
415
+ // must defer the change trigger or the change will actually fire with the old value
416
+ el.prop('checked', false);
417
+ }
418
+ break;
419
+ case 'checkbox':
420
+ // must defer the change trigger or the change will actually fire with the old value
421
+ el.prop('checked') === !!convertedValue || _.defer(function() { el.trigger('change') });
422
+ el.prop('checked', !!convertedValue);
423
+ break;
424
+ case 'file':
425
+ break;
426
+ default:
427
+ el.val(convertedValue);
428
+ }
429
+ }
430
+ else if(el.is('input') || el.is('select') || el.is('textarea')){
431
+ el.val(convertedValue || (convertedValue === 0 ? '0' : ''));
432
+ }
433
+ else {
434
+ el.text(convertedValue || (convertedValue === 0 ? '0' : ''));
435
+ }
436
+ },
437
+
438
+ _copyViewToModel: function (elementBinding, el) {
439
+ var result, value, convertedValue;
440
+
441
+ if (!el._isSetting) {
442
+
443
+ el._isSetting = true;
444
+ result = this._setModel(elementBinding, $(el));
445
+ el._isSetting = false;
446
+
447
+ if(result && elementBinding.converter){
448
+ value = this._model.get(elementBinding.attributeBinding.attributeName);
449
+ convertedValue = this._getConvertedValue(Backbone.ModelBinder.Constants.ModelToView, elementBinding, value);
450
+ this._setEl($(el), elementBinding, convertedValue);
451
+ }
452
+ }
453
+ },
454
+
455
+ _getElValue: function(elementBinding, el){
456
+ switch (el.attr('type')) {
457
+ case 'checkbox':
458
+ return el.prop('checked') ? true : false;
459
+ default:
460
+ if(el.attr('contenteditable') !== undefined){
461
+ return el.html();
462
+ }
463
+ else {
464
+ return el.val();
465
+ }
466
+ }
467
+ },
468
+
469
+ _setModel: function (elementBinding, el) {
470
+ var data = {};
471
+ var elVal = this._getElValue(elementBinding, el);
472
+ elVal = this._getConvertedValue(Backbone.ModelBinder.Constants.ViewToModel, elementBinding, elVal);
473
+ data[elementBinding.attributeBinding.attributeName] = elVal;
474
+ return this._model.set(data, this._options['modelSetOptions']);
475
+ },
476
+
477
+ _getConvertedValue: function (direction, elementBinding, value) {
478
+ if (elementBinding.converter) {
479
+ value = elementBinding.converter(direction, value, elementBinding.attributeBinding.attributeName, this._model, elementBinding.boundEls);
480
+ }
481
+
482
+ return value;
483
+ },
484
+
485
+ _throwException: function(message){
486
+ if(this._options.suppressThrows){
487
+ if(console && console.error){
488
+ console.error(message);
489
+ }
490
+ }
491
+ else {
492
+ throw message;
493
+ }
494
+ }
495
+ });
496
+
497
+ Backbone.ModelBinder.CollectionConverter = function(collection){
498
+ this._collection = collection;
499
+
500
+ if(!this._collection){
501
+ throw 'Collection must be defined';
502
+ }
503
+ _.bindAll(this, 'convert');
504
+ };
505
+
506
+ _.extend(Backbone.ModelBinder.CollectionConverter.prototype, {
507
+ convert: function(direction, value){
508
+ if (direction === Backbone.ModelBinder.Constants.ModelToView) {
509
+ return value ? value.id : undefined;
510
+ }
511
+ else {
512
+ return this._collection.get(value);
513
+ }
514
+ }
515
+ });
516
+
517
+ // A static helper function to create a default set of bindings that you can customize before calling the bind() function
518
+ // rootEl - where to find all of the bound elements
519
+ // attributeType - probably 'name' or 'id' in most cases
520
+ // converter(optional) - the default converter you want applied to all your bindings
521
+ // elAttribute(optional) - the default elAttribute you want applied to all your bindings
522
+ Backbone.ModelBinder.createDefaultBindings = function(rootEl, attributeType, converter, elAttribute){
523
+ var foundEls, elCount, foundEl, attributeName;
524
+ var bindings = {};
525
+
526
+ foundEls = $('[' + attributeType + ']', rootEl);
527
+
528
+ for(elCount = 0; elCount < foundEls.length; elCount++){
529
+ foundEl = foundEls[elCount];
530
+ attributeName = $(foundEl).attr(attributeType);
531
+
532
+ if(!bindings[attributeName]){
533
+ var attributeBinding = {selector: '[' + attributeType + '="' + attributeName + '"]'};
534
+ bindings[attributeName] = attributeBinding;
535
+
536
+ if(converter){
537
+ bindings[attributeName].converter = converter;
538
+ }
539
+
540
+ if(elAttribute){
541
+ bindings[attributeName].elAttribute = elAttribute;
542
+ }
543
+ }
544
+ }
545
+
546
+ return bindings;
547
+ };
548
+
549
+ // Helps you to combine 2 sets of bindings
550
+ Backbone.ModelBinder.combineBindings = function(destination, source){
551
+ _.each(source, function(value, key){
552
+ var elementBinding = {selector: value.selector};
553
+
554
+ if(value.converter){
555
+ elementBinding.converter = value.converter;
556
+ }
557
+
558
+ if(value.elAttribute){
559
+ elementBinding.elAttribute = value.elAttribute;
560
+ }
561
+
562
+ if(!destination[key]){
563
+ destination[key] = elementBinding;
564
+ }
565
+ else {
566
+ destination[key] = [destination[key], elementBinding];
567
+ }
568
+ });
569
+
570
+ return destination;
571
+ };
572
+
573
+
574
+ return Backbone.ModelBinder;
575
+
576
+ }));