blob: 2a1e94f89955f4dd9c1d74a0f09f1df7c27502e6 [file] [log] [blame]
Scott Baker0bf96b22014-11-04 15:41:47 -08001HTMLView = Marionette.ItemView.extend({
2 render: function() {
3 this.$el.append(this.options.html);
4 },
5});
6
Scott Bakerf50222e2014-12-08 14:49:03 -08007FilteredCompositeView = Marionette.CompositeView.extend( {
8 showCollection: function() {
9 var ChildView;
10 this.collection.each(function(child, index) {
11 if (this.filter && !this.filter(child)) {
12 return;
13 }
14 ChildView = this.getChildView(child);
15 this.addChild(child, ChildView, index);
16 }, this);
17
18 },
19});
20
Scott Baker29e8a2c2014-12-02 17:59:02 -080021XOSRouter = Marionette.AppRouter.extend({
22 initialize: function() {
23 this.routeStack=[];
24 },
25
26 onRoute: function(x,y,z) {
27 this.routeStack.push(Backbone.history.fragment);
28 },
29
30 prevPage: function() {
Scott Bakerf50222e2014-12-08 14:49:03 -080031 return this.routeStack.slice(-1)[0];
Scott Baker29e8a2c2014-12-02 17:59:02 -080032 },
Scott Baker6c19f242014-12-04 17:23:01 -080033
34 showPreviousURL: function() {
35 prevPage = this.prevPage();
Scott Bakerf50222e2014-12-08 14:49:03 -080036 console.log("showPreviousURL");
37 console.log(this.routeStack);
Scott Baker6c19f242014-12-04 17:23:01 -080038 if (prevPage) {
39 this.navigate("#"+prevPage, {trigger: false, replace: true} );
40 }
41 },
Scott Bakerab5f1362014-12-09 19:39:45 -080042
43 navigate: function(href, options) {
44 if (options.force) {
45 Marionette.AppRouter.prototype.navigate.call(this, "nowhere", {trigger: false, replace: true});
46 }
47 Marionette.AppRouter.prototype.navigate.call(this, href, options);
48 },
Scott Baker29e8a2c2014-12-02 17:59:02 -080049 });
50
51
52
Scott Bakerfdaee922014-11-03 09:43:23 -080053XOSApplication = Marionette.Application.extend({
Scott Bakerc16b4c12014-11-03 23:54:24 -080054 detailBoxId: "#detailBox",
Scott Bakerfdaee922014-11-03 09:43:23 -080055 errorBoxId: "#errorBox",
56 errorCloseButtonId: "#close-error-box",
57 successBoxId: "#successBox",
58 successCloseButtonId: "#close-success-box",
59 errorTemplate: "#xos-error-template",
60 successTemplate: "#xos-success-template",
Scott Baker0bf96b22014-11-04 15:41:47 -080061 logMessageCount: 0,
Scott Bakerfdaee922014-11-03 09:43:23 -080062
Scott Baker5f9e7672014-12-02 12:13:49 -080063 confirmDialog: function(view, event, callback) {
Scott Baker1e87c5a2014-11-18 23:31:48 -080064 $("#xos-confirm-dialog").dialog({
Scott Baker5ac6abe2014-12-02 14:50:26 -080065 autoOpen: false,
66 modal: true,
67 buttons : {
68 "Confirm" : function() {
69 $(this).dialog("close");
70 if (event) {
71 view.trigger(event);
72 }
73 if (callback) {
74 callback();
75 }
76 },
77 "Cancel" : function() {
78 $(this).dialog("close");
79 }
80 }
Scott Baker1e87c5a2014-11-18 23:31:48 -080081 });
82 $("#xos-confirm-dialog").dialog("open");
83 },
84
Scott Baker562d5952014-11-24 23:26:12 -080085 popupErrorDialog: function(responseText) {
Scott Baker92152752014-12-01 17:06:31 -080086 try {
87 parsed_error=$.parseJSON(responseText);
88 width=300;
89 }
90 catch(err) {
91 parsed_error=undefined;
92 width=640; // django stacktraces like wide width
93 }
94 if (parsed_error) {
Scott Baker5f9e7672014-12-02 12:13:49 -080095 $("#xos-error-dialog").html(templateFromId("#xos-error-response")(parsed_error));
Scott Baker92152752014-12-01 17:06:31 -080096 } else {
97 $("#xos-error-dialog").html(templateFromId("#xos-error-rawresponse")({responseText: responseText}))
98 }
99
Scott Baker562d5952014-11-24 23:26:12 -0800100 $("#xos-error-dialog").dialog({
101 modal: true,
Scott Baker92152752014-12-01 17:06:31 -0800102 width: width,
Scott Baker562d5952014-11-24 23:26:12 -0800103 buttons: {
104 Ok: function() { $(this).dialog("close"); }
105 }
106 });
107 },
108
Scott Baker0bf96b22014-11-04 15:41:47 -0800109 hideLinkedItems: function(result) {
Scott Baker9d37d562014-11-04 23:20:48 -0800110 var index=0;
Scott Baker5ac6abe2014-12-02 14:50:26 -0800111 while (index<4) {
112 this["linkedObjs" + (index+1)].empty();
113 index = index + 1;
114 }
115 },
116
Scott Bakerab5f1362014-12-09 19:39:45 -0800117 hideTabs: function() { $("#tabs").hide(); },
118 showTabs: function() { $("#tabs").show(); },
119
Scott Baker6c19f242014-12-04 17:23:01 -0800120 createListHandler: function(listViewName, collection_name, regionName, title) {
Scott Baker5ac6abe2014-12-02 14:50:26 -0800121 var app=this;
122 return function() {
Scott Bakerca4bf922014-12-09 18:38:13 -0800123 listView = new app[listViewName];
124 app[regionName].show(listView);
Scott Baker5ac6abe2014-12-02 14:50:26 -0800125 app.hideLinkedItems();
126 $("#contentTitle").html(templateFromId("#xos-title-list")({"title": title}));
127 $("#detail").show();
Scott Bakerab5f1362014-12-09 19:39:45 -0800128 app.hideTabs();
Scott Bakerca4bf922014-12-09 18:38:13 -0800129
130 listButtons = new XOSListButtonView({linkedView: listView});
131 app["rightButtonPanel"].show(listButtons);
Scott Baker5ac6abe2014-12-02 14:50:26 -0800132 }
133 },
134
Scott Baker6c19f242014-12-04 17:23:01 -0800135 createAddHandler: function(detailName, collection_name, regionName, title) {
Scott Baker5ac6abe2014-12-02 14:50:26 -0800136 var app=this;
137 return function() {
Scott Bakerab5f1362014-12-09 19:39:45 -0800138 console.log("addHandler");
139
140 app.hideLinkedItems();
141 app.hideTabs();
142
Scott Baker5ac6abe2014-12-02 14:50:26 -0800143 model = new xos[collection_name].model();
144 detailViewClass = app[detailName];
145 detailView = new detailViewClass({model: model, collection:xos[collection_name]});
146 app[regionName].show(detailView);
Scott Bakerab5f1362014-12-09 19:39:45 -0800147
148 detailButtons = new XOSDetailButtonView({linkedView: detailView});
149 app["rightButtonPanel"].show(detailButtons);
Scott Baker5ac6abe2014-12-02 14:50:26 -0800150 }
151 },
152
Scott Baker0a636cb2014-12-07 22:27:09 -0800153 createAddChildHandler: function(addChildName, collection_name) {
Scott Baker6c19f242014-12-04 17:23:01 -0800154 var app=this;
155 return function(parent_modelName, parent_fieldName, parent_id) {
156 app.Router.showPreviousURL();
Scott Baker6c19f242014-12-04 17:23:01 -0800157 model = new xos[collection_name].model();
158 model.attributes[parent_fieldName] = parent_id;
Scott Baker07b4a252014-12-08 23:54:18 -0800159 model.readOnlyFields.push(parent_fieldName);
Scott Baker0a636cb2014-12-07 22:27:09 -0800160 detailViewClass = app[addChildName];
Scott Baker6c19f242014-12-04 17:23:01 -0800161 var detailView = new detailViewClass({model: model, collection:xos[collection_name]});
162 detailView.dialog = $("xos-addchild-dialog");
163 app["addChildDetail"].show(detailView);
164 $("#xos-addchild-dialog").dialog({
165 autoOpen: false,
166 modal: true,
167 width: 640,
168 buttons : {
169 "Save" : function() {
Scott Baker0a636cb2014-12-07 22:27:09 -0800170 var addDialog = this;
171 detailView.synchronous = true;
Scott Bakerab5f1362014-12-09 19:39:45 -0800172 detailView.afterSave = function() { console.log("addChild afterSave"); $(addDialog).dialog("close"); }
Scott Baker6c19f242014-12-04 17:23:01 -0800173 detailView.save();
174
Scott Baker0a636cb2014-12-07 22:27:09 -0800175 //$(this).dialog("close");
Scott Baker6c19f242014-12-04 17:23:01 -0800176 },
177 "Cancel" : function() {
178 $(this).dialog("close");
179 }
180 }
181 });
182 $("#xos-addchild-dialog").dialog("open");
183 }
184 },
185
186 createDeleteHandler: function(collection_name) {
Scott Baker29e8a2c2014-12-02 17:59:02 -0800187 var app=this;
188 return function(model_id) {
189 console.log("deleteCalled");
190 collection = xos[collection_name];
191 model = collection.get(model_id);
192 assert(model!=undefined, "failed to get model " + model_id + " from collection " + collection_name);
Scott Bakerf50222e2014-12-08 14:49:03 -0800193 app.Router.showPreviousURL();
194 app.deleteDialog(model);
Scott Baker29e8a2c2014-12-02 17:59:02 -0800195 }
196 },
197
Scott Baker6c19f242014-12-04 17:23:01 -0800198 createDetailHandler: function(detailName, collection_name, regionName, title) {
Scott Baker5ac6abe2014-12-02 14:50:26 -0800199 var app=this;
200 showModelId = function(model_id) {
201 $("#contentTitle").html(templateFromId("#xos-title-detail")({"title": title}));
202
203 collection = xos[collection_name];
204 model = collection.get(model_id);
205 if (model == undefined) {
206 app[regionName].show(new HTMLView({html: "failed to load object " + model_id + " from collection " + collection_name}));
207 } else {
208 detailViewClass = app[detailName];
209 detailView = new detailViewClass({model: model});
210 app[regionName].show(detailView);
211 detailView.showLinkedItems();
Scott Bakerca4bf922014-12-09 18:38:13 -0800212
213 detailButtons = new XOSDetailButtonView({linkedView: detailView});
214 app["rightButtonPanel"].show(detailButtons);
Scott Baker5ac6abe2014-12-02 14:50:26 -0800215 }
216 }
217 return showModelId;
218 },
219
220 /* error handling callbacks */
221
222 hideError: function() {
Scott Baker5f9e7672014-12-02 12:13:49 -0800223 if (this.logWindowId) {
224 } else {
225 $(this.errorBoxId).hide();
226 $(this.successBoxId).hide();
227 }
228 },
Scott Bakerfdaee922014-11-03 09:43:23 -0800229
Scott Baker5f9e7672014-12-02 12:13:49 -0800230 showSuccess: function(result) {
231 result["statusclass"] = "success";
232 if (this.logTableId) {
233 this.appendLogWindow(result);
234 } else {
235 $(this.successBoxId).show();
236 $(this.successBoxId).html(_.template($(this.successTemplate).html())(result));
237 var that=this;
238 $(this.successCloseButtonId).unbind().bind('click', function() {
239 $(that.successBoxId).hide();
240 });
241 }
Scott Baker5ac6abe2014-12-02 14:50:26 -0800242 },
243
244 showError: function(result) {
Scott Baker5f9e7672014-12-02 12:13:49 -0800245 result["statusclass"] = "failure";
246 if (this.logTableId) {
247 this.appendLogWindow(result);
248 this.popupErrorDialog(result.responseText);
249 } else {
250 // this is really old stuff
251 $(this.errorBoxId).show();
252 $(this.errorBoxId).html(_.template($(this.errorTemplate).html())(result));
253 var that=this;
254 $(this.errorCloseButtonId).unbind().bind('click', function() {
255 $(that.errorBoxId).hide();
256 });
257 }
Scott Baker5ac6abe2014-12-02 14:50:26 -0800258 },
259
260 showInformational: function(result) {
Scott Baker5f9e7672014-12-02 12:13:49 -0800261 result["statusclass"] = "inprog";
262 if (this.logTableId) {
263 return this.appendLogWindow(result);
264 } else {
265 return undefined;
266 }
267 },
268
269 appendLogWindow: function(result) {
270 // compute a new logMessageId for this log message
271 logMessageId = "logMessage" + this.logMessageCount;
272 this.logMessageCount = this.logMessageCount + 1;
273 result["logMessageId"] = logMessageId;
274
275 logMessageTemplate=$("#xos-log-template").html();
276 assert(logMessageTemplate != undefined, "logMessageTemplate is undefined");
277 newRow = _.template(logMessageTemplate, result);
278 assert(newRow != undefined, "newRow is undefined");
279
280 if (result["infoMsgId"] != undefined) {
281 // We were passed the logMessageId of an informational message,
282 // and the caller wants us to replace that message with our own.
283 // i.e. replace an informational message with a success or an error.
284 $("#"+result["infoMsgId"]).replaceWith(newRow);
285 } else {
286 // Create a brand new log message rather than replacing one.
287 logTableBody = $(this.logTableId + " tbody");
288 logTableBody.prepend(newRow);
289 }
290
291 if (this.statusMsgId) {
292 $(this.statusMsgId).html( templateFromId("#xos-status-template")(result) );
293 }
294
295 limitTableRows(this.logTableId, 5);
296
297 return logMessageId;
Scott Baker5ac6abe2014-12-02 14:50:26 -0800298 },
299
300 saveError: function(model, result, xhr, infoMsgId) {
301 console.log("saveError");
302 result["what"] = "save " + model.modelName + " " + model.attributes.humanReadableName;
303 result["infoMsgId"] = infoMsgId;
304 this.showError(result);
305 },
306
307 saveSuccess: function(model, result, xhr, infoMsgId, addToCollection) {
308 console.log("saveSuccess");
Scott Baker29e8a2c2014-12-02 17:59:02 -0800309 if (model.addToCollection) {
310 console.log("addToCollection");
Scott Bakerf50222e2014-12-08 14:49:03 -0800311 console.log(model.addToCollection);
Scott Baker29e8a2c2014-12-02 17:59:02 -0800312 model.addToCollection.add(model);
313 model.addToCollection.sort();
314 model.addToCollection = undefined;
Scott Baker5ac6abe2014-12-02 14:50:26 -0800315 }
316 result = {status: xhr.xhr.status, statusText: xhr.xhr.statusText};
317 result["what"] = "save " + model.modelName + " " + model.attributes.humanReadableName;
318 result["infoMsgId"] = infoMsgId;
319 this.showSuccess(result);
Scott Baker5f9e7672014-12-02 12:13:49 -0800320 },
321
322 destroyError: function(model, result, xhr, infoMsgId) {
Scott Baker5ac6abe2014-12-02 14:50:26 -0800323 result["what"] = "destroy " + model.modelName + " " + model.attributes.humanReadableName;
324 result["infoMsgId"] = infoMsgId;
325 this.showError(result);
326 },
327
328 destroySuccess: function(model, result, xhr, infoMsgId) {
329 result = {status: xhr.xhr.status, statusText: xhr.xhr.statusText};
330 result["what"] = "destroy " + model.modelName + " " + model.attributes.humanReadableName;
331 result["infoMsgId"] = infoMsgId;
332 this.showSuccess(result);
333 },
334
335 /* end error handling callbacks */
336
337 destroyModel: function(model) {
338 //console.log("destroyModel"); console.log(model);
Scott Baker5f9e7672014-12-02 12:13:49 -0800339 this.hideError();
340 var infoMsgId = this.showInformational( {what: "destroy " + model.modelName + " " + model.attributes.humanReadableName, status: "", statusText: "in progress..."} );
341 var that = this;
342 model.destroy({error: function(model, result, xhr) { that.destroyError(model,result,xhr,infoMsgId);},
343 success: function(model, result, xhr) { that.destroySuccess(model,result,xhr,infoMsgId);}});
344 },
345
Scott Baker29e8a2c2014-12-02 17:59:02 -0800346 deleteDialog: function(model, afterDelete) {
Scott Baker5f9e7672014-12-02 12:13:49 -0800347 var that=this;
Scott Baker29e8a2c2014-12-02 17:59:02 -0800348 assert(model!=undefined, "deleteDialog's model is undefined");
Scott Baker5ac6abe2014-12-02 14:50:26 -0800349 //console.log("deleteDialog"); console.log(model);
350 this.confirmDialog(null, null, function() {
351 //console.log("deleteConfirm"); console.log(model);
Scott Baker5f9e7672014-12-02 12:13:49 -0800352 modelName = model.modelName;
353 that.destroyModel(model);
Scott Baker29e8a2c2014-12-02 17:59:02 -0800354 if (afterDelete=="list") {
Scott Baker5f9e7672014-12-02 12:13:49 -0800355 that.navigate("list", modelName);
356 }
Scott Baker5f9e7672014-12-02 12:13:49 -0800357 });
358 },
359});
Scott Baker5ac6abe2014-12-02 14:50:26 -0800360
Scott Bakerca4bf922014-12-09 18:38:13 -0800361XOSButtonView = Marionette.ItemView.extend({
362 events: {"click button.btn-xos-save-continue": "submitContinueClicked",
363 "click button.btn-xos-save-leave": "submitLeaveClicked",
364 "click button.btn-xos-save-another": "submitAddAnotherClicked",
365 "click button.btn-xos-delete": "deleteClicked",
366 "click button.btn-xos-add": "addClicked",
367 "click button.btn-xos-refresh": "refreshClicked",
368 },
369
370 submitLeaveClicked: function(e) {
371 this.options.linkedView.submitLeaveClicked.call(this.options.linkedView, e);
372 },
373
374 submitContinueClicked: function(e) {
375 this.options.linkedView.submitContinueClicked.call(this.options.linkedView, e);
376 },
377
378 submitAddAnotherClicked: function(e) {
379 this.options.linkedView.submitAddAnotherClicked.call(this.options.linkedView, e);
380 },
381
382 submitDeleteClicked: function(e) {
383 this.options.linkedView.submitDeleteClicked.call(this.options.linkedView, e);
384 },
385
386 addClicked: function(e) {
387 this.options.linkedView.addClicked.call(this.options.linkedView, e);
388 },
389
390 refreshClicked: function(e) {
391 this.options.linkedView.refreshClicked.call(this.options.linkedView, e);
392 },
393 });
394
395XOSDetailButtonView = XOSButtonView.extend({ template: "#xos-savebuttons-template" });
396XOSListButtonView = XOSButtonView.extend({ template: "#xos-listbuttons-template" });
397
Scott Bakerc16b4c12014-11-03 23:54:24 -0800398/* XOSDetailView
399 extend with:
400 app - MarionetteApplication
401 template - template (See XOSHelper.html)
402*/
403
404XOSDetailView = Marionette.ItemView.extend({
405 tagName: "div",
406
Scott Bakere49f08c2014-11-07 13:01:43 -0800407 events: {"click button.btn-xos-save-continue": "submitContinueClicked",
408 "click button.btn-xos-save-leave": "submitLeaveClicked",
409 "click button.btn-xos-save-another": "submitAddAnotherClicked",
Scott Baker1e87c5a2014-11-18 23:31:48 -0800410 "click button.btn-xos-delete": "deleteClicked",
Scott Bakerc16b4c12014-11-03 23:54:24 -0800411 "change input": "inputChanged"},
412
Scott Bakerc16b4c12014-11-03 23:54:24 -0800413 /* inputChanged is watching the onChange events of the input controls. We
Scott Baker5ac6abe2014-12-02 14:50:26 -0800414 do this to track when this view is 'dirty', so we can throw up a warning
415 if the user tries to change his slices without saving first.
416 */
417
Scott Baker6c19f242014-12-04 17:23:01 -0800418 initialize: function() {
Scott Bakerf50222e2014-12-08 14:49:03 -0800419 this.on("saveSuccess", this.onAfterSave);
Scott Baker6c19f242014-12-04 17:23:01 -0800420 this.synchronous = false;
421 },
422
423 afterSave: function(e) {
424 },
425
Scott Bakerf50222e2014-12-08 14:49:03 -0800426 onAfterSave: function(e) {
427 this.afterSave(e);
428 },
429
Scott Baker5ac6abe2014-12-02 14:50:26 -0800430 inputChanged: function(e) {
431 this.dirty = true;
432 },
433
434 submitContinueClicked: function(e) {
Scott Bakere49f08c2014-11-07 13:01:43 -0800435 console.log("saveContinue");
436 e.preventDefault();
Scott Bakercb901812014-12-09 17:27:52 -0800437 this.afterSave = function() { };
Scott Bakere49f08c2014-11-07 13:01:43 -0800438 this.save();
439 },
440
441 submitLeaveClicked: function(e) {
442 console.log("saveLeave");
443 e.preventDefault();
Scott Baker6c19f242014-12-04 17:23:01 -0800444 var that=this;
445 this.afterSave = function() {
446 that.app.navigate("list", that.model.modelName);
447 }
Scott Bakere49f08c2014-11-07 13:01:43 -0800448 this.save();
449 },
450
451 submitAddAnotherClicked: function(e) {
452 console.log("saveAnother");
Scott Bakerca4bf922014-12-09 18:38:13 -0800453 console.log(this);
Scott Bakere49f08c2014-11-07 13:01:43 -0800454 e.preventDefault();
Scott Baker6c19f242014-12-04 17:23:01 -0800455 var that=this;
456 this.afterSave = function() {
Scott Bakerab5f1362014-12-09 19:39:45 -0800457 console.log("addAnother afterSave");
Scott Baker6c19f242014-12-04 17:23:01 -0800458 that.app.navigate("add", that.model.modelName);
459 }
Scott Bakere49f08c2014-11-07 13:01:43 -0800460 this.save();
461 },
462
463 save: function() {
464 this.app.hideError();
Scott Baker5ac6abe2014-12-02 14:50:26 -0800465 var data = Backbone.Syphon.serialize(this);
466 var that = this;
467 var isNew = !this.model.id;
468
469 this.$el.find(".help-inline").remove();
470
471 /* although model.validate() is called automatically by
472 model.save, we call it ourselves, so we can throw up our
473 validation error before creating the infoMsg in the log
474 */
475 errors = this.model.xosValidate(data);
476 if (errors) {
477 this.onFormDataInvalid(errors);
478 return;
479 }
480
481 if (isNew) {
482 this.model.attributes.humanReadableName = "new " + model.modelName;
483 this.model.addToCollection = this.collection;
484 } else {
485 this.model.addToCollection = undefined;
486 }
487
488 var infoMsgId = this.app.showInformational( {what: "save " + model.modelName + " " + model.attributes.humanReadableName, status: "", statusText: "in progress..."} );
489
490 this.model.save(data, {error: function(model, result, xhr) { that.app.saveError(model,result,xhr,infoMsgId);},
Scott Baker6c19f242014-12-04 17:23:01 -0800491 success: function(model, result, xhr) { that.app.saveSuccess(model,result,xhr,infoMsgId);
492 if (that.synchronous) {
493 that.trigger("saveSuccess");
494 }
495 }});
Scott Baker5ac6abe2014-12-02 14:50:26 -0800496 this.dirty = false;
Scott Baker6c19f242014-12-04 17:23:01 -0800497
498 if (!this.synchronous) {
499 this.afterSave();
500 }
Scott Bakerc16b4c12014-11-03 23:54:24 -0800501 },
502
Scott Baker6c19f242014-12-04 17:23:01 -0800503 deleteClicked: function(e) {
504 e.preventDefault();
505 this.app.deleteDialog(this.model, "list");
Scott Baker1e87c5a2014-11-18 23:31:48 -0800506 },
507
Scott Baker9d37d562014-11-04 23:20:48 -0800508 tabClick: function(tabId, regionName) {
Scott Baker5ac6abe2014-12-02 14:50:26 -0800509 region = this.app[regionName];
510 if (this.currentTabRegion != undefined) {
511 this.currentTabRegion.$el.hide();
512 }
513 if (this.currentTabId != undefined) {
514 $(this.currentTabId).removeClass('active');
515 }
516 this.currentTabRegion = region;
517 this.currentTabRegion.$el.show();
518
519 this.currentTabId = tabId;
520 $(tabId).addClass('active');
Scott Baker9d37d562014-11-04 23:20:48 -0800521 },
522
523 showTabs: function(tabs) {
524 template = templateFromId("#xos-tabs-template", {tabs: tabs});
525 $("#tabs").html(template(tabs));
526 var that = this;
527
528 _.each(tabs, function(tab) {
529 var regionName = tab["region"];
530 var tabId = '#xos-nav-'+regionName;
531 $(tabId).bind('click', function() { that.tabClick(tabId, regionName); });
532 });
533
534 $("#tabs").show();
535 },
536
Scott Bakerc16b4c12014-11-03 23:54:24 -0800537 showLinkedItems: function() {
Scott Baker9d37d562014-11-04 23:20:48 -0800538 tabs=[];
539
540 tabs.push({name: "details", region: "detail"});
541
Scott Bakercb901812014-12-09 17:27:52 -0800542 makeFilter = function(relatedField, relatedId) {
543 return function(model) { return model.attributes[relatedField] == relatedId; }
544 };
545
Scott Baker9d37d562014-11-04 23:20:48 -0800546 var index=0;
Scott Baker5ac6abe2014-12-02 14:50:26 -0800547 for (relatedName in this.model.collection.relatedCollections) {
Scott Bakerf50222e2014-12-08 14:49:03 -0800548 var relatedField = this.model.collection.relatedCollections[relatedName];
549 var relatedId = this.model.id;
Scott Baker5ac6abe2014-12-02 14:50:26 -0800550 regionName = "linkedObjs" + (index+1);
551
552 relatedListViewClassName = relatedName + "ListView";
553 assert(this.app[relatedListViewClassName] != undefined, relatedListViewClassName + " not found");
Scott Bakerf50222e2014-12-08 14:49:03 -0800554 relatedListViewClass = this.app[relatedListViewClassName].extend({collection: xos[relatedName],
Scott Bakerca4bf922014-12-09 18:38:13 -0800555 filter: makeFilter(relatedField, relatedId),
Scott Baker07cdef02014-12-08 11:13:43 -0800556 parentModel: this.model});
Scott Baker5ac6abe2014-12-02 14:50:26 -0800557 this.app[regionName].show(new relatedListViewClass());
558 if (this.app.hideTabsByDefault) {
559 this.app[regionName].$el.hide();
560 }
561 tabs.push({name: relatedName, region: regionName});
562 index = index + 1;
563 }
564
565 while (index<4) {
566 this.app["linkedObjs" + (index+1)].empty();
567 index = index + 1;
568 }
569
570 this.showTabs(tabs);
571 this.tabClick('#xos-nav-detail', 'detail');
572 },
573
574 onFormDataInvalid: function(errors) {
575 var self=this;
576 var markErrors = function(value, key) {
577 console.log("name='" + key + "'");
578 var $inputElement = self.$el.find("[name='" + key + "']");
579 var $inputContainer = $inputElement.parent();
580 //$inputContainer.find(".help-inline").remove();
581 var $errorEl = $("<span>", {class: "help-inline error", text: value});
582 $inputContainer.append($errorEl).addClass("error");
583 }
584 _.each(errors, markErrors);
585 },
586
Scott Baker0a636cb2014-12-07 22:27:09 -0800587 templateHelpers: function() { return { modelName: this.model.modelName,
588 collectionName: this.model.collectionName,
589 addFields: this.model.addFields,
Scott Bakere68d37b2014-12-09 16:59:08 -0800590 listFields: this.model.listFields,
Scott Baker0a636cb2014-12-07 22:27:09 -0800591 detailFields: this.model.detailFields,
592 foreignFields: this.model.foreignFields,
Scott Bakere68d37b2014-12-09 16:59:08 -0800593 detailLinkFields: this.model.detailLinkFields,
Scott Baker0a636cb2014-12-07 22:27:09 -0800594 inputType: this.model.inputType,
Scott Baker07cdef02014-12-08 11:13:43 -0800595 model: this.model,
Scott Baker0a636cb2014-12-07 22:27:09 -0800596 }},
597
Scott Baker5ac6abe2014-12-02 14:50:26 -0800598});
Scott Bakerc16b4c12014-11-03 23:54:24 -0800599
600/* XOSItemView
601 This is for items that will be displayed as table rows.
602 extend with:
603 app - MarionetteApplication
604 template - template (See XOSHelper.html)
Scott Bakerc16b4c12014-11-03 23:54:24 -0800605*/
606
607XOSItemView = Marionette.ItemView.extend({
608 tagName: 'tr',
609 className: 'test-tablerow',
610
Scott Bakerc91396e2014-12-02 10:49:04 -0800611 templateHelpers: function() { return { modelName: this.model.modelName,
612 collectionName: this.model.collectionName,
Scott Bakere68d37b2014-12-09 16:59:08 -0800613 listFields: this.model.listFields,
Scott Baker0a636cb2014-12-07 22:27:09 -0800614 addFields: this.model.addFields,
615 detailFields: this.model.detailFields,
616 foreignFields: this.model.foreignFields,
Scott Bakere68d37b2014-12-09 16:59:08 -0800617 detailLinkFields: this.model.detailLinkFields,
Scott Baker0a636cb2014-12-07 22:27:09 -0800618 inputType: this.model.inputType,
Scott Baker07cdef02014-12-08 11:13:43 -0800619 model: this.model,
Scott Bakerc91396e2014-12-02 10:49:04 -0800620 }},
Scott Bakerc16b4c12014-11-03 23:54:24 -0800621});
622
623/* XOSListView:
624 extend with:
625 app - MarionetteApplication
626 childView - class of ItemView, probably an XOSItemView
627 template - template (see xosHelper.html)
628 collection - collection that holds these objects
629 title - title to display in template
630*/
631
Scott Bakerf50222e2014-12-08 14:49:03 -0800632XOSListView = FilteredCompositeView.extend({
Scott Baker5ac6abe2014-12-02 14:50:26 -0800633 childViewContainer: 'tbody',
Scott Baker07cdef02014-12-08 11:13:43 -0800634 parentModel: null,
Scott Baker5ac6abe2014-12-02 14:50:26 -0800635
636 events: {"click button.btn-xos-add": "addClicked",
637 "click button.btn-xos-refresh": "refreshClicked",
638 },
639
640 _fetchStateChange: function() {
641 if (this.collection.fetching) {
642 $("#xos-list-title-spinner").show();
643 } else {
644 $("#xos-list-title-spinner").hide();
645 }
646 },
647
Scott Baker7ce23652014-11-07 16:40:30 -0800648 addClicked: function(e) {
Scott Baker7ce23652014-11-07 16:40:30 -0800649 e.preventDefault();
Scott Bakera34d8c42014-11-11 18:02:35 -0800650 this.app.Router.navigate("add" + firstCharUpper(this.collection.modelName), {trigger: true});
Scott Baker7ce23652014-11-07 16:40:30 -0800651 },
Scott Baker5ac6abe2014-12-02 14:50:26 -0800652
653 refreshClicked: function(e) {
654 e.preventDefault();
655 this.collection.refresh(refreshRelated=true);
656 },
657
658 initialize: function() {
659 this.listenTo(this.collection, 'change', this._renderChildren)
Scott Bakerf50222e2014-12-08 14:49:03 -0800660 this.listenTo(this.collection, 'sort', function() { console.log("sort"); })
661 this.listenTo(this.collection, 'add', function() { console.log("add"); })
Scott Baker13e6f0d2014-11-18 17:02:07 -0800662 this.listenTo(this.collection, 'fetchStateChange', this._fetchStateChange);
Scott Bakerc16b4c12014-11-03 23:54:24 -0800663
664 // Because many of the templates use idToName(), we need to
665 // listen to the collections that hold the names for the ids
666 // that we want to display.
667 for (i in this.collection.foreignCollections) {
668 foreignName = this.collection.foreignCollections[i];
669 if (xos[foreignName] == undefined) {
670 console.log("Failed to find xos class " + foreignName);
671 }
672 this.listenTo(xos[foreignName], 'change', this._renderChildren);
673 this.listenTo(xos[foreignName], 'sort', this._renderChildren);
674 }
675 },
676
Scott Baker07cdef02014-12-08 11:13:43 -0800677 getAddChildHash: function() {
678 if (this.parentModel) {
Scott Bakercb901812014-12-09 17:27:52 -0800679 parentFieldName = this.parentModel.relatedCollections[this.collection.collectionName];
680 parentFieldName = parentFieldName || "unknown";
681
682 /*parentFieldName = "unknown";
683
Scott Baker07cdef02014-12-08 11:13:43 -0800684 for (fieldName in this.collection.foreignFields) {
685 cname = this.collection.foreignFields[fieldName];
686 if (cname = this.collection.collectionName) {
687 parentFieldName = fieldName;
688 }
Scott Bakercb901812014-12-09 17:27:52 -0800689 }*/
Scott Baker07cdef02014-12-08 11:13:43 -0800690 return "#addChild" + firstCharUpper(this.collection.modelName) + "/" + this.parentModel.modelName + "/" + parentFieldName + "/" + this.parentModel.id; // modelName, fieldName, id
691 } else {
692 return null;
693 }
694 },
695
Scott Bakerc16b4c12014-11-03 23:54:24 -0800696 templateHelpers: function() {
Scott Baker07cdef02014-12-08 11:13:43 -0800697 return { title: this.title,
Scott Bakere68d37b2014-12-09 16:59:08 -0800698 addChildHash: this.getAddChildHash(),
699 foreignFields: this.collection.foreignFields,
700 listFields: this.collection.listFields,
701 detailLinkFields: this.collection.detailLinkFields, };
Scott Baker5ac6abe2014-12-02 14:50:26 -0800702 },
Scott Bakerc16b4c12014-11-03 23:54:24 -0800703});
704
Scott Baker660b9e02014-12-11 02:27:04 -0800705XOSDataTableView = Marionette.View.extend( {
Scott Bakereb2565f2014-12-12 00:18:11 -0800706 el: '<div style="overflow: hidden">' +
707 '<h3 class="xos-list-title title_placeholder"></h3>' +
708 '<div class="header_placeholder"></div>' +
709 '<table></table>' +
710 '<div class="footer_placeholder"></div>' +
711 '</div>',
Scott Baker660b9e02014-12-11 02:27:04 -0800712
713 filter: undefined,
714
Scott Bakereb2565f2014-12-12 00:18:11 -0800715 events: {"click button.btn-xos-add": "addClicked",
716 "click button.btn-xos-refresh": "refreshClicked",
717 },
718
719 _fetchStateChange: function() {
720 if (this.collection.fetching) {
721 $("#xos-list-title-spinner").show();
722 } else {
723 $("#xos-list-title-spinner").hide();
724 }
725 },
726
727 addClicked: function(e) {
728 e.preventDefault();
729 this.app.Router.navigate("add" + firstCharUpper(this.collection.modelName), {trigger: true});
730 },
731
732 refreshClicked: function(e) {
733 e.preventDefault();
734 this.collection.refresh(refreshRelated=true);
735 },
736
737
Scott Baker660b9e02014-12-11 02:27:04 -0800738 initialize: function() {
Scott Bakereb2565f2014-12-12 00:18:11 -0800739 $(this.el).find(".footer_placeholder").html( xosListFooterTemplate({addChildHash: this.getAddChildHash()}) );
740 $(this.el).find(".header_placeholder").html( xosListHeaderTemplate() );
741
742 this.listenTo(this.collection, 'fetchStateChange', this._fetchStateChange);
Scott Baker660b9e02014-12-11 02:27:04 -0800743 },
744
745 render: function() {
746 var view = this;
747
Scott Bakera0473362014-12-11 23:08:31 -0800748 view.columnsByIndex = [];
749 view.columnsByFieldName = {};
Scott Baker660b9e02014-12-11 02:27:04 -0800750 _.each(this.collection.listFields, function(fieldName) {
751 mRender = undefined;
Scott Bakera0473362014-12-11 23:08:31 -0800752 mSearchText = undefined;
Scott Baker660b9e02014-12-11 02:27:04 -0800753 if (fieldName in view.collection.foreignFields) {
754 var foreignCollection = view.collection.foreignFields[fieldName];
Scott Bakera0473362014-12-11 23:08:31 -0800755 mSearchText = function(x) { return idToName(x, foreignCollection, "humanReadableName"); };
756 }
757 if ($.inArray(fieldName, view.collection.detailLinkFields)>=0) {
Scott Baker660b9e02014-12-11 02:27:04 -0800758 var collectionName = view.collection.collectionName;
759 mRender = function(x,y,z) { return '<a href="#' + collectionName + '/' + z.id + '">' + x + '</a>'; };
760 }
Scott Bakera0473362014-12-11 23:08:31 -0800761 thisColumn = {sTitle: fieldNameToHumanReadable(fieldName), mData: fieldName, mRender: mRender, mSearchText: mSearchText};
762 view.columnsByIndex.push( thisColumn );
763 view.columnsByFieldName[fieldName] = thisColumn;
Scott Baker660b9e02014-12-11 02:27:04 -0800764 });
765
Scott Bakereb2565f2014-12-12 00:18:11 -0800766 deleteColumn = {sTitle: "delete", mRender: function(x,y,z) { return xosDeleteButtonTemplate({modelName: view.collection.modelName, id: z.id}); }, mData: function() { return "delete"; }};
767 view.columnsByIndex.push(deleteColumn);
768 view.columnsByFieldName["delete"] = deleteColumn;
769
Scott Baker660b9e02014-12-11 02:27:04 -0800770 oTable = $(this.el).find("table").dataTable( {
771 "bJQueryUI": true,
772 "bStateSave": true,
773 "bServerSide": true,
Scott Bakera0473362014-12-11 23:08:31 -0800774 "aoColumns": view.columnsByIndex,
Scott Baker660b9e02014-12-11 02:27:04 -0800775
776 fnServerData: function(sSource, aoData, fnCallback, settings) {
777 var compareColumns = function(sortCols, sortDirs, a, b) {
Scott Bakera0473362014-12-11 23:08:31 -0800778 a = a[sortCols[0]];
779 b = b[sortCols[0]];
780 result = (a==b) ? 0 : ((a<b) ? -1 : 1);
Scott Baker660b9e02014-12-11 02:27:04 -0800781 if (sortDirs[0] == "desc") {
782 result = -result;
783 }
784 return result;
785 };
Scott Bakera0473362014-12-11 23:08:31 -0800786
787 var searchMatch = function(row, sSearch) {
788 for (fieldName in row) {
789 if (fieldName in view.columnsByFieldName) {
790 try {
791 value = row[fieldName].toString();
792 } catch(e) {
793 continue;
794 }
795 if (value.indexOf(sSearch) >= 0) {
796 return true;
797 }
798 }
799 }
800 return false;
801 };
802
Scott Bakereb2565f2014-12-12 00:18:11 -0800803 //console.log(aoData);
Scott Baker660b9e02014-12-11 02:27:04 -0800804
805 // function used to populate the DataTable with the current
806 // content of the collection
807 var populateTable = function()
808 {
Scott Bakereb2565f2014-12-12 00:18:11 -0800809 console.log("populatetable!");
810
Scott Baker660b9e02014-12-11 02:27:04 -0800811 // clear out old row views
812 rows = [];
813
Scott Bakera0473362014-12-11 23:08:31 -0800814 sSearch = null;
815 iDisplayStart = 0;
816 iDisplayLength = 1000;
Scott Baker660b9e02014-12-11 02:27:04 -0800817 sortDirs = [];
818 sortCols = [];
819 _.each(aoData, function(param) {
820 if (param.name == "sSortDir_0") {
821 sortDirs = [param.value];
822 } else if (param.name == "iSortCol_0") {
Scott Bakera0473362014-12-11 23:08:31 -0800823 sortCols = [view.columnsByIndex[param.value].mData];
824 } else if (param.name == "iDisplayStart") {
825 iDisplayStart = param.value;
826 } else if (param.name == "iDisplayLength") {
827 iDisplayLength = param.value;
828 } else if (param.name == "sSearch") {
829 sSearch = param.value;
Scott Baker660b9e02014-12-11 02:27:04 -0800830 }
831 });
832
833 aaData = view.collection.toJSON();
834
Scott Bakera0473362014-12-11 23:08:31 -0800835 // apply backbone filtering on the models
Scott Baker660b9e02014-12-11 02:27:04 -0800836 if (view.filter) {
837 aaData = aaData.filter( function(row) { model = {}; model.attributes = row; return view.filter(model); } );
838 }
839
Scott Bakera0473362014-12-11 23:08:31 -0800840 var totalSize = aaData.length;
841
842 // turn the ForeignKey fields into human readable things
843 for (rowIndex in aaData) {
844 row = aaData[rowIndex];
845 for (fieldName in row) {
846 if (fieldName in view.columnsByFieldName) {
847 mSearchText = view.columnsByFieldName[fieldName].mSearchText;
848 if (mSearchText) {
849 row[fieldName] = mSearchText(row[fieldName]);
850 }
851 }
852 }
853 }
854
855 // apply datatables search
856 if (sSearch) {
857 aaData = aaData.filter( function(row) { return searchMatch(row, sSearch); });
858 }
859
860 var filteredSize = aaData.length;
861
862 // apply datatables sort
Scott Baker660b9e02014-12-11 02:27:04 -0800863 aaData.sort(function(a,b) { return compareColumns(sortCols, sortDirs, a, b); });
864
Scott Bakera0473362014-12-11 23:08:31 -0800865 // slice it for pagination
866 aaData = aaData.slice(iDisplayStart, iDisplayStart+iDisplayLength);
Scott Baker660b9e02014-12-11 02:27:04 -0800867
868 return fnCallback({iTotalRecords: totalSize,
869 iTotalDisplayRecords: filteredSize,
870 aaData: aaData});
871 };
872
873 aoData.shift(); // ignore sEcho
874 populateTable();
Scott Bakereb2565f2014-12-12 00:18:11 -0800875
876 view.listenTo(view.collection, 'change', populateTable);
877 view.listenTo(view.collection, 'add', populateTable);
878 view.listenTo(view.collection, 'remove', populateTable);
Scott Baker660b9e02014-12-11 02:27:04 -0800879 },
880 } );
881
882 return this;
883 },
884
Scott Bakereb2565f2014-12-12 00:18:11 -0800885 getAddChildHash: function() {
886 if (this.parentModel) {
887 parentFieldName = this.parentModel.relatedCollections[this.collection.collectionName];
888 parentFieldName = parentFieldName || "unknown";
889
890 /*parentFieldName = "unknown";
891
892 for (fieldName in this.collection.foreignFields) {
893 cname = this.collection.foreignFields[fieldName];
894 if (cname = this.collection.collectionName) {
895 parentFieldName = fieldName;
896 }
897 }*/
898 return "#addChild" + firstCharUpper(this.collection.modelName) + "/" + this.parentModel.modelName + "/" + parentFieldName + "/" + this.parentModel.id; // modelName, fieldName, id
899 } else {
900 return null;
901 }
902 },
903
Scott Baker660b9e02014-12-11 02:27:04 -0800904});
905
Scott Bakerfdaee922014-11-03 09:43:23 -0800906idToName = function(id, collectionName, fieldName) {
Scott Baker35935202014-12-08 21:35:06 -0800907 return xos.idToName(id, collectionName, fieldName);
Scott Bakerfdaee922014-11-03 09:43:23 -0800908};
909
910/* Constructs lists of <option> html blocks for items in a collection.
911
912 selectedId = the id of an object that should be selected, if any
913 collectionName = name of collection
914 fieldName = name of field within models of collection that will be displayed
915*/
916
917idToOptions = function(selectedId, collectionName, fieldName) {
918 result=""
919 for (index in xos[collectionName].models) {
920 linkedObject = xos[collectionName].models[index];
921 linkedId = linkedObject["id"];
922 linkedName = linkedObject.attributes[fieldName];
923 if (linkedId == selectedId) {
924 selected = " selected";
925 } else {
926 selected = "";
927 }
928 result = result + '<option value="' + linkedId + '"' + selected + '>' + linkedName + '</option>';
929 }
930 return result;
931};
932
933/* Constructs an html <select> and the <option>s to go with it.
934
935 variable = variable name to return to form
936 selectedId = the id of an object that should be selected, if any
937 collectionName = name of collection
938 fieldName = name of field within models of collection that will be displayed
939*/
940
Scott Baker07b4a252014-12-08 23:54:18 -0800941idToSelect = function(variable, selectedId, collectionName, fieldName, readOnly) {
942 if (readOnly) {
943 readOnly = " readonly";
944 } else {
945 readOnly = "";
946 }
947 result = '<select name="' + variable + '"' + readOnly + '>' +
Scott Bakerfdaee922014-11-03 09:43:23 -0800948 idToOptions(selectedId, collectionName, fieldName) +
949 '</select>';
Scott Bakerfdaee922014-11-03 09:43:23 -0800950 return result;
951}
952