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*/
7 var _events = "change readOnlyChange statusChange",
10 // TODO: picker onblur
11 // http://forums.enyojs.com/discussion/1578/picker-onblur#latest
15 @class Holds the data within the cell of a row of a grid.<br />
17 enyo.kind(/** @lends XV.GridAttr# */{
19 classes: "xv-grid-attr",
30 @class Represents a column within the read only section of a grid.
32 enyo.kind(/** @lends XV.Column# */{
33 name: "XV.GridColumn",
34 classes: "xv-grid-column"
39 @class Represents a row within the read only section of a grid.
42 var readOnlyRow = enyo.mixin(/** @lends XV.GridRow# */{
44 classes: "xv-grid-row readonly",
51 this.inherited(arguments);
52 this.columnsChanged();
55 columnsChanged: function () {
56 this.inherited(arguments);
57 var columns = this.getColumns() || [],
59 _.each(columns, function (column) {
62 // Build component array
63 _.each(column.rows, function (row) {
66 attr: row.readOnlyAttr || row.attr,
67 formatter: row.formatter,
68 placeholder: row.placeholder
72 that.createComponent({
73 kind: "XV.GridColumn",
74 classes: column.classes || "grid-item",
75 components: components
79 setValue: function (value) {
83 valueChanged: function () {
84 var model = this.getValue(),
86 views = _.filter(this.$, function (view) {
87 return view.attr || view.formatter;
91 if (!model) { return; }
93 // Loop through each view matched to an attribute and set the value
94 _.each(views, function (view) {
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;
104 // Set this first in case other formatting sets error
105 view.addRemoveClass("error", isError);
107 // Show "required" placeholder if necessary,
108 // but only once per top level attribute
110 if (!_.contains(handled, attr)) {
111 value = _.contains(handled, attr) ? "" : "_required".loc();
115 // Handle formatting if applicable
116 } else if (view.formatter) {
117 value = that[view.formatter](value, view, model);
119 // Handle placeholder if applicable
120 } else if (isNothing) {
121 value = view.placeholder || "";
122 isPlaceholder = true;
124 } else if (_.contains(that.formatted, type)) {
125 value = that["format" + type](value, view, model);
127 view.setContent(value);
128 view.addRemoveClass("placeholder", isPlaceholder);
132 }, XV.FormattingMixin);
134 enyo.kind(readOnlyRow);
137 The editable row of a GridBox for grid entry.
138 @see XV.RelationsEditorMixin.
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",
148 addButtonKeyup: function (inSender, inEvent) {
149 inEvent.preventDefault();
151 create: function () {
152 this.inherited(arguments);
153 this.columnsChanged();
155 columnsChanged: function () {
156 this.inherited(arguments);
158 columns = this.getColumns() || [],
161 // Loop through each column and build rows
162 _.each(columns, function (column) {
166 classes: "xv-grid-column " + (column.classes || "grid-item"),
167 components: _.pluck(column.rows, "editor")
171 // Create the controls
173 {classes: "xv-grid-column grid-actions", components: [
175 {kind: "enyo.Button",
176 classes: "icon-plus xv-gridbox-button",
177 name: "addGridRowButton",
178 onkeypress: "addButtonKeyup",
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" }
189 this.createComponents(components, {owner: this});
191 keyUp: function (inSender, inEvent) {
192 if (inEvent.keyCode === XV.KEY_DOWN) {
193 this.doMoveDown(inEvent);
195 } else if (inEvent.keyCode === XV.KEY_UP) {
196 this.doMoveUp(inEvent);
198 } else if (inEvent.keyCode === XV.KEY_ENTER) {
199 this.doEnterOut(inEvent);
204 The editor will select the first non-disabled widget it can.
206 @param {Object} Widget
208 setFirstFocus: function () {
212 _.each(this.children, function (column) {
213 editors = editors.concat(column.children);
215 first = _.find(editors, function (editor) {
216 return !editor.disabled && editor.focus;
219 if (first) { first.focus(); }
222 _.extend(editor.events, {
227 _.extend(editor.handlers, {
233 Input system for grid entry
237 /** @lends XV.GridBox# */{
240 classes: "panel xv-grid-box",
245 ontap: "buttonTapped",
246 onEnterOut: "enterOut",
248 onMoveDown: "moveDown"
254 editableIndex: null, // number
261 gridRowKind: "XV.GridRow",
262 gridEditorKind: "XV.GridEditor",
265 bindCollection: function () {
267 collection = this.getValue();
268 this.unbindCollection();
269 collection.once("add remove", this.valueChanged, this);
270 _.each(collection.models, function (model) {
271 model.on("change status:DESTROYED_DIRTY", that.refreshLists, that);
274 buildComponents: function () {
276 title = this.getTitle(),
277 columns = this.getColumns() || [],
280 summary = this.getSummary(),
282 editorMixin = this.getEditorMixin() || {},
283 editorCreate = editorMixin.create,
285 gridRowKind = this.getGridRowKind(),
286 gridEditorKind = this.getGridEditorKind();
288 editor = enyo.mixin({
289 kind: gridEditorKind,
290 name: "editableGridRow",
295 // Hack: because unfortunately some mixins have implemented create
296 // and it's too much work to untangle that right now
298 editor.create = function () {
299 editorCreate.apply(this, arguments);
300 this.columnsChanged();
306 kind: "onyx.GroupboxHeader",
308 classes: "xv-grid-groupbox-header"
312 _.each(columns, function (column) {
313 var contents = _.isArray(column.header) ? column.header : [column.header],
316 _.each(contents, function (content) {
318 //classes: "xv-grid-header " + (column.classes || "grid-item"),
323 header.push({kind: "FittableRows", components: components,
324 classes: "xv-grid-header " + (column.classes || "grid-item")});
328 classes: "xv-grid-row",
335 {kind: "XV.Scroller", name: "mainGroup", horizontal: "hidden", fit: true, components: [
336 {kind: "List", name: "aboveGridList", classes: "xv-above-grid-list",
337 onSetupItem: "setupRowAbove", ontap: "gridRowTapAbove", components: [
338 { kind: gridRowKind, name: "aboveGridRow", columns: columns}
341 {kind: "List", name: "belowGridList", classes: "xv-below-grid-list",
342 onSetupItem: "setupRowBelow", ontap: "gridRowTapBelow", components: [
343 {kind: gridRowKind, name: "belowGridRow", columns: columns}
350 kind: "FittableColumns",
351 name: "navigationButtonPanel",
352 classes: "xv-buttons",
354 {kind: "onyx.Button", name: "newButton", onclick: "newItem", classes: "icon-plus"}
368 create: function () {
369 this.inherited(arguments);
370 this.createComponents(this.buildComponents());
372 buttonTapped: function (inSender, inEvent) {
373 var editor = this.$.editableGridRow,
378 switch (inEvent.originator.name) {
379 case "addGridRowButton":
382 case "deleteGridRowButton":
383 // note that can't remove the model from the middle of the collection w/o error
384 // we just destroy the model and hide the row.
385 model = inEvent.originator.parent.parent.parent.value; // XXX better ways to do this
387 success: function () {
388 that.setEditableIndex(null);
389 that.$.editableGridRow.hide();
394 case "expandGridRowButton":
395 model = inEvent.originator.parent.parent.parent.value; // XXX better ways to do this
396 this.unbindCollection();
397 editor.value.off("notify", editor.notify, editor);
398 this.doChildWorkspace({
399 workspace: this.getWorkspace(),
400 collection: this.getValue(),
401 index: this.getValue().indexOf(model),
402 callback: function () {
403 editor.value.on("notify", editor.notify, editor);
404 that.bindCollection();
410 destroy: function () {
411 var model = this.value[this.getParentKey()];
412 model.off("status:READY_CLEAN", this.reset, this);
413 this.unbindCollection();
414 this.inherited(arguments);
417 Propagate down disability to widgets.
419 disabledChanged: function () {
420 this.$.newButton.setDisabled(this.getDisabled());
423 When a user taps the grid row we open it up for editing
425 gridRowTapAbove: function (inSender, inEvent) {
426 this.gridRowTapEither(inEvent.index, 0);
428 gridRowTapBelow: function (inSender, inEvent) {
429 this.gridRowTapEither(inEvent.index, this.getEditableIndex() + 1);
431 gridRowTapEither: function (index, indexStart, firstFocus) {
432 firstFocus = firstFocus !== false;
433 var editableIndex = index + indexStart,
434 belowListCount = this.getValue().length - editableIndex - 1,
435 editor = this.$.editableGridRow;
437 if (this.getDisabled()) {
438 // read-only means read-only
441 if (index === undefined) {
442 // tap somewhere other than a row item
445 this.setEditableIndex(editableIndex);
446 this.$.aboveGridList.setCount(editableIndex);
447 this.$.aboveGridList.render();
448 // XXX hack. not sure why the port doesn't know to resize down
449 this.$.aboveGridList.$.port.applyStyle("height", (_readOnlyHeight * this.liveModels(editableIndex - 1).length) + "px");
450 editor.setValue(this.getValue().at(editableIndex));
451 if (!editor.showing) {
456 editor.setFirstFocus();
458 this.$.belowGridList.setCount(belowListCount);
459 this.$.belowGridList.render();
460 // XXX hack. not sure why the port doesn't know to resize down
461 this.$.belowGridList.$.port.applyStyle("height", (_readOnlyHeight * belowListCount) + "px");
464 Get all the models that are not destroyed, up through optional maxIndex parameter
466 liveModels: function (maxIndex) {
467 return _.compact(_.map(this.getValue().models, function (model, index) {
468 if (maxIndex !== undefined && index > maxIndex) {
470 } else if (model.isDestroyed()) {
477 moveDown: function (inSender, inEvent) {
478 var outIndex = this.getEditableIndex(),
479 rowCount = this.getValue().length,
480 wrap = inEvent.wrap === true,
483 if (wrap && outIndex + 1 === rowCount) {
486 // go to the next live row, as if it had been tapped
487 for (i = outIndex + 1; i < this.getValue().length; i++) {
488 if (!this.getValue().at(i).isDestroyed()) {
489 this.gridRowTapEither(i, 0, wrap);
495 moveUp: function (inSender, inEvent) {
496 var outIndex = this.getEditableIndex(),
497 firstFocus = inEvent.firstFocus === true,
501 // go to the next live row, as if it had been tapped
502 for (i = outIndex - 1; i >= 0; i--) {
503 if (!this.getValue().at(i).isDestroyed()) {
504 this.gridRowTapEither(i, 0, firstFocus);
511 Add a row to the grid
513 newItem: function () {
514 var editableIndex = this.getValue().length,
515 aboveListCount = this.liveModels(editableIndex - 1).length,
516 Klass = this.getValue().model,
517 model = new Klass(null, {isNew: true}),
518 editor = this.$.editableGridRow;
520 this.getValue().add(model, {status: XM.Model.READY_NEW});
521 this.setEditableIndex(editableIndex);
523 // XXX hack. not sure why the port doesn't know to resize down
524 this.$.aboveGridList.$.port.applyStyle("height", (_readOnlyHeight * aboveListCount) + "px");
525 editor.setValue(model);
528 editor.setFirstFocus();
530 refreshLists: function () {
531 this.$.aboveGridList.refresh();
532 this.$.belowGridList.refresh();
535 this.setEditableIndex(null);
538 setupRowAbove: function (inSender, inEvent) {
539 this.setupRowEither(inEvent.index, this.$.aboveGridRow, 0);
541 setupRowBelow: function (inSender, inEvent) {
542 this.setupRowEither(inEvent.index, this.$.belowGridRow, this.getEditableIndex() + 1);
544 setupRowEither: function (index, gridRow, indexStart) {
546 model = this.getValue().at(indexStart + index);
548 // set the contents of the row
549 gridRow.setValue(model);
550 gridRow.setShowing(model && !model.isDestroyed());
554 enterOut: function (inSender, inEvent) {
556 this.moveDown(inSender, inEvent);
558 unbindCollection: function () {
559 var collection = this.getValue(),
562 collection.off("add remove", this.valueChanged, this);
563 _.each(collection.models, function (model) {
564 model.off("change status:DESTROYED_DIRTY", that.refreshLists, that);
567 setValue: function (value) {
568 var orderBy = this.getOrderBy(),
570 this._setProperty("value", value, "valueChanged");
573 if (orderBy && !collection.comparator) {
574 collection.comparator = function (a, b) {
579 for (i = 0; i < orderBy.length; i++) {
580 attr = orderBy[i].attribute;
581 aval = orderBy[i].descending ? b.getValue(attr) : a.getValue(attr);
582 bval = orderBy[i].descending ? a.getValue(attr) : b.getValue(attr);
583 aval = !isNaN(aval) ? aval - 0 : aval;
584 bval = !isNaN(aval) ? bval - 0 : bval;
586 return aval > bval ? 1 : -1;
596 valueChanged: function () {
598 collection = this.getValue(),
599 model = this.value[this.getParentKey()];
601 // Make sure lists refresh if data changes
602 this.bindCollection();
603 this.$.aboveGridList.setCount(this.getEditableIndex() !== null ? this.getEditableIndex() : this.getValue().length);
604 // XXX hack. not sure why the port doesn't know to resize down
605 this.$.aboveGridList.$.port.applyStyle("height", (_readOnlyHeight * this.liveModels().length) + "px");
606 this.$.belowGridList.setCount(0);
607 this.$.belowGridList.$.port.applyStyle("height", "0px");
608 this.$.editableGridRow.setShowing(false);
609 this.$.aboveGridList.render();
610 this.$.editableGridRow.render();
612 // Should the model get saved and the server changed something
613 // come back here and reset things.
614 model.once("status:READY_CLEAN", this.reset, this);
616 // Update summary panel if applicable
617 if (this.$.summaryPanel) {
618 this.$.summaryPanel.setValue(model);