fixed errors
[xtuple] / lib / enyo-x / source / views / grid_box.js
1   /*jshint bitwise:false, indent:2, curly:true, eqeqeq:true, immed:true,
2 latedef:true, newcap:true, noarg:true, regexp:true, undef:true,
3 trailing:true, white:true, strict: false*/
4 /*global XT:true, XM:true, XV:true, _:true, enyo:true, Globalize:true*/
5
6 (function () {
7   var _events = "change readOnlyChange statusChange",
8     _readOnlyHeight = 57;
9
10   // TODO: picker onblur
11   // http://forums.enyojs.com/discussion/1578/picker-onblur#latest
12
13   /**
14     @name XV.GridAttr
15     @class Holds the data within the cell of a row of a grid.<br />
16   */
17   enyo.kind(/** @lends XV.GridAttr# */{
18     name: "XV.GridAttr",
19     classes: "xv-grid-attr",
20     published: {
21       attr: "",
22       isKey: false,
23       isLayout: false,
24       placeholder: null
25     }
26   });
27
28   /**
29     @name XV.Column
30     @class Represents a column within the read only section of a grid.
31   */
32   enyo.kind(/** @lends XV.Column# */{
33     name: "XV.GridColumn",
34     classes: "xv-grid-column"
35   });
36
37   /**
38     @name XV.GridRow
39     @class Represents a row within the read only section of a grid.
40
41   */
42   var readOnlyRow = enyo.mixin(/** @lends XV.GridRow# */{
43     name: "XV.GridRow",
44     classes: "xv-grid-row readonly",
45     published: {
46       value: null,
47       columns: null
48     },
49
50     create: function () {
51       this.inherited(arguments);
52       this.columnsChanged();
53     },
54
55     columnsChanged: function () {
56       this.inherited(arguments);
57       var columns = this.getColumns() || [],
58         that = this;
59       _.each(columns, function (column) {
60         var components = [];
61
62         // Build component array
63         _.each(column.rows, function (row) {
64           components.push({
65             kind: "XV.GridAttr",
66             attr: row.readOnlyAttr || row.attr,
67             formatter: row.formatter,
68             placeholder: row.placeholder
69           });
70         });
71
72         that.createComponent({
73           kind: "XV.GridColumn",
74           classes: column.classes || "grid-item",
75           components: components
76         });
77       });
78     },
79     setValue: function (value) {
80       this.value = value;
81       this.valueChanged();
82     },
83     valueChanged: function () {
84       var model = this.getValue(),
85         that = this,
86         views = _.filter(this.$, function (view) {
87           return view.attr || view.formatter;
88         }),
89         handled = [];
90
91       if (!model) { return; }
92
93       // Loop through each view matched to an attribute and set the value
94       _.each(views, function (view) {
95         var prop = view.attr,
96           attr = prop.indexOf(".") > -1 ? prop.prefix() : prop,
97           isRequired = _.contains(model.requiredAttributes, attr),
98           value = model.getValue(view.attr),
99           type = model.getType(view.attr),
100           isNothing = _.isUndefined(value) || _.isNull(value) || value === "",
101           isError = isNothing && isRequired,
102           isPlaceholder = false;
103
104         // Set this first in case other formatting sets error
105         view.addRemoveClass("error", isError);
106
107         // Show "required" placeholder if necessary,
108         // but only once per top level attribute
109         if (isError) {
110           if (!_.contains(handled, attr)) {
111             value = _.contains(handled, attr) ? "" : "_required".loc();
112             handled.push(attr);
113           }
114
115         // Handle formatting if applicable
116         } else if (view.formatter) {
117           value = that[view.formatter](value, view, model);
118
119         // Handle placeholder if applicable
120         } else if (isNothing) {
121           value = view.placeholder || "";
122           isPlaceholder = true;
123
124         } else if (_.contains(that.formatted, type)) {
125           value = that["format" + type](value, view, model);
126         }
127         view.setContent(value);
128         view.addRemoveClass("placeholder", isPlaceholder);
129       });
130
131     }
132   }, XV.FormattingMixin);
133
134   enyo.kind(readOnlyRow);
135
136   /**
137     The editable row of a GridBox for grid entry.
138     @see XV.RelationsEditorMixin.
139   */
140   var editor = enyo.mixin({}, XV.RelationsEditorMixin);
141   editor = enyo.mixin(editor, /** @lends XV.GridEditor */{
142     name: "XV.GridEditor",
143     classes: "xv-grid-row selected",
144     published: {
145       columns: null,
146       value: null
147     },
148     addButtonKeyup: function (inSender, inEvent) {
149       inEvent.preventDefault();
150     },
151     create: function () {
152       this.inherited(arguments);
153       this.columnsChanged();
154     },
155     columnsChanged: function () {
156       this.inherited(arguments);
157       var that = this,
158         columns = this.getColumns() || [],
159         components = [];
160
161       // Loop through each column and build rows
162       _.each(columns, function (column) {
163
164         // Create the column
165         components.push({
166           classes: "xv-grid-column " + (column.classes || "grid-item"),
167           components: _.pluck(column.rows, "editor")
168         });
169       });
170
171       // Create the controls
172       components.push(
173         {classes: "xv-grid-column grid-actions", components: [
174           {components: [
175             {kind: "enyo.Button",
176               classes: "icon-plus xv-gridbox-button",
177               name: "addGridRowButton",
178               onkeypress: "addButtonKeyup",
179             },
180             {kind: "enyo.Button",
181               classes: "icon-folder-open xv-gridbox-button",
182               name: "expandGridRowButton" },
183             {kind: "enyo.Button",
184               classes: "icon-remove-sign xv-gridbox-button",
185               name: "deleteGridRowButton" }
186           ]}
187         ]}
188       );
189       this.createComponents(components, {owner: this});
190     },
191     keyUp: function (inSender, inEvent) {
192       if (inEvent.keyCode === XV.KEY_DOWN) {
193         this.doMoveDown(inEvent);
194         return true;
195       } else if (inEvent.keyCode === XV.KEY_UP) {
196         this.doMoveUp(inEvent);
197         return true;
198       } else if (inEvent.keyCode === XV.KEY_ENTER) {
199         this.doEnterOut(inEvent);
200         return true;
201       }
202     },
203     /**
204       The editor will select the first non-disabled widget it can.
205
206       @param {Object} Widget
207     */
208     setFirstFocus: function () {
209       var editors = [],
210         first;
211
212       _.each(this.children, function (column) {
213         editors = editors.concat(column.children);
214       });
215       first = _.find(editors, function (editor) {
216         return !editor.disabled && editor.focus;
217       });
218
219       if (first) { first.focus(); }
220     }
221   });
222   _.extend(editor.events, {
223       onMoveUp: "",
224       onMoveDown: "",
225       onEnterOut: ""
226     });
227   _.extend(editor.handlers, {
228       onkeyup: "keyUp"
229     });
230   enyo.kind(editor);
231
232   /**
233     Input system for grid entry
234     @extends XV.Groupbox
235    */
236   enyo.kind(
237   /** @lends XV.GridBox# */{
238     name: "XV.GridBox",
239     kind: "XV.Groupbox",
240     classes: "panel xv-grid-box",
241     events: {
242       onChildWorkspace: "",
243       onExportAttr:     ""
244     },
245     handlers: {
246       ontap: "buttonTapped",
247       onEnterOut: "enterOut",
248       onMoveUp: "moveUp",
249       onMoveDown: "moveDown"
250     },
251     published: {
252       attr: null,
253       disabled: null,
254       value: null,
255       editableIndex: null, // number
256       title: "",
257       columns: null,
258       editorMixin: null,
259       summary: null,
260       workspace: null,
261       parentKey: null,
262       gridRowKind: "XV.GridRow",
263       gridEditorKind: "XV.GridEditor",
264       orderBy: null
265     },
266     bindCollection: function () {
267       var that = this,
268         collection = this.getValue();
269       this.unbindCollection();
270       collection.once("add remove", this.valueChanged, this);
271       _.each(collection.models, function (model) {
272         model.on("change status:DESTROYED_DIRTY", that.refreshLists, that);
273       });
274     },
275     buildComponents: function () {
276       var that = this,
277         title = this.getTitle(),
278         columns = this.getColumns() || [],
279         header = [],
280         readOnlyRow = [],
281         summary = this.getSummary(),
282         components = [],
283         editorMixin = this.getEditorMixin() || {},
284         editorCreate = editorMixin.create,
285         editor,
286         gridRowKind = this.getGridRowKind(),
287         gridEditorKind = this.getGridEditorKind();
288
289       editor = enyo.mixin({
290         kind: gridEditorKind,
291         name: "editableGridRow",
292         showing: false,
293         columns: columns
294       }, editorMixin);
295
296       // Hack: because unfortunately some mixins have implemented create
297       // and it's too much work to untangle that right now
298       if (editorCreate) {
299         editor.create = function () {
300           editorCreate.apply(this, arguments);
301           this.columnsChanged();
302         };
303       }
304
305       // View Header
306       components.push({
307         kind: "onyx.GroupboxHeader",
308         content: title,
309         classes: "xv-grid-groupbox-header"
310       });
311
312       // Row Header
313       _.each(columns, function (column) {
314         var contents = _.isArray(column.header) ? column.header : [column.header],
315           components = [];
316
317         _.each(contents, function (content) {
318           components.push({
319             //classes: "xv-grid-header " + (column.classes || "grid-item"),
320             content: content
321           });
322         });
323
324         header.push({kind: "FittableRows", components: components,
325           classes: "xv-grid-header " + (column.classes || "grid-item")});
326       });
327
328       components.push({
329         classes: "xv-grid-row",
330         name: "gridHeader",
331         components: header
332       });
333
334       // Row Definition
335       components.push(
336         {kind: "XV.Scroller", name: "mainGroup", horizontal: "hidden", fit: true, components: [
337           {kind: "List", name: "aboveGridList", classes: "xv-above-grid-list",
338               onSetupItem: "setupRowAbove", ontap: "gridRowTapAbove", components: [
339             { kind: gridRowKind, name: "aboveGridRow", columns: columns}
340           ]},
341           editor,
342           {kind: "List", name: "belowGridList", classes: "xv-below-grid-list",
343               onSetupItem: "setupRowBelow", ontap: "gridRowTapBelow", components: [
344             {kind: gridRowKind, name: "belowGridRow", columns: columns}
345           ]}
346         ]}
347       );
348
349       components.push({
350         kind: "FittableColumns",
351         name: "navigationButtonPanel",
352         classes: "xv-buttons",
353         components: [
354           {kind: "onyx.Button", name: "newButton", ontap: "newItem", classes: "icon-plus"},
355           {kind: "onyx.Button", name: "exportButton", ontap: "exportAttr",
356             icon: "share", classes: "icon-share"}
357         ]
358       });
359
360       // Summary
361       if (summary) {
362         components.push({
363           kind: summary,
364           name: "summaryPanel"
365         });
366       }
367
368       return components;
369     },
370     create: function () {
371       this.inherited(arguments);
372       this.createComponents(this.buildComponents());
373     },
374     buttonTapped: function (inSender, inEvent) {
375       var editor = this.$.editableGridRow,
376         model,
377         that = this,
378         success;
379
380       switch (inEvent.originator.name) {
381       case "addGridRowButton":
382         this.newItem();
383         break;
384       case "deleteGridRowButton":
385         // note that can't remove the model from the middle of the collection w/o error
386         // we just destroy the model and hide the row.
387         model = inEvent.originator.parent.parent.parent.value; // XXX better ways to do this
388         model.destroy({
389           success: function () {
390             that.setEditableIndex(null);
391             that.$.editableGridRow.hide();
392             that.valueChanged();
393           }
394         });
395         break;
396       case "expandGridRowButton":
397         model = inEvent.originator.parent.parent.parent.value; // XXX better ways to do this
398         this.unbindCollection();
399         editor.value.off("notify", editor.notify, editor);
400         this.doChildWorkspace({
401           workspace: this.getWorkspace(),
402           collection: this.getValue(),
403           index: this.getValue().indexOf(model),
404           callback: function () {
405             editor.value.on("notify", editor.notify, editor);
406             that.bindCollection();
407           }
408         });
409         break;
410       }
411     },
412     destroy: function () {
413       var model = this.value[this.getParentKey()];
414       model.off("status:READY_CLEAN", this.reset, this);
415       this.unbindCollection();
416       this.inherited(arguments);
417     },
418     /**
419      Propagate down disability to widgets.
420      */
421     disabledChanged: function () {
422       this.$.newButton.setDisabled(this.getDisabled());
423     },
424     /*
425       When a user taps the grid row we open it up for editing
426     */
427     gridRowTapAbove: function (inSender, inEvent) {
428       this.gridRowTapEither(inEvent.index, 0);
429     },
430     gridRowTapBelow: function (inSender, inEvent) {
431       this.gridRowTapEither(inEvent.index, this.getEditableIndex() + 1);
432     },
433     gridRowTapEither: function (index, indexStart, firstFocus) {
434       firstFocus = firstFocus !== false;
435       var editableIndex = index + indexStart,
436         belowListCount = this.getValue().length - editableIndex - 1,
437         editor = this.$.editableGridRow;
438
439       if (this.getDisabled()) {
440         // read-only means read-only
441         return;
442       }
443       if (index === undefined) {
444         // tap somewhere other than a row item
445         return;
446       }
447       this.setEditableIndex(editableIndex);
448       this.$.aboveGridList.setCount(editableIndex);
449       this.$.aboveGridList.render();
450       // XXX hack. not sure why the port doesn't know to resize down
451       this.$.aboveGridList.$.port.applyStyle("height", (_readOnlyHeight * this.liveModels(editableIndex - 1).length) + "px");
452       editor.setValue(this.getValue().at(editableIndex));
453       if (!editor.showing) {
454         editor.show();
455         editor.render();
456       }
457       if (firstFocus) {
458         editor.setFirstFocus();
459       }
460       this.$.belowGridList.setCount(belowListCount);
461       this.$.belowGridList.render();
462       // XXX hack. not sure why the port doesn't know to resize down
463       this.$.belowGridList.$.port.applyStyle("height", (_readOnlyHeight * belowListCount) + "px");
464     },
465     /**
466       Get all the models that are not destroyed, up through optional maxIndex parameter
467     */
468     liveModels: function (maxIndex) {
469       return _.compact(_.map(this.getValue().models, function (model, index) {
470         if (maxIndex !== undefined && index > maxIndex) {
471           return null;
472         } else if (model.isDestroyed()) {
473           return null;
474         } else {
475           return model;
476         }
477       }));
478     },
479     moveDown: function (inSender, inEvent) {
480       var outIndex = this.getEditableIndex(),
481         rowCount = this.getValue().length,
482         wrap = inEvent.wrap === true,
483         i;
484
485       if (wrap && outIndex + 1 === rowCount) {
486         this.newItem();
487       } else {
488         // go to the next live row, as if it had been tapped
489         for (i = outIndex + 1; i < this.getValue().length; i++) {
490           if (!this.getValue().at(i).isDestroyed()) {
491             this.gridRowTapEither(i, 0, wrap);
492             break;
493           }
494         }
495       }
496     },
497     moveUp: function (inSender, inEvent) {
498       var outIndex = this.getEditableIndex(),
499         firstFocus = inEvent.firstFocus === true,
500         i;
501
502       if (outIndex >= 0) {
503         // go to the next live row, as if it had been tapped
504         for (i = outIndex - 1; i >= 0; i--) {
505           if (!this.getValue().at(i).isDestroyed()) {
506             this.gridRowTapEither(i, 0, firstFocus);
507             break;
508           }
509         }
510       }
511     },
512     /*
513       Add a row to the grid
514     */
515     newItem: function () {
516       var editableIndex = this.getValue().length,
517         aboveListCount = this.liveModels(editableIndex - 1).length,
518         Klass = this.getValue().model,
519         model = new Klass(null, {isNew: true}),
520         editor = this.$.editableGridRow;
521
522       this.getValue().add(model, {status: XM.Model.READY_NEW});
523       this.setEditableIndex(editableIndex);
524       this.valueChanged();
525       // XXX hack. not sure why the port doesn't know to resize down
526       this.$.aboveGridList.$.port.applyStyle("height", (_readOnlyHeight * aboveListCount) + "px");
527       editor.setValue(model);
528       editor.show();
529       editor.render();
530       editor.setFirstFocus();
531     },
532     exportAttr: function (inSender, inEvent) {
533       var gridbox = this;
534       this.doExportAttr({ attr: gridbox.attr });
535     },
536     refreshLists: function () {
537       this.$.aboveGridList.refresh();
538       this.$.belowGridList.refresh();
539     },
540     reset: function () {
541       this.setEditableIndex(null);
542       this.valueChanged();
543     },
544     setupRowAbove: function (inSender, inEvent) {
545       this.setupRowEither(inEvent.index, this.$.aboveGridRow, 0);
546     },
547     setupRowBelow: function (inSender, inEvent) {
548       this.setupRowEither(inEvent.index, this.$.belowGridRow, this.getEditableIndex() + 1);
549     },
550     setupRowEither: function (index, gridRow, indexStart) {
551       var that = this,
552         model = this.getValue().at(indexStart + index);
553
554       // set the contents of the row
555       gridRow.setValue(model);
556       gridRow.setShowing(model && !model.isDestroyed());
557
558       return true;
559     },
560     enterOut: function (inSender, inEvent) {
561       inEvent.wrap = true;
562       this.moveDown(inSender, inEvent);
563     },
564     unbindCollection: function () {
565       var collection = this.getValue(),
566         that = this;
567
568       collection.off("add remove", this.valueChanged, this);
569       _.each(collection.models, function (model) {
570         model.off("change status:DESTROYED_DIRTY", that.refreshLists, that);
571       });
572     },
573     setValue: function (value) {
574       var orderBy = this.getOrderBy(),
575         collection;
576       this._setProperty("value", value, "valueChanged");
577
578       collection = value;
579       if (orderBy && !collection.comparator) {
580         collection.comparator = function (a, b) {
581           var aval,
582             bval,
583             attr,
584             i;
585           for (i = 0; i < orderBy.length; i++) {
586             attr = orderBy[i].attribute;
587             aval = orderBy[i].descending ? b.getValue(attr) : a.getValue(attr);
588             bval = orderBy[i].descending ? a.getValue(attr) : b.getValue(attr);
589             aval = !isNaN(aval) ? aval - 0 : aval;
590             bval = !isNaN(aval) ? bval - 0 : bval;
591             if (aval !== bval) {
592               return aval > bval ? 1 : -1;
593             }
594           }
595           return 0;
596         };
597       }
598       if (orderBy) {
599         collection.sort();
600       }
601     },
602     valueChanged: function () {
603       var that = this,
604         collection = this.getValue(),
605         model = this.value[this.getParentKey()];
606
607       // Make sure lists refresh if data changes
608       this.bindCollection();
609       this.$.aboveGridList.setCount(this.getEditableIndex() !== null ? this.getEditableIndex() : this.getValue().length);
610       // XXX hack. not sure why the port doesn't know to resize down
611       this.$.aboveGridList.$.port.applyStyle("height", (_readOnlyHeight * this.liveModels().length) + "px");
612       this.$.belowGridList.setCount(0);
613       this.$.belowGridList.$.port.applyStyle("height", "0px");
614       this.$.editableGridRow.setShowing(false);
615       this.$.aboveGridList.render();
616       this.$.editableGridRow.render();
617
618       // Should the model get saved and the server changed something
619       // come back here and reset things.
620       model.once("status:READY_CLEAN", this.reset, this);
621
622       // Update summary panel if applicable
623       if (this.$.summaryPanel) {
624         this.$.summaryPanel.setValue(model);
625       }
626     }
627   });
628 }());