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", onclick: "newItem", classes: "icon-plus"},
355 {kind: "font.TextIcon", name: "exportButton", onclick: "exportAttr", icon: "share", content: "_export".loc()} // TODO: classes?
369 create: function () {
370 this.inherited(arguments);
371 this.createComponents(this.buildComponents());
373 buttonTapped: function (inSender, inEvent) {
374 var editor = this.$.editableGridRow,
379 switch (inEvent.originator.name) {
380 case "addGridRowButton":
383 case "deleteGridRowButton":
384 // note that can't remove the model from the middle of the collection w/o error
385 // we just destroy the model and hide the row.
386 model = inEvent.originator.parent.parent.parent.value; // XXX better ways to do this
388 success: function () {
389 that.setEditableIndex(null);
390 that.$.editableGridRow.hide();
395 case "expandGridRowButton":
396 model = inEvent.originator.parent.parent.parent.value; // XXX better ways to do this
397 this.unbindCollection();
398 editor.value.off("notify", editor.notify, editor);
399 this.doChildWorkspace({
400 workspace: this.getWorkspace(),
401 collection: this.getValue(),
402 index: this.getValue().indexOf(model),
403 callback: function () {
404 editor.value.on("notify", editor.notify, editor);
405 that.bindCollection();
411 destroy: function () {
412 var model = this.value[this.getParentKey()];
413 model.off("status:READY_CLEAN", this.reset, this);
414 this.unbindCollection();
415 this.inherited(arguments);
418 Propagate down disability to widgets.
420 disabledChanged: function () {
421 this.$.newButton.setDisabled(this.getDisabled());
424 When a user taps the grid row we open it up for editing
426 gridRowTapAbove: function (inSender, inEvent) {
427 this.gridRowTapEither(inEvent.index, 0);
429 gridRowTapBelow: function (inSender, inEvent) {
430 this.gridRowTapEither(inEvent.index, this.getEditableIndex() + 1);
432 gridRowTapEither: function (index, indexStart, firstFocus) {
433 firstFocus = firstFocus !== false;
434 var editableIndex = index + indexStart,
435 belowListCount = this.getValue().length - editableIndex - 1,
436 editor = this.$.editableGridRow;
438 if (this.getDisabled()) {
439 // read-only means read-only
442 if (index === undefined) {
443 // tap somewhere other than a row item
446 this.setEditableIndex(editableIndex);
447 this.$.aboveGridList.setCount(editableIndex);
448 this.$.aboveGridList.render();
449 // XXX hack. not sure why the port doesn't know to resize down
450 this.$.aboveGridList.$.port.applyStyle("height", (_readOnlyHeight * this.liveModels(editableIndex - 1).length) + "px");
451 editor.setValue(this.getValue().at(editableIndex));
452 if (!editor.showing) {
457 editor.setFirstFocus();
459 this.$.belowGridList.setCount(belowListCount);
460 this.$.belowGridList.render();
461 // XXX hack. not sure why the port doesn't know to resize down
462 this.$.belowGridList.$.port.applyStyle("height", (_readOnlyHeight * belowListCount) + "px");
465 Get all the models that are not destroyed, up through optional maxIndex parameter
467 liveModels: function (maxIndex) {
468 return _.compact(_.map(this.getValue().models, function (model, index) {
469 if (maxIndex !== undefined && index > maxIndex) {
471 } else if (model.isDestroyed()) {
478 moveDown: function (inSender, inEvent) {
479 var outIndex = this.getEditableIndex(),
480 rowCount = this.getValue().length,
481 wrap = inEvent.wrap === true,
484 if (wrap && outIndex + 1 === rowCount) {
487 // go to the next live row, as if it had been tapped
488 for (i = outIndex + 1; i < this.getValue().length; i++) {
489 if (!this.getValue().at(i).isDestroyed()) {
490 this.gridRowTapEither(i, 0, wrap);
496 moveUp: function (inSender, inEvent) {
497 var outIndex = this.getEditableIndex(),
498 firstFocus = inEvent.firstFocus === true,
502 // go to the next live row, as if it had been tapped
503 for (i = outIndex - 1; i >= 0; i--) {
504 if (!this.getValue().at(i).isDestroyed()) {
505 this.gridRowTapEither(i, 0, firstFocus);
512 Add a row to the grid
514 newItem: function () {
515 var editableIndex = this.getValue().length,
516 aboveListCount = this.liveModels(editableIndex - 1).length,
517 Klass = this.getValue().model,
518 model = new Klass(null, {isNew: true}),
519 editor = this.$.editableGridRow;
521 this.getValue().add(model, {status: XM.Model.READY_NEW});
522 this.setEditableIndex(editableIndex);
524 // XXX hack. not sure why the port doesn't know to resize down
525 this.$.aboveGridList.$.port.applyStyle("height", (_readOnlyHeight * aboveListCount) + "px");
526 editor.setValue(model);
529 editor.setFirstFocus();
531 exportAttr: function(inSender, inEvent) {
532 var gridbox = inEvent.originator.parent.parent.parent;
533 this.doExportAttr({ recordType: gridbox.parent.parent.model,
534 uuid: gridbox.parent.parent.value.attributes.uuid,
535 attr: gridbox.attr });
537 refreshLists: function () {
538 this.$.aboveGridList.refresh();
539 this.$.belowGridList.refresh();
542 this.setEditableIndex(null);
545 setupRowAbove: function (inSender, inEvent) {
546 this.setupRowEither(inEvent.index, this.$.aboveGridRow, 0);
548 setupRowBelow: function (inSender, inEvent) {
549 this.setupRowEither(inEvent.index, this.$.belowGridRow, this.getEditableIndex() + 1);
551 setupRowEither: function (index, gridRow, indexStart) {
553 model = this.getValue().at(indexStart + index);
555 // set the contents of the row
556 gridRow.setValue(model);
557 gridRow.setShowing(model && !model.isDestroyed());
561 enterOut: function (inSender, inEvent) {
563 this.moveDown(inSender, inEvent);
565 unbindCollection: function () {
566 var collection = this.getValue(),
569 collection.off("add remove", this.valueChanged, this);
570 _.each(collection.models, function (model) {
571 model.off("change status:DESTROYED_DIRTY", that.refreshLists, that);
574 setValue: function (value) {
575 var orderBy = this.getOrderBy(),
577 this._setProperty("value", value, "valueChanged");
580 if (orderBy && !collection.comparator) {
581 collection.comparator = function (a, b) {
586 for (i = 0; i < orderBy.length; i++) {
587 attr = orderBy[i].attribute;
588 aval = orderBy[i].descending ? b.getValue(attr) : a.getValue(attr);
589 bval = orderBy[i].descending ? a.getValue(attr) : b.getValue(attr);
590 aval = !isNaN(aval) ? aval - 0 : aval;
591 bval = !isNaN(aval) ? bval - 0 : bval;
593 return aval > bval ? 1 : -1;
603 valueChanged: function () {
605 collection = this.getValue(),
606 model = this.value[this.getParentKey()];
608 // Make sure lists refresh if data changes
609 this.bindCollection();
610 this.$.aboveGridList.setCount(this.getEditableIndex() !== null ? this.getEditableIndex() : this.getValue().length);
611 // XXX hack. not sure why the port doesn't know to resize down
612 this.$.aboveGridList.$.port.applyStyle("height", (_readOnlyHeight * this.liveModels().length) + "px");
613 this.$.belowGridList.setCount(0);
614 this.$.belowGridList.$.port.applyStyle("height", "0px");
615 this.$.editableGridRow.setShowing(false);
616 this.$.aboveGridList.render();
617 this.$.editableGridRow.render();
619 // Should the model get saved and the server changed something
620 // come back here and reset things.
621 model.once("status:READY_CLEAN", this.reset, this);
623 // Update summary panel if applicable
624 if (this.$.summaryPanel) {
625 this.$.summaryPanel.setValue(model);