missing comma. tsbere++
[working/Evergreen.git] / Open-ILS / web / js / dojo / openils / widget / FlattenerGrid.js
1 if (!dojo._hasResource["openils.widget.FlattenerGrid"]) {
2     dojo.provide("openils.widget.FlattenerGrid");
3
4     dojo.require("DojoSRF");
5     dojo.require("dojox.grid.DataGrid");
6     dojo.require("openils.FlattenerStore");
7     dojo.require("openils.PermaCrud");
8     dojo.require("openils.widget.GridColumnPicker");
9     dojo.require("openils.widget.EditDialog");  /* includes EditPane */
10     dojo.require("openils.widget._GridHelperColumns");
11
12     dojo.declare(
13         "openils.widget.FlattenerGrid",
14         [dojox.grid.DataGrid, openils.widget._GridHelperColumns], {
15             /* These potential constructor arguments are useful to
16              * FlattenerGrid in their own right */
17             "columnReordering": true,
18             "columnPersistKey": null,
19             "showLoadFilter": false,    /* use FlattenerFilterDialog */
20
21             /* These potential constructor arguments maybe useful to
22              * FlattenerGrid in their own right, and are passed to
23              * FlattenerStore. */
24             "fmClass": null,
25             "fmIdentifier": null,
26             "mapExtras": null,
27             "defaultSort": null,  /* whatever any part of the UI says will
28                                      /replace/ this */
29             "baseSort": null,     /* will contains what the columnpicker
30                                      dictates, and precedes whatever the column
31                                      headers provide. */
32
33             /* These potential constructor arguments are for functionality
34              * copied from AutoGrid */
35             "editOnEnter": false,       /* also implies edit-on-dblclick */
36             "editStyle": "dialog",      /* "dialog" or "pane" */
37             "requiredFields": null,     /* affects create/edit dialogs */
38             "suppressEditFields": null, /* affects create/edit dialogs */
39
40             /* _generateMap() lives to interpret the attributes of the
41              * FlattenerGrid dijit itself plus those definined in
42              * <table>
43              *  <thead>
44              *   <tr>
45              *    <th field="foo" ...>
46              * to build the map to hand to the FlattenerStore, which in turn
47              * uses it to query the flattener service.
48              */
49             "_generateMap": function() {
50                 var map = this.mapClause = {};
51                 var fields = this.structure[0].cells[0];
52
53                 /* These are the fields defined in thead -> tr -> [th,th,...].
54                  * For purposes of building the map, where each field has
55                  * three boolean attributes "display", "sort" and "filter",
56                  * assume "display" and "sort" are always true for these.
57                  * That doesn't mean that at the UI level we can't hide a
58                  * column later.
59                  *
60                  * If you need extra fields in the map for which display
61                  * or sort should *not* be true, use mapExtras.
62                  */
63                 dojo.forEach(
64                     fields, function(field) {
65                         if (field.field.match(/^\+/))
66                             return; /* special fields e.g. checkbox/line # */
67
68                         map[field.field] = {
69                             "display": true,
70                             "filter": (field.ffilter || false),
71                             "sort": true,
72                             "path": field.fpath || field.field
73                         };
74                         /* The following attribute is not for the flattener
75                          * service's benefit, but for other uses. We capture
76                          * the hardcoded <th> value (the header label) if any.*/
77                         if (field.name)
78                             map[field.field]._label = field.name;
79                     }
80                 );
81
82                 if (this.mapExtras) {
83                     /* It's not particularly useful to add simple fields, i.e.
84                      *  circ_lib: "circ_lib.name"
85                      * to mapExtras, because by convention used elsewhere in
86                      * Flattener, that gives all attributes, including
87                      * display, a true value. Still, be consistent to avoid
88                      * stumping users.
89                      */
90                     for (var key in this.mapExtras) {
91                         if (typeof this.mapExtras[key] != "object") {
92                             this.mapExtras[key] = {
93                                 "path": this.mapExtras[key],
94                                 "sort": true,
95                                 "filter": true,
96                                 "display": true
97                             };
98                         }
99                     }
100                     dojo.mixin(map, this.mapExtras);
101                 }
102
103                 /* Do this now, since we don't want a silently added
104                  * identifier attribute in the terminii list (see its uses). */
105                 this._calculateMapTerminii();
106                 this._supplementHeaderNames();
107
108                 /* make sure we always have a field for fm identifier */
109                 if (!map[this.fmIdentifier]) {
110                     map[this.fmIdentifier] = {
111                         "path": this.fmIdentifier,
112                         "display": true,    /* Flattener displays it to us,
113                                                but we don't display to user. */
114                         "sort": false,
115                         "filter": true
116                     };
117                 }
118
119                 return map;
120             },
121
122             "_cleanMapForStore": function(map) {
123                 var clean = dojo.clone(map);
124
125                 for (var column in clean) {
126                     openils.Util.objectProperties(clean[column]).filter(
127                         function(k) { return k.match(/^_/); }
128                     ).forEach(
129                         function(k) { delete clean[column][k]; }
130                     );
131                 }
132
133                 return clean;
134             },
135
136             /* The FlattenerStore doesn't need this, but it has at least two
137              * uses: 1) FlattenerFilterDialog, 2) setting column header labels
138              * to IDL defaults.
139              *
140              * To call these 'Terminii' can be misleading. In certain
141              * (actually probably common) cases, they won't really be the last
142              * field in a path, but the next-to-last. Read on. */
143             "_calculateMapTerminii": function() {
144                 function _fm_is_selector_for_class(hint, field) {
145                     var cl = fieldmapper.IDL.fmclasses[hint];
146                     return (cl.field_map[cl.pkey].selector == field);
147                 }
148
149                 function _follow_to_end(hint, path) {
150                     var last_field, last_hint;
151                     var orig_path = dojo.clone(path);
152                     var field;
153
154                     while (field = path.shift()) {
155                         /* XXX this assumes we have the whole IDL loaded. I
156                          * guess we could teach this to work by loading classes
157                          * on demand when we don't have the whole IDL loaded. */
158                         var field_def =
159                             fieldmapper.IDL.fmclasses[hint].field_map[field];
160
161                         if (field_def["class"] && path.length) {
162                             last_field = field;
163                             last_hint = hint;
164
165                             hint = field_def["class"];
166                         } else if (path.length) {
167                             /* There are more fields left but we can't follow
168                              * the chain via IDL any further. */
169                             throw new Error(
170                                 "_calculateMapTerminii can't parse path " +
171                                 orig_path + " (at " + field + ")"
172                             );
173                         } else {
174                             break;  /* keeps field defined after loop */
175                         }
176                     }
177
178                     var datatype = field_def.datatype;
179                     var indirect = false;
180                     /* Back off the last field in the path if it's a selector
181                      * for its class, because the preceding field will be
182                      * a better thing to hand to AutoFieldWidget.
183                      */
184                     if (orig_path.length > 1 &&
185                             _fm_is_selector_for_class(hint, field)) {
186                         hint = last_hint;
187                         field = last_field;
188                         datatype = "link";
189                         indirect = true;
190                     }
191
192                     return {
193                         "fmClass": hint,
194                         "name": field,
195                         "label": field_def.label,
196                         "datatype": datatype,
197                         "indirect": indirect
198                     };
199                 }
200
201                 this.mapTerminii = [];
202                 for (var column in this.mapClause) {
203                     var terminus = dojo.mixin(
204                         _follow_to_end(
205                             this.fmClass,
206                             this.mapClause[column].path.split(/\./)
207                         ), {
208                             "simple_name": column,
209                             "isfilter": this.mapClause[column].filter
210                         }
211                     );
212                     if (this.mapClause[column]._label)
213                         terminus.label = this.mapClause[column]._label;
214
215                     this.mapTerminii.push(terminus);
216                 }
217             },
218
219             "_supplementHeaderNames": function() {
220                 /* You'd be surprised how rarely this make sense in Flattener
221                  * use cases, but if we didn't give a particular header cell
222                  * (<th>) a display name (the innerHTML of that <th>), then
223                  * use the IDL to provide the label of the terminus of the
224                  * flattener path for that column. It may be better than using
225                  * the raw field name. */
226                 var self = this;
227                 this.structure[0].cells[0].forEach(
228                     function(header) {
229                         if (!header.name) {
230                             header.name = self.mapTerminii.filter(
231                                 function(t) {
232                                     return t.simple_name == header.field;
233                                 }
234                             )[0].label;
235                         }
236                     }
237                 );
238             },
239
240             "constructor": function(args) {
241                 dojo.mixin(this, args);
242
243                 this.fmIdentifier = this.fmIdentifier ||
244                     fieldmapper.IDL.fmclasses[this.fmClass].pkey;
245             },
246
247             "startup": function() {
248
249                 /* Save original query for further filtering later */
250                 this._baseQuery = dojo.clone(this.query);
251                 this._startupGridHelperColumns();
252
253                 if (!this.columnPicker) {
254                     this.columnPicker =
255                         new openils.widget.GridColumnPicker(
256                             null, this.columnPersistKey, this);
257                     this.columnPicker.onLoad = dojo.hitch(
258                         this, function(opts) { this._finishStartup(opts.sortFields) });
259
260                     this.columnPicker.onSortChange = dojo.hitch(this,
261                         /* directly after, this.update() is called by the
262                            column picker, causing a re-fetch */
263                         function(fields) {
264                             this.store.baseSort = this._mapCPSortFields(fields)
265                         }
266                     );
267
268                     this.columnPicker.load();
269                 }
270
271                 this.inherited(arguments);
272             },
273
274             /*  Maps ColumnPicker sort fields to the correct format.
275                 If no sort fields specified, falls back to defaultSort */
276             "_mapCPSortFields": function(sortFields) {
277                 var sort = this.defaultSort;
278                 if (sortFields.length) {
279                     sort = sortFields.map(function(f) {
280                         a = {};
281                         a[f.field] = f.direction;
282                         return a;
283                     });
284                 }
285                 return sort;
286             },
287
288             "_finishStartup": function(sortFields) {
289
290                 this.setStore(
291                     new openils.FlattenerStore({
292                         "fmClass": this.fmClass,
293                         "fmIdentifier": this.fmIdentifier,
294                         "mapClause": (this.mapClause ||
295                             this._cleanMapForStore(this._generateMap())),
296                         "baseSort": this.baseSort,
297                         "defaultSort": this._mapCPSortFields(sortFields)
298                     }), this.query
299                 );
300
301                 this._showing_create_pane = false;
302
303                 this.overrideEditWidgets = {};
304                 this.overrideEditWidgetClass = {};
305                 this.overrideWidgetArgs = {};
306
307                 if (this.editOnEnter)
308                     this._applyEditOnEnter();
309                 else if (this.singleEditStyle)
310                     this._applySingleEditStyle();
311
312                 /* Like AutoGrid's paginator, but we'll never have Back/Next
313                  * links.  Just a place to hold misc links */
314                 this._setupLinks();
315             },
316
317
318             "_setupLinks": function() {
319                 this.linkHolder = new dijit.layout.ContentPane();
320                 dojo.place(this.linkHolder.domNode, this.domNode, "before");
321
322                 if (this.showLoadFilter) {
323                     dojo.require("openils.widget.FlattenerFilterDialog");
324                     this.filterDialog =
325                         new openils.widget.FlattenerFilterDialog({
326                             "fmClass": this.fmClass,
327                             "mapTerminii": this.mapTerminii
328                         });
329
330                     this.filterDialog.onApply = dojo.hitch(
331                         this, function(filter) {
332                             this.filter(
333                                 dojo.mixin(filter, this._baseQuery),
334                                 true    /* re-render */
335                             );
336                         }
337                     );
338
339                     this.filterDialog.startup();
340                     dojo.create(
341                         "a", {
342                             "innerHTML": "Filter",  /* XXX i18n */
343                             "href": "javascript:void(0);",
344                             "onclick": dojo.hitch(this, function() {
345                                 this.filterDialog.show();
346                             })
347                         }, this.linkHolder.domNode
348                     );
349                 }
350             },
351
352             /* ******** below are methods mostly copied but
353              * slightly changed from AutoGrid ******** */
354
355             "_applySingleEditStyle": function() {
356                 this.onMouseOverRow = function(e) {};
357                 this.onMouseOutRow = function(e) {};
358                 this.onCellFocus = function(cell, rowIndex) {
359                     this.selection.deselectAll();
360                     this.selection.select(this.focus.rowIndex);
361                 };
362             },
363
364             /* capture keydown and launch edit dialog on enter */
365             "_applyEditOnEnter": function() {
366                 this._applySingleEditStyle();
367
368                 dojo.connect(
369                     this, "onRowDblClick", function(e) {
370                         if (this.editStyle == "pane")
371                             this._drawEditPane(
372                                 this.selection.getFirstSelected(),
373                                 this.focus.rowIndex
374                             );
375                         else
376                             this._drawEditDialog(
377                                 this.selection.getFirstSelected(),
378                                 this.focus.rowIndex
379                             );
380                     }
381                 );
382
383                 dojo.connect(
384                     this, "onKeyDown", function(e) {
385                         if (e.keyCode == dojo.keys.ENTER) {
386                             this.selection.deselectAll();
387                             this.selection.select(this.focus.rowIndex);
388                             if (this.editStyle == "pane")
389                                 this._drawEditPane(
390                                     this.selection.getFirstSelected(),
391                                     this.focus.rowIndex
392                                 );
393                             else
394                                 this._drawEditDialog(
395                                     this.selection.getFirstSelected(),
396                                     this.focus.rowIndex
397                                 );
398                         }
399                     }
400                 );
401             },
402
403             "_makeEditPane": function(storeItem, rowIndex, onPostSubmit, onCancel) {
404                 var grid = this;
405                 var fmObject = (new openils.PermaCrud()).retrieve(
406                     this.fmClass,
407                     this.store.getIdentity(storeItem)
408                 );
409
410                 var pane = new openils.widget.EditPane({
411                     "fmObject": fmObject,
412                     "hideSaveButton": this.editReadOnly,
413                     "readOnly": this.editReadOnly,
414                     "overrideWidgets": this.overrideEditWidgets,
415                     "overrideWidgetClass": this.overrideEditWidgetClass,
416                     "overrideWidgetArgs": this.overrideWidgetArgs,
417                     "disableWidgetTest": this.disableWidgetTest,
418                     "requiredFields": this.requiredFields,
419                     "suppressFields": this.suppressEditFields,
420                     "onPostSubmit": function() {
421                         /* ask the store to call flattener specially to get
422                          * the flat row related to only this fmobj */
423                         grid.store.loadItem({"force": true, "item": storeItem});
424
425                         if (grid.onPostUpdate)
426                             grid.onPostUpdate(storeItem, rowIndex);
427
428                         setTimeout(
429                             function() {
430                                 try {
431                                     grid.views.views[0].getCellNode(
432                                         rowIndex, 0
433                                     ).focus();
434                                 } catch (E) { }
435                             }, 200
436                         );
437                         if (onPostSubmit)
438                             onPostSubmit();
439                     },
440                     "onCancel": function() {
441                         setTimeout(
442                             function() {
443                                 grid.views.views[0].getCellNode(
444                                     rowIndex, 0
445                                 ).focus();
446                             }, 200
447                         );
448                         if (onCancel)
449                             onCancel();
450                     }
451                 });
452
453                 if (typeof this.editPaneOnSubmit == "function")
454                     pane.onSubmit = this.editPaneOnSubmit;
455
456                 pane.fieldOrder = this.fieldOrder;
457                 pane.mode = "update";
458                 return pane;
459             },
460
461             "_makeCreatePane": function(onPostSubmit, onCancel) {
462                 var grid = this;
463                 var pane = new openils.widget.EditPane({
464                     "fmClass": this.fmClass,
465                     "overrideWidgets": this.overrideEditWidgets,
466                     "overrideWidgetClass": this.overrideEditWidgetClass,
467                     "overrideWidgetArgs": this.overrideWidgetArgs,
468                     "disableWidgetTest": this.disableWidgetTest,
469                     "requiredFields": this.requiredFields,
470                     "suppressFields": this.suppressEditFields,
471                     "onPostSubmit": function(req, cudResults) {
472                         var fmObject = cudResults[0];
473                         if (grid.onPostCreate)
474                             grid.onPostCreate(fmObject);
475                         if (fmObject) {
476                             grid.store.fetchItemByIdentity({
477                                 "identity": fmObject[grid.fmIdentifier](),
478                                 "onItem": function(item) {
479                                     grid.store.onNew(item);
480                                 }
481                             });
482                         }
483
484                         setTimeout(
485                             function() {
486                                 try {
487                                     grid.selection.select(grid.rowCount - 1);
488                                     grid.views.views[0].getCellNode(
489                                         grid.rowCount - 1, 1
490                                     ).focus();
491                                 } catch (E) { }
492                             }, 200
493                         );
494
495                         if (onPostSubmit)
496                             onPostSubmit(fmObject);
497                     },
498                     "onCancel": function() { if (onCancel) onCancel(); }
499                 });
500
501                 if (typeof this.createPaneOnSubmit == "function")
502                     pane.onSubmit = this.createPaneOnSubmit;
503                 pane.fieldOrder = this.fieldOrder;
504                 pane.mode = "create";
505                 return pane;
506             },
507
508             /**
509              * Creates an EditPane with a copy of the data from the provided store
510              * item for cloning said item
511              * @param {Object} storeItem Dojo data item
512              * @param {Number} rowIndex The Grid row index of the item to be cloned
513              * @param {Function} onPostSubmit Optional callback for post-submit behavior
514              * @param {Function} onCancel Optional callback for clone cancelation
515              * @return {Object} The clone EditPane
516              */
517             "_makeClonePane": function(storeItem,rowIndex,onPostSubmit,onCancel) {
518                 var clonePane = this._makeCreatePane(onPostSubmit, onCancel);
519                 var origPane = this._makeEditPane(storeItem, rowIndex);
520                 clonePane.startup();
521                 origPane.startup();
522                 dojo.forEach(
523                     origPane.fieldList, function(field) {
524                         if (field.widget.widget.attr('disabled'))
525                             return;
526
527                         var w = clonePane.fieldList.filter(
528                             function(i) { return (i.name == field.name) }
529                         )[0];
530
531                         // sync widgets
532                         w.widget.baseWidgetValue(field.widget.widget.attr('value'));
533
534                         // async widgets
535                         w.widget.onload = function() {
536                             w.widget.baseWidgetValue(
537                                 field.widget.widget.attr('value')
538                             )
539                         };
540                     }
541                 );
542                 origPane.destroy();
543                 return clonePane;
544             },
545
546
547             "_drawEditDialog": function(storeItem, rowIndex) {
548                 var done = dojo.hitch(this, function() { this.hideDialog(); });
549                 var pane = this._makeEditPane(storeItem, rowIndex, done, done);
550                 this.editDialog = new openils.widget.EditDialog({editPane:pane});
551                 this.editDialog.startup();
552                 this.editDialog.show();
553             },
554
555             /**
556              * Generates an EditDialog for object creation and displays it to the user
557              */
558             "showCreateDialog": function() {
559                 var done = dojo.hitch(this, function() { this.hideDialog(); });
560                 var pane = this._makeCreatePane(done, done);
561                 this.editDialog = new openils.widget.EditDialog({editPane:pane});
562                 this.editDialog.startup();
563                 this.editDialog.show();
564             },
565
566             "_drawEditPane": function(storeItem, rowIndex) {
567                 var done = dojo.hitch(this, function() { this.hidePane(); });
568
569                 dojo.style(this.domNode, "display", "none");
570
571                 this.editPane = this._makeEditPane(storeItem, rowIndex, done, done);
572                 this.editPane.startup();
573                 dojo.place(this.editPane.domNode, this.domNode, "before");
574
575                 if (this.onEditPane)
576                     this.onEditPane(this.editPane);
577             },
578
579             "showClonePane": function(onPostSubmit) {
580                 var done = dojo.hitch(this, function() { this.hidePane(); });
581                 var row = this.getFirstSelectedRow();
582
583                 if (!row)
584                     return;
585
586                 if (onPostSubmit) {
587                     postSubmit = dojo.hitch(
588                         this, function(result) {
589                             onPostSubmit(this.getItem(row), result);
590                             this.hidePane();
591                         }
592                     );
593                 } else {
594                     postSubmit = done;
595                 }
596
597                 dojo.style(this.domNode, "display", "none");
598                 this.editPane = this._makeClonePane(
599                     this.getItem(row), row, postSubmit, done
600                 );
601                 dojo.place(this.editPane.domNode, this.domNode, "before");
602                 if (this.onEditPane)
603                     this.onEditPane(this.editPane);
604             },
605
606             "showCreatePane": function() {
607                 if (this._showing_create_pane)
608                     return;
609                 this._showing_create_pane = true;
610
611                 var done = dojo.hitch(
612                     this, function() {
613                         this._showing_create_pane = false;
614                         this.hidePane();
615                     }
616                 );
617
618                 dojo.style(this.domNode, "display", "none");
619
620                 this.editPane = this._makeCreatePane(done, done);
621                 this.editPane.startup();
622
623                 dojo.place(this.editPane.domNode, this.domNode, "before");
624
625                 if (this.onEditPane)
626                     this.onEditPane(this.editPane);
627             },
628
629             "hideDialog": function() {
630                 this.editDialog.hide();
631                 this.editDialog.destroy();
632                 delete this.editDialog;
633                 this.update();
634             },
635
636             "hidePane": function() {
637                 this.domNode.parentNode.removeChild(this.editPane.domNode);
638                 this.editPane.destroy();
639                 delete this.editPane;
640                 dojo.style(this.domNode, "display", "block");
641                 this.update();
642             },
643
644             "deleteSelected": function() {
645                 var self = this;
646
647                 this.getSelectedItems().forEach(
648                     function(item) {
649                         var fmobj = new fieldmapper[self.fmClass]();
650                         fmobj[self.fmIdentifier](
651                             self.store.getIdentity(item)
652                         );
653                         (new openils.PermaCrud()).eliminate(
654                             fmobj, {
655                                 "oncomplete": function() {
656                                     self.store.deleteItem(item);
657                                 }
658                             }
659                         );
660                     }
661                 );
662             }
663         }
664     );
665
666     /* monkey patch so we can get more attributes from each column in the
667      * markup that specifies grid columns (table->thead->tr->[td,...])
668      */
669     (function() {
670         var b = dojox.grid.cells._Base;
671         var orig_mf = b.markupFactory;
672
673         b.markupFactory = function(node, cellDef) {
674             orig_mf(node, cellDef);
675
676             dojo.forEach(
677                 ["fpath", "ffilter"], function(a) {
678                     var value = dojo.attr(node, a);
679                     if (value)
680                         cellDef[a] = value;
681                 }
682             );
683         };
684     })();
685
686     /* the secret to successfully subclassing dojox.grid.DataGrid */
687     openils.widget.FlattenerGrid.markupFactory =
688         dojox.grid.DataGrid.markupFactory;
689 }