iugu-ux 0.8.8 → 0.8.9

Sign up to get free protection for your applications and to get access to all the features.
Files changed (28) hide show
  1. data/lib/iugu-ux/version.rb +1 -1
  2. data/vendor/assets/javascripts/iugu-ux/components/presenters/iugu-ui-alert.jst.eco +7 -3
  3. data/vendor/assets/javascripts/iugu-ux/components/presenters/iugu-ui-popup-container.jst.eco +6 -0
  4. data/vendor/assets/javascripts/iugu-ux/components/presenters/iugu-ui-search-filter.jst.eco +9 -6
  5. data/vendor/assets/javascripts/iugu-ux/components/usecode/iugu-ui-alert.js.coffee +2 -2
  6. data/vendor/assets/javascripts/iugu-ux/components/usecode/iugu-ui-base.js.coffee +12 -7
  7. data/vendor/assets/javascripts/iugu-ux/components/usecode/iugu-ui-dataset.js.coffee +5 -2
  8. data/vendor/assets/javascripts/iugu-ux/components/usecode/iugu-ui-popup-container.js.coffee +42 -0
  9. data/vendor/assets/javascripts/iugu-ux/components/usecode/iugu-ui-responsive-box.js.coffee +5 -3
  10. data/vendor/assets/javascripts/iugu-ux/components/usecode/iugu-ui-scrollable-content.js.coffee +2 -2
  11. data/vendor/assets/javascripts/iugu-ux/components/usecode/iugu-ui-search-filter.js.coffee +6 -6
  12. data/vendor/assets/javascripts/iugu-ux/components/usecode/iugu-ui-view.js.coffee +47 -6
  13. data/vendor/assets/javascripts/iugu-ux/web-app.js +2 -0
  14. data/vendor/assets/javascripts/vendor.js +6 -0
  15. data/vendor/assets/javascripts/vendor/backbone.advanced-delegate.js +39 -0
  16. data/vendor/assets/javascripts/vendor/backbone.fetch-event.js +15 -0
  17. data/vendor/assets/javascripts/vendor/backbone.forms.default.js +91 -0
  18. data/vendor/assets/javascripts/vendor/backbone.forms.js +2355 -0
  19. data/vendor/assets/javascripts/vendor/backbone.forms.list.js +579 -0
  20. data/vendor/assets/javascripts/vendor/jquery.fileupload.js +1114 -0
  21. data/vendor/assets/javascripts/web-app/comm.coffee +1 -1
  22. data/vendor/assets/javascripts/web-app/environment.js.coffee +2 -0
  23. data/vendor/assets/javascripts/web-app/helpers.coffee +3 -1
  24. data/vendor/assets/javascripts/web-app/managed_request.coffee +54 -0
  25. data/vendor/assets/stylesheets/iugu-ux.css +1 -0
  26. data/vendor/assets/stylesheets/iugu-ux/backbone.forms.default.css +140 -0
  27. data/vendor/assets/stylesheets/iugu-ux/components.sass +6 -1
  28. metadata +40 -30
@@ -0,0 +1,579 @@
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
+ var 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
+ var 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 error ? false : true;
342
+ });
343
+
344
+ //Show/hide error
345
+ if (error){
346
+ this.setError(error);
347
+ } else {
348
+ this.clearError();
349
+ }
350
+
351
+ //Return error to be aggregated by list
352
+ return error ? error : null;
353
+ },
354
+
355
+ /**
356
+ * Show a validation error
357
+ */
358
+ setError: function(err) {
359
+ this.$el.addClass(Form.classNames.error);
360
+ this.$el.attr('title', err.message);
361
+ },
362
+
363
+ /**
364
+ * Hide validation errors
365
+ */
366
+ clearError: function() {
367
+ this.$el.removeClass(Form.classNames.error);
368
+ this.$el.attr('title', null);
369
+ }
370
+ });
371
+
372
+
373
+ /**
374
+ * Modal object editor for use with the List editor.
375
+ * To use it, set the 'itemType' property in a List schema to 'Object' or 'NestedModel'
376
+ */
377
+ editors.List.Modal = editors.List.Object = editors.List.NestedModel = editors.Base.extend({
378
+ events: {
379
+ 'click': 'openEditor'
380
+ },
381
+
382
+ /**
383
+ * @param {Object} options
384
+ * @param {Function} [options.schema.itemToString] Function to transform the value for display in the list.
385
+ * @param {String} [options.schema.itemType] Editor type e.g. 'Text', 'Object'.
386
+ * @param {Object} [options.schema.subSchema] Schema for nested form,. Required when itemType is 'Object'
387
+ * @param {Function} [options.schema.model] Model constructor function. Required when itemType is 'NestedModel'
388
+ */
389
+ initialize: function(options) {
390
+ editors.Base.prototype.initialize.call(this, options);
391
+
392
+ var schema = this.schema;
393
+
394
+ //Dependencies
395
+ if (!editors.List.Modal.ModalAdapter) throw 'A ModalAdapter is required';
396
+
397
+ //Get nested schema if Object
398
+ if (schema.itemType === 'Object') {
399
+ if (!schema.subSchema) throw 'Missing required option "schema.subSchema"';
400
+
401
+ this.nestedSchema = schema.subSchema;
402
+ }
403
+
404
+ //Get nested schema if NestedModel
405
+ if (schema.itemType === 'NestedModel') {
406
+ if (!schema.model) throw 'Missing required option "schema.model"';
407
+
408
+ this.nestedSchema = schema.model.prototype.schema;
409
+ if (_.isFunction(this.nestedSchema)) this.nestedSchema = this.nestedSchema();
410
+ }
411
+ },
412
+
413
+ /**
414
+ * Render the list item representation
415
+ */
416
+ render: function() {
417
+ var self = this;
418
+
419
+ //New items in the list are only rendered when the editor has been OK'd
420
+ if (_.isEmpty(this.value)) {
421
+ this.openEditor();
422
+ }
423
+
424
+ //But items with values are added automatically
425
+ else {
426
+ this.renderSummary();
427
+
428
+ setTimeout(function() {
429
+ self.trigger('readyToAdd');
430
+ }, 0);
431
+ }
432
+
433
+ if (this.hasFocus) this.trigger('blur', this);
434
+
435
+ return this;
436
+ },
437
+
438
+ /**
439
+ * Renders the list item representation
440
+ */
441
+ renderSummary: function() {
442
+ var template = Form.templates['list.Modal'];
443
+
444
+ this.$el.html(template({
445
+ summary: this.getStringValue()
446
+ }));
447
+ },
448
+
449
+ /**
450
+ * Function which returns a generic string representation of an object
451
+ *
452
+ * @param {Object} value
453
+ *
454
+ * @return {String}
455
+ */
456
+ itemToString: function(value) {
457
+ value = value || {};
458
+
459
+ //Pretty print the object keys and values
460
+ var parts = [];
461
+ _.each(this.nestedSchema, function(schema, key) {
462
+ var desc = schema.title ? schema.title : Form.helpers.keyToTitle(key),
463
+ val = value[key];
464
+
465
+ if (_.isUndefined(val) || _.isNull(val)) val = '';
466
+
467
+ parts.push(desc + ': ' + val);
468
+ });
469
+
470
+ return parts.join('<br />');
471
+ },
472
+
473
+ /**
474
+ * Returns the string representation of the object value
475
+ */
476
+ getStringValue: function() {
477
+ var schema = this.schema,
478
+ value = this.getValue();
479
+
480
+ if (_.isEmpty(value)) return '[Empty]';
481
+
482
+ //If there's a specified toString use that
483
+ if (schema.itemToString) return schema.itemToString(value);
484
+
485
+ //Otherwise check if it's NestedModel with it's own toString() method
486
+ if (schema.itemType === 'NestedModel') {
487
+ return new (schema.model)(value).toString();
488
+ }
489
+
490
+ //Otherwise use the generic method or custom overridden method
491
+ return this.itemToString(value);
492
+ },
493
+
494
+ openEditor: function() {
495
+ var self = this;
496
+
497
+ var form = new Form({
498
+ schema: this.nestedSchema,
499
+ data: this.value
500
+ });
501
+
502
+ var modal = this.modal = new Backbone.BootstrapModal({
503
+ content: form,
504
+ animate: true
505
+ }).open();
506
+
507
+ this.trigger('open', this);
508
+ this.trigger('focus', this);
509
+
510
+ modal.on('cancel', function() {
511
+ this.modal = null;
512
+
513
+ this.trigger('close', this);
514
+ this.trigger('blur', this);
515
+ }, this);
516
+
517
+ modal.on('ok', _.bind(this.onModalSubmitted, this, form, modal));
518
+ },
519
+
520
+ /**
521
+ * Called when the user clicks 'OK'.
522
+ * Runs validation and tells the list when ready to add the item
523
+ */
524
+ onModalSubmitted: function(form, modal) {
525
+ var isNew = !this.value;
526
+
527
+ //Stop if there are validation errors
528
+ var error = form.validate();
529
+ if (error) return modal.preventClose();
530
+ this.modal = null;
531
+
532
+ //If OK, render the list item
533
+ this.value = form.getValue();
534
+
535
+ this.renderSummary();
536
+
537
+ if (isNew) this.trigger('readyToAdd');
538
+
539
+ this.trigger('change', this);
540
+
541
+ this.trigger('close', this);
542
+ this.trigger('blur', this);
543
+ },
544
+
545
+ getValue: function() {
546
+ return this.value;
547
+ },
548
+
549
+ setValue: function(value) {
550
+ this.value = value;
551
+ },
552
+
553
+ focus: function() {
554
+ if (this.hasFocus) return;
555
+
556
+ this.openEditor();
557
+ },
558
+
559
+ blur: function() {
560
+ if (!this.hasFocus) return;
561
+
562
+ if (this.modal) {
563
+ this.modal.trigger('cancel');
564
+ this.modal.close();
565
+ }
566
+ }
567
+ }, {
568
+ //STATICS
569
+
570
+ //The modal adapter that creates and manages the modal dialog.
571
+ //Defaults to BootstrapModal (http://github.com/powmedia/backbone.bootstrap-modal)
572
+ //Can be replaced with another adapter that implements the same interface.
573
+ ModalAdapter: Backbone.BootstrapModal,
574
+
575
+ //Make the wait list for the 'ready' event before adding the item to the list
576
+ isAsync: true
577
+ });
578
+
579
+ })();