pbw 0.0.10 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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
+ }));