ec885ff4d860155895d8908d316ccb508ca8db61
[xtuple] / lib / enyo-x / source / views / list_relations_editor_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 XM:true, XV:true, XT:true, _:true, enyo:true*/
5
6 (function () {
7
8   var _events = "change readOnlyChange statusChange refreshView";
9
10   XV.RelationsEditorMixin = enyo.mixin({
11     events: {
12       onNotify: ""
13     },
14     published: {
15       value: null
16     },
17     handlers: {
18       onValueChange: "controlValueChanged"
19     },
20     bind: function (action) {
21       if (this.value) {
22         this.value[action](_events, this.attributesChanged, this);
23         this.value[action]("notify", this.notify, this);
24         if (this.value.meta) {
25           this.value.meta[action]("change", this.attributesChanged, this);
26         }
27       }
28     },
29     destroy: function () {
30       this.bind("off");
31       this.value = null;
32       this.inherited(arguments);
33     },
34     setValue: function (value) {
35       this.bind("off");
36       this.value = value;
37       this.bind("on");
38       this.attributesChanged(value);
39       if (this.valueChanged) { this.valueChanged(value); }
40     }
41
42   }, XV.EditorMixin);
43
44   /**
45     @name XV.RelationsEditor
46     @class Use to define the editor for {@link XV.ListRelationsEditorBox}.
47     @extends XV.Groupbox
48   */
49   var editor = enyo.mixin({
50     name: "XV.RelationsEditor",
51     kind: "XV.Groupbox",
52   }, XV.RelationsEditorMixin);
53   enyo.kind(editor);
54
55   /**
56     @name XV.ListRelationsEditorBox
57     @class Provides a container in which its components
58     are a vertically stacked group of horizontal rows.<br />
59     Made up of a header, panels, and a row of navigation buttons.<br />
60     Must include a component called `list`.
61     List must be of subkind {@link XV.ListRelations}.
62     The `value` must be set to a collection of `XM.Model`.
63     @extends XV.Groupbox
64   */
65   enyo.kind(/** @lends XV.ListRelationsEditorBox# */{
66     name: "XV.ListRelationsEditorBox",
67     kind: "XV.Groupbox",
68     classes: "panel xv-relations-editor-box",
69     published: {
70       attr: null,
71       disabled: false,
72       value: null,
73       title: "",
74       parentKey: "",
75       listRelations: "",
76       editor: null,
77       summary: null,
78       childWorkspace: null,
79       fitButtons: true
80     },
81     events: {
82       onError: "",
83       onChildWorkspace: ""
84     },
85     handlers: {
86       onSelect: "selectionChanged",
87       onDeselect: "selectionChanged",
88       onTransitionFinish: "transitionFinished",
89       onValueChange: "controlValueChanged"
90     },
91     /**
92     @todo Document the attrChanged method.
93     */
94     attrChanged: function () {
95       this.$.list.setAttr(this.attr);
96     },
97     /**
98     @todo Document the controlValueChanged method.
99     */
100     controlValueChanged: function () {
101       this.$.list.refresh();
102       return true;
103     },
104     /**
105     @todo Document the create method.
106     */
107     create: function () {
108       this.inherited(arguments);
109       var editor = this.getEditor(),
110         panels,
111         control;
112
113       // Header
114       this.createComponent({
115         kind: "onyx.GroupboxHeader",
116         content: this.getTitle()
117       });
118
119       // List
120       panels = {
121         kind: "Panels",
122         fit: true,
123         arrangerKind: "CollapsingArranger",
124         components: [
125           {kind: editor, name: "editor"},
126           {kind: this.getListRelations(), name: "list",
127             attr: this.getAttr()}
128         ]
129       };
130       control = this.createComponent(panels);
131       control.setIndex(1);
132
133       // Buttons
134       this.createComponent({
135         kind: "FittableColumns",
136         name: "navigationButtonPanel",
137         classes: "xv-buttons",
138         components: [
139           {kind: "onyx.Button", name: "newButton", onclick: "newItem",
140           classes: "icon icon-plus"},
141           {kind: "onyx.Button", name: "deleteButton", onclick: "deleteItem",
142             disabled: true, classes: "icon-minus"},
143           {kind: "onyx.Button", name: "prevButton", onclick: "prevItem",
144             disabled: true, classes: "icon-chevron-left"},
145           {kind: "onyx.Button", name: "nextButton", onclick: "nextItem",
146             disabled: true, classes: "icon-chevron-right"},
147           {kind: "onyx.Button", name: "doneButton", onclick: "doneItem",
148             disabled: true, classes: "icon-ok"},
149           {kind: "onyx.Button", name: "expandButton", ontap: "launchWorkspace",
150             classes: "icon-resize-full"}
151         ]
152       });
153       this.$.expandButton.setShowing(_.isString(this.getChildWorkspace()));
154     },
155
156     /**
157       Marks the model of the selected item to be deleted on save
158       and remove it from its parent collection and the Enyo list
159     */
160     deleteItem: function () {
161       var index = this.$.list.getFirstSelected(),
162         model = index ? this.$.list.getModel(index) : null;
163       this.$.list.getSelection().deselect(index, false);
164       model.destroy();
165       this.$.list.lengthChanged();
166     },
167     destroy: function () {
168       this.unbind();
169       this.getValue().off("add remove", this.valueChanged, this);
170       this.inherited(arguments);
171     },
172     /**
173       Disables or enables the view
174      */
175     disabledChanged: function () {
176       this.$.newButton.setDisabled(this.getDisabled());
177       // complicated logic we need to disable and enable the
178       // done and delete buttons is here:
179       this.selectionChanged();
180       this.$.expandButton.setDisabled(this.getDisabled());
181     },
182     /**
183       Close the edit session and return to read-only summary view
184     */
185     doneItem: function () {
186       var index = this.$.list.getFirstSelected(),
187         selection = this.$.list.getSelection();
188       if (this.validate() && index) {
189         selection.deselect(index);
190         if (this.$.list.$.page0 && !this.$.list.$.page0.hasNode()) {
191           XT.log("Warning: page0 doesn't hasNode");
192         } else if (this.$.list.$.page1 && !this.$.list.$.page1.hasNode()) {
193           XT.log("Warning: page1 doesn't hasNode");
194         }
195         this.$.list.render();
196       }
197     },
198     error: function (model, error) {
199       var inEvent = {
200         model: model,
201         error: error
202       };
203       this.doError(inEvent);
204     },
205     launchWorkspace: function (inSender, inEvent) {
206       var index = Number(this.$.list.getFirstSelected());
207       this.doChildWorkspace({
208         workspace: this.getChildWorkspace(),
209         collection: this.getValue(),
210         index: index,
211         listRelations: this.$.list
212       });
213       return true;
214     },
215     /**
216       Add a new model to the collection and bring up a blank editor to fill it in
217     */
218     newItem: function () {
219       var collection = this.$.list.getValue(),
220         Klass = collection.model,
221         model = new Klass(null, {isNew: true}),
222         components = this.$.editor.getComponents(),
223         scroller,
224         length,
225         first;
226       if (this.validate()) {
227         this.$.editor.clear();
228         collection.add(model);
229         if (collection.comparator) { collection.sort(); }
230
231         // Exclude models marked for deletion
232         length = _.filter(collection.models, function (model) {
233           return !model.isDestroyed();
234         }).length;
235         this.$.list.select(length - 1);
236
237         // Scroll to top and set focus on first available widget
238         scroller = _.find(components, function (c) {
239           return c instanceof enyo.Scroller;
240         });
241         if (scroller) { scroller.scrollToTop(); }
242         first = _.find(components, function (c) {
243           return c.attr && !model.isReadOnly(c.attr);
244         });
245         if (first && first.focus) {
246           first.focus();
247         }
248       }
249     },
250     /**
251       Move to edit the next item in the collection.
252     */
253     nextItem: function () {
254       var index = this.$.list.getFirstSelected() - 0;
255       if (this.validate()) {
256         this.$.list.select(index + 1);
257       }
258     },
259     /**
260       Move to edit the previous line in the collection.
261     */
262     prevItem: function () {
263       var index = this.$.list.getFirstSelected() - 0;
264       if (this.validate()) {
265         this.$.list.select(index - 1);
266       }
267     },
268     /**
269     @todo Document the selectionChanged method.
270     */
271     selectionChanged: function () {
272       var index = this.$.list.getFirstSelected(),
273         model = index ? this.$.list.getModel(index) : null,
274         K = XM.Model,
275         that = this;
276       this.unbind();
277       this.$.deleteButton.setDisabled(true);
278       this.$.doneButton.setDisabled(!index || this.getDisabled());
279       if (model) {
280         model.on("invalid", this.error, this); // Error event binding
281         this.$.editor.setValue(model);
282         if (model.isNew() ||
283           model.isBusy() && model._prevStatus === K.READY_NEW) {
284           this.$.deleteButton.setDisabled(this.getDisabled());
285         } else {
286           model.used({
287             success: function (resp) {
288               if (that.$.deleteButton) { // Sometimes the workspace has been closed
289                 that.$.deleteButton.setDisabled(resp || that.getDisabled());
290               }
291             }
292           });
293         }
294         if (this.$.panels.getIndex()) { this.$.panels.setIndex(0); }
295         this.$.prevButton.setDisabled(index - 0 === 0);
296         this.$.nextButton.setDisabled(index - 0 === this.$.list.value.length - 1);
297       } else {
298         if (!this.$.panels.getIndex()) { this.$.panels.setIndex(1); }
299         this.$.prevButton.setDisabled(true);
300         this.$.nextButton.setDisabled(true);
301       }
302     },
303     /**
304     @todo Document the transitionFinished method.
305     */
306     transitionFinished: function (inSender, inEvent) {
307       if (inEvent.originator.name === 'panels') {
308         if (this.$.panels.getIndex() === 1) {
309           this.doneItem();
310         }
311         return true;
312       }
313     },
314     /**
315       Remove current model bindings.
316     */
317     unbind: function () {
318       var model = this.$.editor.getValue();
319       if (model) {
320         model.off("invalid", this.error, this);
321       }
322     },
323     /**
324       Returns whether a selected model is validate. If
325       none selected returns `true`. If an error is found
326       an error event is raised.
327
328       @returns {Boolean}
329     */
330     validate: function () {
331       var list = this.$.list,
332         index = list.getFirstSelected() - 0,
333         model = isNaN(index) ? false : list.getModel(index),
334         error = model ? model.validate(model.attributes) : false;
335       if (error) {
336         this.error(model, error);
337         return false;
338       }
339       return true;
340     },
341     /**
342     @todo Document the valueChanged method.
343     */
344     valueChanged: function () {
345       var value = this.getValue();
346       // Make sure list refreshes if collection changed
347       if (value) {
348         value.once("add remove", this.valueChanged, this);
349       }
350       this.$.list.setValue(value);
351     }
352   });
353
354 }());