templateize detail inline button panel, add save/continue and save/another buttons...
[plstackapi.git] / planetstack / core / xoslib / static / js / xoslib / xosHelper.js
1 function assert(outcome, description) {
2     if (!outcome) {
3         console.log(description);
4     }
5 }
6
7 function templateFromId(id) {
8     return _.template($(id).html());
9 }
10
11 HTMLView = Marionette.ItemView.extend({
12   render: function() {
13       this.$el.append(this.options.html);
14   },
15 });
16
17 XOSApplication = Marionette.Application.extend({
18     detailBoxId: "#detailBox",
19     errorBoxId: "#errorBox",
20     errorCloseButtonId: "#close-error-box",
21     successBoxId: "#successBox",
22     successCloseButtonId: "#close-success-box",
23     errorTemplate: "#xos-error-template",
24     successTemplate: "#xos-success-template",
25     logMessageCount: 0,
26
27     hideError: function() {
28         if (this.logWindowId) {
29         } else {
30             $(this.errorBoxId).hide();
31             $(this.successBoxId).hide();
32         }
33     },
34
35     showSuccess: function(result) {
36          result["success"] = "success";
37          if (this.logTableId) {
38              this.appendLogWindow(result);
39          } else {
40              $(this.successBoxId).show();
41              $(this.successBoxId).html(_.template($(this.successTemplate).html())(result));
42              var that=this;
43              $(this.successCloseButtonId).unbind().bind('click', function() {
44                  $(that.successBoxId).hide();
45              });
46          }
47     },
48
49     showError: function(result) {
50          result["success"] = "failure";
51          if (this.logTableId) {
52              this.appendLogWindow(result);
53          } else {
54              $(this.errorBoxId).show();
55              $(this.errorBoxId).html(_.template($(this.errorTemplate).html())(result));
56              var that=this;
57              $(this.errorCloseButtonId).unbind().bind('click', function() {
58                  $(that.errorBoxId).hide();
59              });
60          }
61     },
62
63     showInformational: function(result) {
64          result["success"] = "information";
65          if (this.logTableId) {
66              return this.appendLogWindow(result);
67          } else {
68              return undefined;
69          }
70     },
71
72     appendLogWindow: function(result) {
73         // compute a new logMessageId for this log message
74         logMessageId = "logMessage" + this.logMessageCount;
75         this.logMessageCount = this.logMessageCount + 1;
76         result["logMessageId"] = logMessageId;
77
78         logMessageTemplate=$("#xos-log-template").html();
79         assert(logMessageTemplate != undefined, "logMessageTemplate is undefined");
80         newRow = _.template(logMessageTemplate, result);
81         assert(newRow != undefined, "newRow is undefined");
82
83         if (result["infoMsgId"] != undefined) {
84             // We were passed the logMessageId of an informational message,
85             // and the caller wants us to replace that message with our own.
86             // i.e. replace an informational message with a success or an error.
87             $("#"+result["infoMsgId"]).replaceWith(newRow);
88         } else {
89             // Create a brand new log message rather than replacing one.
90             logTableBody = $(this.logTableId + " tbody");
91             logTableBody.prepend(newRow);
92         }
93
94         if (this.statusMsgId) {
95             $(this.statusMsgId).html( templateFromId("#xos-status-template")(result) );
96         }
97
98         return logMessageId;
99     },
100
101     hideLinkedItems: function(result) {
102         var index=0;
103         while (index<4) {\r
104             this["linkedObjs" + (index+1)].empty();\r
105             index = index + 1;\r
106         }\r
107     },\r
108 \r
109     listViewShower: function(listViewName, collection_name, regionName, title) {\r
110         var app=this;\r
111         return function() {\r
112             app[regionName].show(new app[listViewName]);\r
113             app.hideLinkedItems();\r
114             $("#contentTitle").html(templateFromId("#xos-title-list")({"title": title}));\r
115             $("#detail").show();\r
116             $("#xos-listview-button-box").show();\r
117             $("#tabs").hide();\r
118             $("#xos-detail-button-box").hide();\r
119         }\r
120     },\r
121 \r
122     detailShower: function(detailName, collection_name, regionName, title) {\r
123         var app=this;\r
124         showModelId = function(model_id) {\r
125             showModel = function(model) {\r
126                 detailViewClass = app[detailName];\r
127                 detailView = new detailViewClass({model: model});\r
128                 app[regionName].show(detailView);\r
129                 detailView.showLinkedItems();\r
130                 $("#xos-detail-button-box").show();\r
131                 $("#xos-listview-button-box").hide();\r
132             }\r
133 \r
134             $("#contentTitle").html(templateFromId("#xos-title-detail")({"title": title}));\r
135 \r
136             collection = xos[collection_name];\r
137             model = collection.get(model_id);\r
138             if (model == undefined) {\r
139                 if (!collection.isLoaded) {\r
140                     // If the model cannot be found, then maybe it's because\r
141                     // we haven't finished loading the collection yet. So wait for\r
142                     // the sort event to complete, then try again.\r
143                     collection.once("sort", function() {\r
144                         collection = xos[collection_name];\r
145                         model = collection.get(model_id);\r
146                         if (model == undefined) {\r
147                             // We tried. It's not here. Complain to the user.\r
148                             app[regionName].show(new HTMLView({html: "failed to load object " + model_id + " from collection " + collection_name}));\r
149                         } else {\r
150                             showModel(model);\r
151                         }\r
152                     });\r
153                 } else {\r
154                     // The collection was loaded, the user must just be asking for something we don't have.\r
155                     app[regionName].show(new HTMLView({html: "failed to load object " + model_id + " from collection " + collection_name}));\r
156                 }\r
157             } else {\r
158                 showModel(model);\r
159             }\r
160         }\r
161         return showModelId;\r
162     },\r
163 });
164
165 /* XOSDetailView
166       extend with:
167          app - MarionetteApplication
168          template - template (See XOSHelper.html)
169 */
170
171 XOSDetailView = Marionette.ItemView.extend({
172             tagName: "div",
173
174             events: {"click button.btn-xos-save-continue": "submitContinueClicked",
175                      "click button.btn-xos-save-leave": "submitLeaveClicked",
176                      "click button.btn-xos-save-another": "submitAddAnotherClicked",
177                      "change input": "inputChanged"},
178
179             /* inputChanged is watching the onChange events of the input controls. We
180                do this to track when this view is 'dirty', so we can throw up a warning\r
181                if the user tries to change his slices without saving first.\r
182             */\r
183 \r
184             inputChanged: function(e) {\r
185                 this.dirty = true;\r
186             },\r
187 \r
188             saveError: function(model, result, xhr, infoMsgId) {\r
189                 result["what"] = "save " + model.__proto__.modelName;\r
190                 result["infoMsgId"] = infoMsgId;\r
191                 this.app.showError(result);\r
192             },\r
193 \r
194             saveSuccess: function(model, result, xhr, infoMsgId) {\r
195                 result = {status: xhr.xhr.status, statusText: xhr.xhr.statusText};\r
196                 result["what"] = "save " + model.__proto__.modelName;\r
197                 result["infoMsgId"] = infoMsgId;\r
198                 this.app.showSuccess(result);\r
199             },
200
201             submitContinueClicked: function(e) {
202                 console.log("saveContinue");
203                 e.preventDefault();
204                 this.save();
205             },
206
207             submitLeaveClicked: function(e) {
208                 console.log("saveLeave");
209                 e.preventDefault();
210                 this.save();
211             },
212
213             submitAddAnotherClicked: function(e) {
214                 console.log("saveAnother");
215                 e.preventDefault();
216                 this.save();
217             },
218
219             save: function() {
220                 this.app.hideError();
221                 var infoMsgId = this.app.showInformational( {what: "save " + this.model.__proto__.modelName, status: "", statusText: "in progress..."} );\r
222                 var data = Backbone.Syphon.serialize(this);\r
223                 var that = this;\r
224                 this.model.save(data, {error: function(model, result, xhr) { that.saveError(model,result,xhr,infoMsgId);},\r
225                                        success: function(model, result, xhr) { that.saveSuccess(model,result,xhr,infoMsgId);}});\r
226                 this.dirty = false;\r
227             },
228
229             tabClick: function(tabId, regionName) {
230                     region = this.app[regionName];\r
231                     if (this.currentTabRegion != undefined) {\r
232                         this.currentTabRegion.$el.hide();\r
233                     }\r
234                     if (this.currentTabId != undefined) {\r
235                         $(this.currentTabId).removeClass('active');\r
236                     }\r
237                     this.currentTabRegion = region;\r
238                     this.currentTabRegion.$el.show();\r
239 \r
240                     this.currentTabId = tabId;\r
241                     $(tabId).addClass('active');\r
242             },
243
244             showTabs: function(tabs) {
245                 template = templateFromId("#xos-tabs-template", {tabs: tabs});
246                 $("#tabs").html(template(tabs));
247                 var that = this;
248
249                 _.each(tabs, function(tab) {
250                     var regionName = tab["region"];
251                     var tabId = '#xos-nav-'+regionName;
252                     $(tabId).bind('click', function() { that.tabClick(tabId, regionName); });
253                 });
254
255                 $("#tabs").show();
256             },
257
258             showLinkedItems: function() {
259                     tabs=[];
260
261                     tabs.push({name: "details", region: "detail"});
262
263                     var index=0;
264                     for (relatedName in this.model.collection.relatedCollections) {\r
265                         relatedField = this.model.collection.relatedCollections[relatedName];\r
266                         regionName = "linkedObjs" + (index+1);\r
267 \r
268                         relatedListViewClassName = relatedName + "ListView";\r
269                         assert(this.app[relatedListViewClassName] != undefined, relatedListViewClassName + " not found");\r
270                         relatedListViewClass = this.app[relatedListViewClassName].extend({collection: xos[relatedName].filterBy(relatedField,this.model.id)});\r
271                         this.app[regionName].show(new relatedListViewClass());\r
272                         if (this.app.hideTabsByDefault) {\r
273                             this.app[regionName].$el.hide();\r
274                         }\r
275                         tabs.push({name: relatedName, region: regionName});\r
276                         index = index + 1;\r
277                     }\r
278 \r
279                     while (index<4) {\r
280                         this.app["linkedObjs" + (index+1)].empty();\r
281                         index = index + 1;\r
282                     }\r
283 \r
284                     this.showTabs(tabs);\r
285                     this.tabClick('#xos-nav-detail', 'detail');\r
286               },\r
287 \r
288 });\r
289
290 /* XOSItemView
291       This is for items that will be displayed as table rows.
292       extend with:
293          app - MarionetteApplication
294          template - template (See XOSHelper.html)
295          detailClass - class of detail view, probably an XOSDetailView
296 */
297
298 XOSItemView = Marionette.ItemView.extend({
299              tagName: 'tr',
300              className: 'test-tablerow',
301
302              events: {"click": "changeItem"},
303
304              changeItem: function(e) {\r
305                     this.app.hideError();\r
306                     e.preventDefault();\r
307                     e.stopPropagation();\r
308 \r
309                     this.app.navigateToModel(this.app, this.detailClass, this.detailNavLink, this.model);\r
310              },\r
311 });
312
313 /* XOSListView:
314       extend with:
315          app - MarionetteApplication
316          childView - class of ItemView, probably an XOSItemView
317          template - template (see xosHelper.html)
318          collection - collection that holds these objects
319          title - title to display in template
320 */
321
322 XOSListView = Marionette.CompositeView.extend({
323              childViewContainer: 'tbody',\r
324 \r
325              initialize: function() {\r
326                  this.listenTo(this.collection, 'change', this._renderChildren)
327
328                  // Because many of the templates use idToName(), we need to
329                  // listen to the collections that hold the names for the ids
330                  // that we want to display.
331                  for (i in this.collection.foreignCollections) {
332                      foreignName = this.collection.foreignCollections[i];
333                      if (xos[foreignName] == undefined) {
334                          console.log("Failed to find xos class " + foreignName);
335                      }
336                      this.listenTo(xos[foreignName], 'change', this._renderChildren);
337                      this.listenTo(xos[foreignName], 'sort', this._renderChildren);
338                  }
339              },
340
341              templateHelpers: function() {
342                 return { title: this.title };
343              },\r
344 });
345
346 /* Give an id, the name of a collection, and the name of a field for models
347    within that collection, lookup the id and return the value of the field.
348 */
349
350 idToName = function(id, collectionName, fieldName) {
351     linkedObject = xos[collectionName].get(id);
352     if (linkedObject == undefined) {
353         return "#" + id;
354     } else {
355         return linkedObject.attributes[fieldName];
356     }
357 };
358
359 /* Constructs lists of <option> html blocks for items in a collection.
360
361    selectedId = the id of an object that should be selected, if any
362    collectionName = name of collection
363    fieldName = name of field within models of collection that will be displayed
364 */
365
366 idToOptions = function(selectedId, collectionName, fieldName) {
367     result=""
368     for (index in xos[collectionName].models) {
369         linkedObject = xos[collectionName].models[index];
370         linkedId = linkedObject["id"];
371         linkedName = linkedObject.attributes[fieldName];
372         if (linkedId == selectedId) {
373             selected = " selected";
374         } else {
375             selected = "";
376         }
377         result = result + '<option value="' + linkedId + '"' + selected + '>' + linkedName + '</option>';
378     }
379     return result;
380 };
381
382 /* Constructs an html <select> and the <option>s to go with it.
383
384    variable = variable name to return to form
385    selectedId = the id of an object that should be selected, if any
386    collectionName = name of collection
387    fieldName = name of field within models of collection that will be displayed
388 */
389
390 idToSelect = function(variable, selectedId, collectionName, fieldName) {
391     result = '<select name="' + variable + '">' +
392              idToOptions(selectedId, collectionName, fieldName) +
393              '</select>';
394     return result;
395 }
396