rails-backbone-forms 0.0.1
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.
- data/.gitignore +4 -0
- data/Gemfile +4 -0
- data/README.md +24 -0
- data/Rakefile +1 -0
- data/lib/rails/backbone/forms/engine.rb +9 -0
- data/lib/rails/backbone/forms/version.rb +7 -0
- data/lib/rails-backbone-forms.rb +9 -0
- data/rails-backbone-forms.gemspec +20 -0
- data/vendor/assets/javascripts/rails-backbone-forms/adapters/backbone.bootstrap-modal.js +220 -0
- data/vendor/assets/javascripts/rails-backbone-forms/backbone-forms.js +2309 -0
- data/vendor/assets/javascripts/rails-backbone-forms/editors/jquery-ui.js +500 -0
- data/vendor/assets/javascripts/rails-backbone-forms/editors/list.js +575 -0
- data/vendor/assets/javascripts/rails-backbone-forms/jquery-ui-editors.js +405 -0
- data/vendor/assets/javascripts/rails-backbone-forms/templates/bootstrap.js +92 -0
- data/vendor/assets/javascripts/rails-backbone-forms/templates/default.js +89 -0
- data/vendor/assets/javascripts/rails-backbone-forms.js +2 -0
- data/vendor/assets/stylesheets/rails-backbone-forms/editors/jquery-ui.css +65 -0
- data/vendor/assets/stylesheets/rails-backbone-forms/templates/bootstrap.css +43 -0
- data/vendor/assets/stylesheets/rails-backbone-forms/templates/default.css +134 -0
- data/vendor/assets/stylesheets/rails-backbone-forms.css +3 -0
- metadata +66 -0
@@ -0,0 +1,575 @@
|
|
1
|
+
;(function() {
|
2
|
+
|
3
|
+
var Form = Backbone.Form,
|
4
|
+
editors = Form.editors;
|
5
|
+
|
6
|
+
/**
|
7
|
+
* LIST
|
8
|
+
*
|
9
|
+
* An array editor. Creates a list of other editor items.
|
10
|
+
*
|
11
|
+
* Special options:
|
12
|
+
* @param {String} [options.schema.itemType] The editor type for each item in the list. Default: 'Text'
|
13
|
+
* @param {String} [options.schema.confirmDelete] Text to display in a delete confirmation dialog. If falsey, will not ask for confirmation.
|
14
|
+
*/
|
15
|
+
editors.List = editors.Base.extend({
|
16
|
+
|
17
|
+
events: {
|
18
|
+
'click [data-action="add"]': function(event) {
|
19
|
+
event.preventDefault();
|
20
|
+
this.addItem(null, true);
|
21
|
+
}
|
22
|
+
},
|
23
|
+
|
24
|
+
initialize: function(options) {
|
25
|
+
editors.Base.prototype.initialize.call(this, options);
|
26
|
+
|
27
|
+
var schema = this.schema;
|
28
|
+
if (!schema) throw "Missing required option 'schema'";
|
29
|
+
|
30
|
+
//List schema defaults
|
31
|
+
this.schema = _.extend({
|
32
|
+
listTemplate: 'list',
|
33
|
+
listItemTemplate: 'listItem'
|
34
|
+
}, schema);
|
35
|
+
|
36
|
+
//Determine the editor to use
|
37
|
+
this.Editor = (function() {
|
38
|
+
var type = schema.itemType;
|
39
|
+
|
40
|
+
//Default to Text
|
41
|
+
if (!type) return editors.Text;
|
42
|
+
|
43
|
+
//Use List-specific version if available
|
44
|
+
if (editors.List[type]) return editors.List[type];
|
45
|
+
|
46
|
+
//Or whichever was passed
|
47
|
+
return editors[type];
|
48
|
+
})();
|
49
|
+
|
50
|
+
this.items = [];
|
51
|
+
},
|
52
|
+
|
53
|
+
render: function() {
|
54
|
+
var self = this,
|
55
|
+
value = this.value || [];
|
56
|
+
|
57
|
+
//Create main element
|
58
|
+
var $el = $(Form.templates[this.schema.listTemplate]({
|
59
|
+
items: '<b class="bbf-tmp"></b>'
|
60
|
+
}));
|
61
|
+
|
62
|
+
//Store a reference to the list (item container)
|
63
|
+
this.$list = $el.find('.bbf-tmp').parent().empty();
|
64
|
+
|
65
|
+
//Add existing items
|
66
|
+
if (value.length) {
|
67
|
+
_.each(value, function(itemValue) {
|
68
|
+
self.addItem(itemValue);
|
69
|
+
});
|
70
|
+
}
|
71
|
+
|
72
|
+
//If no existing items create an empty one, unless the editor specifies otherwise
|
73
|
+
else {
|
74
|
+
if (!this.Editor.isAsync) this.addItem();
|
75
|
+
}
|
76
|
+
|
77
|
+
this.setElement($el);
|
78
|
+
this.$el.attr('id', this.id);
|
79
|
+
this.$el.attr('name', this.key);
|
80
|
+
|
81
|
+
if (this.hasFocus) this.trigger('blur', this);
|
82
|
+
|
83
|
+
return this;
|
84
|
+
},
|
85
|
+
|
86
|
+
/**
|
87
|
+
* Add a new item to the list
|
88
|
+
* @param {Mixed} [value] Value for the new item editor
|
89
|
+
* @param {Boolean} [userInitiated] If the item was added by the user clicking 'add'
|
90
|
+
*/
|
91
|
+
addItem: function(value, userInitiated) {
|
92
|
+
var self = this;
|
93
|
+
|
94
|
+
//Create the item
|
95
|
+
var item = new editors.List.Item({
|
96
|
+
list: this,
|
97
|
+
schema: this.schema,
|
98
|
+
value: value,
|
99
|
+
Editor: this.Editor,
|
100
|
+
key: this.key
|
101
|
+
}).render();
|
102
|
+
|
103
|
+
var _addItem = function() {
|
104
|
+
self.items.push(item);
|
105
|
+
self.$list.append(item.el);
|
106
|
+
|
107
|
+
item.editor.on('all', function(event) {
|
108
|
+
if (event == 'change') return;
|
109
|
+
|
110
|
+
// args = ["key:change", itemEditor, fieldEditor]
|
111
|
+
args = _.toArray(arguments);
|
112
|
+
args[0] = 'item:' + event;
|
113
|
+
args.splice(1, 0, self);
|
114
|
+
// args = ["item:key:change", this=listEditor, itemEditor, fieldEditor]
|
115
|
+
|
116
|
+
editors.List.prototype.trigger.apply(this, args);
|
117
|
+
}, self);
|
118
|
+
|
119
|
+
item.editor.on('change', function() {
|
120
|
+
if (!item.addEventTriggered) {
|
121
|
+
item.addEventTriggered = true;
|
122
|
+
this.trigger('add', this, item.editor);
|
123
|
+
}
|
124
|
+
this.trigger('item:change', this, item.editor);
|
125
|
+
this.trigger('change', this);
|
126
|
+
}, self);
|
127
|
+
|
128
|
+
item.editor.on('focus', function() {
|
129
|
+
if (this.hasFocus) return;
|
130
|
+
this.trigger('focus', this);
|
131
|
+
}, self);
|
132
|
+
item.editor.on('blur', function() {
|
133
|
+
if (!this.hasFocus) return;
|
134
|
+
var self = this;
|
135
|
+
setTimeout(function() {
|
136
|
+
if (_.find(self.items, function(item) { return item.editor.hasFocus; })) return;
|
137
|
+
self.trigger('blur', self);
|
138
|
+
}, 0);
|
139
|
+
}, self);
|
140
|
+
|
141
|
+
if (userInitiated || value) {
|
142
|
+
item.addEventTriggered = true;
|
143
|
+
}
|
144
|
+
|
145
|
+
if (userInitiated) {
|
146
|
+
self.trigger('add', self, item.editor);
|
147
|
+
self.trigger('change', self);
|
148
|
+
}
|
149
|
+
};
|
150
|
+
|
151
|
+
//Check if we need to wait for the item to complete before adding to the list
|
152
|
+
if (this.Editor.isAsync) {
|
153
|
+
item.editor.on('readyToAdd', _addItem, this);
|
154
|
+
}
|
155
|
+
|
156
|
+
//Most editors can be added automatically
|
157
|
+
else {
|
158
|
+
_addItem();
|
159
|
+
}
|
160
|
+
|
161
|
+
return item;
|
162
|
+
},
|
163
|
+
|
164
|
+
/**
|
165
|
+
* Remove an item from the list
|
166
|
+
* @param {List.Item} item
|
167
|
+
*/
|
168
|
+
removeItem: function(item) {
|
169
|
+
//Confirm delete
|
170
|
+
var confirmMsg = this.schema.confirmDelete;
|
171
|
+
if (confirmMsg && !confirm(confirmMsg)) return;
|
172
|
+
|
173
|
+
var index = _.indexOf(this.items, item);
|
174
|
+
|
175
|
+
this.items[index].remove();
|
176
|
+
this.items.splice(index, 1);
|
177
|
+
|
178
|
+
if (item.addEventTriggered) {
|
179
|
+
this.trigger('remove', this, item.editor);
|
180
|
+
this.trigger('change', this);
|
181
|
+
}
|
182
|
+
|
183
|
+
if (!this.items.length && !this.Editor.isAsync) this.addItem();
|
184
|
+
},
|
185
|
+
|
186
|
+
getValue: function() {
|
187
|
+
var values = _.map(this.items, function(item) {
|
188
|
+
return item.getValue();
|
189
|
+
});
|
190
|
+
|
191
|
+
//Filter empty items
|
192
|
+
return _.without(values, undefined, '');
|
193
|
+
},
|
194
|
+
|
195
|
+
setValue: function(value) {
|
196
|
+
this.value = value;
|
197
|
+
this.render();
|
198
|
+
},
|
199
|
+
|
200
|
+
focus: function() {
|
201
|
+
if (this.hasFocus) return;
|
202
|
+
|
203
|
+
if (this.items[0]) this.items[0].editor.focus();
|
204
|
+
},
|
205
|
+
|
206
|
+
blur: function() {
|
207
|
+
if (!this.hasFocus) return;
|
208
|
+
|
209
|
+
focusedItem = _.find(this.items, function(item) { return item.editor.hasFocus; });
|
210
|
+
|
211
|
+
if (focusedItem) focusedItem.editor.blur();
|
212
|
+
},
|
213
|
+
|
214
|
+
/**
|
215
|
+
* Override default remove function in order to remove item views
|
216
|
+
*/
|
217
|
+
remove: function() {
|
218
|
+
_.invoke(this.items, 'remove');
|
219
|
+
|
220
|
+
editors.Base.prototype.remove.call(this);
|
221
|
+
},
|
222
|
+
|
223
|
+
/**
|
224
|
+
* Run validation
|
225
|
+
*
|
226
|
+
* @return {Object|Null}
|
227
|
+
*/
|
228
|
+
validate: function() {
|
229
|
+
if (!this.validators) return null;
|
230
|
+
|
231
|
+
//Collect errors
|
232
|
+
var errors = _.map(this.items, function(item) {
|
233
|
+
return item.validate();
|
234
|
+
});
|
235
|
+
|
236
|
+
//Check if any item has errors
|
237
|
+
var hasErrors = _.compact(errors).length ? true : false;
|
238
|
+
if (!hasErrors) return null;
|
239
|
+
|
240
|
+
//If so create a shared error
|
241
|
+
var fieldError = {
|
242
|
+
type: 'list',
|
243
|
+
message: 'Some of the items in the list failed validation',
|
244
|
+
errors: errors
|
245
|
+
};
|
246
|
+
|
247
|
+
return fieldError;
|
248
|
+
}
|
249
|
+
});
|
250
|
+
|
251
|
+
|
252
|
+
/**
|
253
|
+
* A single item in the list
|
254
|
+
*
|
255
|
+
* @param {editors.List} options.list The List editor instance this item belongs to
|
256
|
+
* @param {Function} options.Editor Editor constructor function
|
257
|
+
* @param {String} options.key Model key
|
258
|
+
* @param {Mixed} options.value Value
|
259
|
+
* @param {Object} options.schema Field schema
|
260
|
+
*/
|
261
|
+
editors.List.Item = Backbone.View.extend({
|
262
|
+
events: {
|
263
|
+
'click [data-action="remove"]': function(event) {
|
264
|
+
event.preventDefault();
|
265
|
+
this.list.removeItem(this);
|
266
|
+
},
|
267
|
+
'keydown input[type=text]': function(event) {
|
268
|
+
if(event.keyCode != 13) return;
|
269
|
+
event.preventDefault();
|
270
|
+
this.list.addItem();
|
271
|
+
this.list.$list.find("> li:last input").focus();
|
272
|
+
}
|
273
|
+
},
|
274
|
+
|
275
|
+
initialize: function(options) {
|
276
|
+
this.list = options.list;
|
277
|
+
this.schema = options.schema || this.list.schema;
|
278
|
+
this.value = options.value;
|
279
|
+
this.Editor = options.Editor || editors.Text;
|
280
|
+
this.key = options.key;
|
281
|
+
},
|
282
|
+
|
283
|
+
render: function() {
|
284
|
+
//Create editor
|
285
|
+
this.editor = new this.Editor({
|
286
|
+
key: this.key,
|
287
|
+
schema: this.schema,
|
288
|
+
value: this.value,
|
289
|
+
list: this.list,
|
290
|
+
item: this
|
291
|
+
}).render();
|
292
|
+
|
293
|
+
//Create main element
|
294
|
+
var $el = $(Form.templates[this.schema.listItemTemplate]({
|
295
|
+
editor: '<b class="bbf-tmp"></b>'
|
296
|
+
}));
|
297
|
+
|
298
|
+
$el.find('.bbf-tmp').replaceWith(this.editor.el);
|
299
|
+
|
300
|
+
//Replace the entire element so there isn't a wrapper tag
|
301
|
+
this.setElement($el);
|
302
|
+
|
303
|
+
return this;
|
304
|
+
},
|
305
|
+
|
306
|
+
getValue: function() {
|
307
|
+
return this.editor.getValue();
|
308
|
+
},
|
309
|
+
|
310
|
+
setValue: function(value) {
|
311
|
+
this.editor.setValue(value);
|
312
|
+
},
|
313
|
+
|
314
|
+
focus: function() {
|
315
|
+
this.editor.focus();
|
316
|
+
},
|
317
|
+
|
318
|
+
blur: function() {
|
319
|
+
this.editor.blur();
|
320
|
+
},
|
321
|
+
|
322
|
+
remove: function() {
|
323
|
+
this.editor.remove();
|
324
|
+
|
325
|
+
Backbone.View.prototype.remove.call(this);
|
326
|
+
},
|
327
|
+
|
328
|
+
validate: function() {
|
329
|
+
var value = this.getValue(),
|
330
|
+
formValues = this.list.form ? this.list.form.getValue() : {},
|
331
|
+
validators = this.schema.validators,
|
332
|
+
getValidator = Form.helpers.getValidator;
|
333
|
+
|
334
|
+
if (!validators) return null;
|
335
|
+
|
336
|
+
//Run through validators until an error is found
|
337
|
+
var error = null;
|
338
|
+
_.every(validators, function(validator) {
|
339
|
+
error = getValidator(validator)(value, formValues);
|
340
|
+
|
341
|
+
return continueLoop = error ? false : true;
|
342
|
+
});
|
343
|
+
|
344
|
+
//Show/hide error
|
345
|
+
error ? this.setError(error) : this.clearError();
|
346
|
+
|
347
|
+
//Return error to be aggregated by list
|
348
|
+
return error ? error : null;
|
349
|
+
},
|
350
|
+
|
351
|
+
/**
|
352
|
+
* Show a validation error
|
353
|
+
*/
|
354
|
+
setError: function(err) {
|
355
|
+
this.$el.addClass(Form.classNames.error);
|
356
|
+
this.$el.attr('title', err.message);
|
357
|
+
},
|
358
|
+
|
359
|
+
/**
|
360
|
+
* Hide validation errors
|
361
|
+
*/
|
362
|
+
clearError: function() {
|
363
|
+
this.$el.removeClass(Form.classNames.error);
|
364
|
+
this.$el.attr('title', null);
|
365
|
+
}
|
366
|
+
});
|
367
|
+
|
368
|
+
|
369
|
+
/**
|
370
|
+
* Modal object editor for use with the List editor.
|
371
|
+
* To use it, set the 'itemType' property in a List schema to 'Object' or 'NestedModel'
|
372
|
+
*/
|
373
|
+
editors.List.Modal = editors.List.Object = editors.List.NestedModel = editors.Base.extend({
|
374
|
+
events: {
|
375
|
+
'click': 'openEditor'
|
376
|
+
},
|
377
|
+
|
378
|
+
/**
|
379
|
+
* @param {Object} options
|
380
|
+
* @param {Function} [options.schema.itemToString] Function to transform the value for display in the list.
|
381
|
+
* @param {String} [options.schema.itemType] Editor type e.g. 'Text', 'Object'.
|
382
|
+
* @param {Object} [options.schema.subSchema] Schema for nested form,. Required when itemType is 'Object'
|
383
|
+
* @param {Function} [options.schema.model] Model constructor function. Required when itemType is 'NestedModel'
|
384
|
+
*/
|
385
|
+
initialize: function(options) {
|
386
|
+
editors.Base.prototype.initialize.call(this, options);
|
387
|
+
|
388
|
+
var schema = this.schema;
|
389
|
+
|
390
|
+
//Dependencies
|
391
|
+
if (!editors.List.Modal.ModalAdapter) throw 'A ModalAdapter is required';
|
392
|
+
|
393
|
+
//Get nested schema if Object
|
394
|
+
if (schema.itemType == 'Object') {
|
395
|
+
if (!schema.subSchema) throw 'Missing required option "schema.subSchema"';
|
396
|
+
|
397
|
+
this.nestedSchema = schema.subSchema;
|
398
|
+
}
|
399
|
+
|
400
|
+
//Get nested schema if NestedModel
|
401
|
+
if (schema.itemType == 'NestedModel') {
|
402
|
+
if (!schema.model) throw 'Missing required option "schema.model"';
|
403
|
+
|
404
|
+
this.nestedSchema = schema.model.prototype.schema;
|
405
|
+
if (_.isFunction(this.nestedSchema)) this.nestedSchema = this.nestedSchema();
|
406
|
+
}
|
407
|
+
},
|
408
|
+
|
409
|
+
/**
|
410
|
+
* Render the list item representation
|
411
|
+
*/
|
412
|
+
render: function() {
|
413
|
+
var self = this;
|
414
|
+
|
415
|
+
//New items in the list are only rendered when the editor has been OK'd
|
416
|
+
if (_.isEmpty(this.value)) {
|
417
|
+
this.openEditor();
|
418
|
+
}
|
419
|
+
|
420
|
+
//But items with values are added automatically
|
421
|
+
else {
|
422
|
+
this.renderSummary();
|
423
|
+
|
424
|
+
setTimeout(function() {
|
425
|
+
self.trigger('readyToAdd');
|
426
|
+
}, 0);
|
427
|
+
}
|
428
|
+
|
429
|
+
if (this.hasFocus) this.trigger('blur', this);
|
430
|
+
|
431
|
+
return this;
|
432
|
+
},
|
433
|
+
|
434
|
+
/**
|
435
|
+
* Renders the list item representation
|
436
|
+
*/
|
437
|
+
renderSummary: function() {
|
438
|
+
var template = Form.templates['list.Modal'];
|
439
|
+
|
440
|
+
this.$el.html(template({
|
441
|
+
summary: this.getStringValue()
|
442
|
+
}));
|
443
|
+
},
|
444
|
+
|
445
|
+
/**
|
446
|
+
* Function which returns a generic string representation of an object
|
447
|
+
*
|
448
|
+
* @param {Object} value
|
449
|
+
*
|
450
|
+
* @return {String}
|
451
|
+
*/
|
452
|
+
itemToString: function(value) {
|
453
|
+
value = value || {};
|
454
|
+
|
455
|
+
//Pretty print the object keys and values
|
456
|
+
var parts = [];
|
457
|
+
_.each(this.nestedSchema, function(schema, key) {
|
458
|
+
var desc = schema.title ? schema.title : Form.helpers.keyToTitle(key),
|
459
|
+
val = value[key];
|
460
|
+
|
461
|
+
if (_.isUndefined(val) || _.isNull(val)) val = '';
|
462
|
+
|
463
|
+
parts.push(desc + ': ' + val);
|
464
|
+
});
|
465
|
+
|
466
|
+
return parts.join('<br />');
|
467
|
+
},
|
468
|
+
|
469
|
+
/**
|
470
|
+
* Returns the string representation of the object value
|
471
|
+
*/
|
472
|
+
getStringValue: function() {
|
473
|
+
var schema = this.schema,
|
474
|
+
value = this.getValue();
|
475
|
+
|
476
|
+
if (_.isEmpty(value)) return '[Empty]';
|
477
|
+
|
478
|
+
//If there's a specified toString use that
|
479
|
+
if (schema.itemToString) return schema.itemToString(value);
|
480
|
+
|
481
|
+
//Otherwise check if it's NestedModel with it's own toString() method
|
482
|
+
if (schema.itemType == 'NestedModel') {
|
483
|
+
return new (schema.model)(value).toString();
|
484
|
+
}
|
485
|
+
|
486
|
+
//Otherwise use the generic method or custom overridden method
|
487
|
+
return this.itemToString(value);
|
488
|
+
},
|
489
|
+
|
490
|
+
openEditor: function() {
|
491
|
+
var self = this;
|
492
|
+
|
493
|
+
var form = new Form({
|
494
|
+
schema: this.nestedSchema,
|
495
|
+
data: this.value
|
496
|
+
});
|
497
|
+
|
498
|
+
var modal = this.modal = new Backbone.BootstrapModal({
|
499
|
+
content: form,
|
500
|
+
animate: true
|
501
|
+
}).open();
|
502
|
+
|
503
|
+
this.trigger('open', this);
|
504
|
+
this.trigger('focus', this);
|
505
|
+
|
506
|
+
modal.on('cancel', function() {
|
507
|
+
this.modal = null;
|
508
|
+
|
509
|
+
this.trigger('close', this);
|
510
|
+
this.trigger('blur', this);
|
511
|
+
}, this);
|
512
|
+
|
513
|
+
modal.on('ok', _.bind(this.onModalSubmitted, this, form, modal));
|
514
|
+
},
|
515
|
+
|
516
|
+
/**
|
517
|
+
* Called when the user clicks 'OK'.
|
518
|
+
* Runs validation and tells the list when ready to add the item
|
519
|
+
*/
|
520
|
+
onModalSubmitted: function(form, modal) {
|
521
|
+
var isNew = !this.value;
|
522
|
+
|
523
|
+
//Stop if there are validation errors
|
524
|
+
var error = form.validate();
|
525
|
+
if (error) return modal.preventClose();
|
526
|
+
this.modal = null;
|
527
|
+
|
528
|
+
//If OK, render the list item
|
529
|
+
this.value = form.getValue();
|
530
|
+
|
531
|
+
this.renderSummary();
|
532
|
+
|
533
|
+
if (isNew) this.trigger('readyToAdd');
|
534
|
+
|
535
|
+
this.trigger('change', this);
|
536
|
+
|
537
|
+
this.trigger('close', this);
|
538
|
+
this.trigger('blur', this);
|
539
|
+
},
|
540
|
+
|
541
|
+
getValue: function() {
|
542
|
+
return this.value;
|
543
|
+
},
|
544
|
+
|
545
|
+
setValue: function(value) {
|
546
|
+
this.value = value;
|
547
|
+
},
|
548
|
+
|
549
|
+
focus: function() {
|
550
|
+
if (this.hasFocus) return;
|
551
|
+
|
552
|
+
this.openEditor();
|
553
|
+
},
|
554
|
+
|
555
|
+
blur: function() {
|
556
|
+
if (!this.hasFocus) return;
|
557
|
+
|
558
|
+
if (this.modal) {
|
559
|
+
this.modal.trigger('cancel');
|
560
|
+
this.modal.close();
|
561
|
+
}
|
562
|
+
}
|
563
|
+
}, {
|
564
|
+
//STATICS
|
565
|
+
|
566
|
+
//The modal adapter that creates and manages the modal dialog.
|
567
|
+
//Defaults to BootstrapModal (http://github.com/powmedia/backbone.bootstrap-modal)
|
568
|
+
//Can be replaced with another adapter that implements the same interface.
|
569
|
+
ModalAdapter: Backbone.BootstrapModal,
|
570
|
+
|
571
|
+
//Make the wait list for the 'ready' event before adding the item to the list
|
572
|
+
isAsync: true
|
573
|
+
});
|
574
|
+
|
575
|
+
})();
|