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",
242 onChildWorkspace: "",
246 ontap: "buttonTapped",
247 onEnterOut: "enterOut",
249 onMoveDown: "moveDown"
255 editableIndex: null, // number
262 gridRowKind: "XV.GridRow",
263 gridEditorKind: "XV.GridEditor",
266 bindCollection: function () {
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);
275 buildComponents: function () {
277 title = this.getTitle(),
278 columns = this.getColumns() || [],
281 summary = this.getSummary(),
283 editorMixin = this.getEditorMixin() || {},
284 editorCreate = editorMixin.create,
286 gridRowKind = this.getGridRowKind(),
287 gridEditorKind = this.getGridEditorKind();
289 editor = enyo.mixin({
290 kind: gridEditorKind,
291 name: "editableGridRow",
296 // Hack: because unfortunately some mixins have implemented create
297 // and it's too much work to untangle that right now
299 editor.create = function () {
300 editorCreate.apply(this, arguments);
301 this.columnsChanged();
307 kind: "onyx.GroupboxHeader",
309 classes: "xv-grid-groupbox-header"
313 _.each(columns, function (column) {
314 var contents = _.isArray(column.header) ? column.header : [column.header],
317 _.each(contents, function (content) {
319 //classes: "xv-grid-header " + (column.classes || "grid-item"),
324 header.push({kind: "FittableRows", components: components,
325 classes: "xv-grid-header " + (column.classes || "grid-item")});
329 classes: "xv-grid-row",
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}
342 {kind: "List", name: "belowGridList", classes: "xv-below-grid-list",
343 onSetupItem: "setupRowBelow", ontap: "gridRowTapBelow", components: [
344 {kind: gridRowKind, name: "belowGridRow", columns: columns}
350 kind: "FittableColumns",
351 name: "navigationButtonPanel",
352 classes: "xv-buttons",
354 {kind: "onyx.Button", name: "newButton", ontap: "newItem", classes: "icon-plus"},
355 {kind: "onyx.Button", name: "exportButton", ontap: "exportAttr",
356 icon: "share", content: "_export".loc(), classes: "icon-share"}
370 create: function () {
371 this.inherited(arguments);
372 this.createComponents(this.buildComponents());
374 buttonTapped: function (inSender, inEvent) {
375 var editor = this.$.editableGridRow,
380 switch (inEvent.originator.name) {
381 case "addGridRowButton":
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
389 success: function () {
390 that.setEditableIndex(null);
391 that.$.editableGridRow.hide();
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();
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);
419 Propagate down disability to widgets.
421 disabledChanged: function () {
422 this.$.newButton.setDisabled(this.getDisabled());
425 When a user taps the grid row we open it up for editing
427 gridRowTapAbove: function (inSender, inEvent) {
428 this.gridRowTapEither(inEvent.index, 0);
430 gridRowTapBelow: function (inSender, inEvent) {
431 this.gridRowTapEither(inEvent.index, this.getEditableIndex() + 1);
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;
439 if (this.getDisabled()) {
440 // read-only means read-only
443 if (index === undefined) {
444 // tap somewhere other than a row item
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) {
458 editor.setFirstFocus();
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");
466 Get all the models that are not destroyed, up through optional maxIndex parameter
468 liveModels: function (maxIndex) {
469 return _.compact(_.map(this.getValue().models, function (model, index) {
470 if (maxIndex !== undefined && index > maxIndex) {
472 } else if (model.isDestroyed()) {
479 moveDown: function (inSender, inEvent) {
480 var outIndex = this.getEditableIndex(),
481 rowCount = this.getValue().length,
482 wrap = inEvent.wrap === true,
485 if (wrap && outIndex + 1 === rowCount) {
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);
497 moveUp: function (inSender, inEvent) {
498 var outIndex = this.getEditableIndex(),
499 firstFocus = inEvent.firstFocus === true,
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);
513 Add a row to the grid
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;
522 this.getValue().add(model, {status: XM.Model.READY_NEW});
523 this.setEditableIndex(editableIndex);
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);
530 editor.setFirstFocus();
532 exportAttr: function (inSender, inEvent) {
534 this.doExportAttr({ attr: gridbox.attr });
536 refreshLists: function () {
537 this.$.aboveGridList.refresh();
538 this.$.belowGridList.refresh();
541 this.setEditableIndex(null);
544 setupRowAbove: function (inSender, inEvent) {
545 this.setupRowEither(inEvent.index, this.$.aboveGridRow, 0);
547 setupRowBelow: function (inSender, inEvent) {
548 this.setupRowEither(inEvent.index, this.$.belowGridRow, this.getEditableIndex() + 1);
550 setupRowEither: function (index, gridRow, indexStart) {
552 model = this.getValue().at(indexStart + index);
554 // set the contents of the row
555 gridRow.setValue(model);
556 gridRow.setShowing(model && !model.isDestroyed());
560 enterOut: function (inSender, inEvent) {
562 this.moveDown(inSender, inEvent);
564 unbindCollection: function () {
565 var collection = this.getValue(),
568 collection.off("add remove", this.valueChanged, this);
569 _.each(collection.models, function (model) {
570 model.off("change status:DESTROYED_DIRTY", that.refreshLists, that);
573 setValue: function (value) {
574 var orderBy = this.getOrderBy(),
576 this._setProperty("value", value, "valueChanged");
579 if (orderBy && !collection.comparator) {
580 collection.comparator = function (a, b) {
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;
592 return aval > bval ? 1 : -1;
602 valueChanged: function () {
604 collection = this.getValue(),
605 model = this.value[this.getParentKey()];
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();
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);
622 // Update summary panel if applicable
623 if (this.$.summaryPanel) {
624 this.$.summaryPanel.setValue(model);