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