]> git.evergreen-ils.org Git - working/Evergreen.git/blob - Open-ILS/web/js/dojo/openils/widget/FlattenerGrid.js
54e67c25243e678158a04f03b95555e9bcb6862d
[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             "autoCoreFields": false,
20             "autoFieldFields": null,
21             "showLoadFilter": false,    /* use FlattenerFilter(Dialog|Pane) */
22             "filterAlwaysInDiv": null,  /* use FlattenerFilterPane and put its
23                                            content in this HTML element */
24             "fetchLock": false,
25             "filterInitializers": null,
26             "filterWidgetBuilders": null,
27             "filterSemaphore": null,
28             "filterSemaphoreCallback": null,
29             "baseQuery": null,  /* Good place to mix in data from, say, context
30                                    OU selectors so that it should get mixed
31                                    correctly with the generated query from the
32                                    filter dialog. */
33
34             /* These potential constructor arguments may be useful to
35              * FlattenerGrid in their own right, and are passed to
36              * FlattenerStore. */
37             "fmClass": null,
38             "fmIdentifier": null,
39             "mapExtras": null,
40             "sortFieldReMap": null,
41             "defaultSort": null,  /* whatever any part of the UI says will
42                                      /replace/ this */
43             "baseSort": null,     /* will contains what the columnpicker
44                                      dictates, and precedes whatever the column
45                                      headers provide. */
46
47             /* These potential constructor arguments are for functionality
48              * copied from AutoGrid */
49             "editOnEnter": false,       /* also implies edit-on-dblclick */
50             "editStyle": "dialog",      /* "dialog" or "pane" */
51             "requiredFields": null,     /* affects create/edit dialogs */
52             "suppressEditFields": null, /* affects create/edit dialogs */
53             "suppressFilterFields": null, /* affects filter dialog */
54
55             /* _generateMap() lives to interpret the attributes of the
56              * FlattenerGrid dijit itself plus those definined in
57              * <table>
58              *  <thead>
59              *   <tr>
60              *    <th field="foo" ...>
61              * to build the map to hand to the FlattenerStore, which in turn
62              * uses it to query the flattener service.
63              */
64             "_generateMap": function() {
65                 var map = this.mapClause = {};
66                 var fields = this.structure[0].cells[0];
67
68                 /* These are the fields defined in thead -> tr -> [th,th,...].
69                  * For purposes of building the map, where each field has
70                  * three boolean attributes "display", "sort" and "filter",
71                  * assume "display" is always true for these.
72                  * That doesn't mean that at the UI level we can't hide a
73                  * column later.
74                  *
75                  * If you need extra fields in the map for which display
76                  * should *not* be true, use mapExtras.
77                  */
78                 dojo.forEach(
79                     fields, function(field) {
80                         if (field.field.match(/^\+/))
81                             return; /* special fields e.g. checkbox/line # */
82
83                         map[field.field] = {
84                             "display": true,
85                             "filter": (field.ffilter || false),
86                             "sort": field.fsort,
87                             "path": field.fpath || field.field
88                         };
89                         /* The following attribute is not for the flattener
90                          * service's benefit, but for other uses. We capture
91                          * the hardcoded <th> value (the header label) if any.*/
92                         if (field.name)
93                             map[field.field]._label = field.name;
94                     }
95                 );
96
97                 if (this.mapExtras) {
98                     /* It's not particularly useful to add simple fields, i.e.
99                      *  circ_lib: "circ_lib.name"
100                      * to mapExtras, because by convention used elsewhere in
101                      * Flattener, that gives all attributes, including
102                      * display, a true value. Still, be consistent to avoid
103                      * stumping users.
104                      */
105                     for (var key in this.mapExtras) {
106                         if (typeof this.mapExtras[key] != "object") {
107                             this.mapExtras[key] = {
108                                 "path": this.mapExtras[key],
109                                 "sort": true,
110                                 "filter": true,
111                                 "display": true
112                             };
113                         }
114                     }
115                     dojo.mixin(map, this.mapExtras);
116                 }
117
118                 /* Do this now, since we don't want a silently added
119                  * identifier attribute in the terminii list (see its uses). */
120                 this._calculateMapTerminii();
121                 this._supplementHeaderNames();
122
123                 /* make sure we always have a field for fm identifier */
124                 if (!map[this.fmIdentifier]) {
125                     map[this.fmIdentifier] = {
126                         "path": this.fmIdentifier,
127                         "display": true,    /* Flattener displays it to us,
128                                                but we don't display to user. */
129                         "sort": false,
130                         "filter": true
131                     };
132                 }
133
134                 return map;
135             },
136
137             "_cleanMapForStore": function(map) {
138                 var clean = dojo.clone(map);
139
140                 for (var column in clean) {
141                     openils.Util.objectProperties(clean[column]).filter(
142                         function(k) { return k.match(/^_/); }
143                     ).forEach(
144                         function(k) { delete clean[column][k]; }
145                     );
146                 }
147
148                 return clean;
149             },
150
151             /* Given the hint of a class to start at, follow path to the end
152              * and return information on the last field.  */
153             "_followPathToEnd": function(hint, path, allow_selector_backoff) {
154                 function _fm_is_selector_for_class(h, field) {
155                     var cl = fieldmapper.IDL.fmclasses[h];
156                     return (cl.field_map[cl.pkey].selector == field);
157                 }
158
159                 var last_field, last_hint;
160                 var orig_path = dojo.clone(path);
161                 var field, field_def;
162
163                 while (field = path.shift()) {
164                     /* XXX this assumes we have the whole IDL loaded. I
165                      * guess we could teach this to work by loading classes
166                      * on demand when we don't have the whole IDL loaded. */
167                     field_def =
168                         fieldmapper.IDL.fmclasses[hint].field_map[field];
169
170                     if (!field_def) {
171                         /* This can be ok in some cases. Columns following
172                          * IDL paths involving links with a nonempty "map"
173                          * attribute can be used for display only (no
174                          * sort, no filter). */
175                         console.info(
176                             "Lost our way in IDL at hint " + hint +
177                             ", field " + field + "; may be ok"
178                         );
179                         return null;
180                     }
181
182                     if (field_def["class"]) {
183                         last_field = field;
184                         last_hint = hint;
185
186                         hint = field_def["class"];
187                     } else if (path.length) {
188                         /* There are more fields left but we can't follow
189                          * the chain via IDL any further. */
190                         throw new Error(
191                             "_calculateMapTerminii can't parse path " +
192                             orig_path + " (at " + field + ")"
193                         );
194                     }
195                 }
196
197                 var datatype = field_def.datatype;
198                 var indirect = false;
199                 /* If allowed, back off the last field in the path if it's a
200                  * selector for its class, because the preceding field will be
201                  * a better thing to hand to AutoFieldWidget.
202                  */
203                 if (orig_path.length > 1 && allow_selector_backoff &&
204                         _fm_is_selector_for_class(hint, field_def.name)) {
205                     hint = last_hint;
206                     field = last_field;
207                     datatype = "link";
208                     indirect = true;
209                 } else {
210                     field = field_def.name;
211                 }
212
213                 return {
214                     "fmClass": hint,
215                     "name": field,
216                     "label": field_def.label,
217                     "datatype": datatype,
218                     "indirect": indirect
219                 };
220             },
221
222             /* The FlattenerStore doesn't need this, but it has at least two
223              * uses: 1) FlattenerFilterDialog, 2) setting column header labels
224              * to IDL defaults.
225              *
226              * To call these 'Terminii' can be misleading. In certain
227              * (actually probably common) cases, they won't really be the last
228              * field in a path, but the next-to-last. Read on. */
229             "_calculateMapTerminii": function() {
230                 this.mapTerminii = [];
231                 for (var column in this.mapClause) {
232                     var end = this._followPathToEnd(
233                         this.fmClass,
234                         this.mapClause[column].path.split(/\./),
235                         true /* allow selector backoff */
236                     );
237                     if (!end)
238                         continue;
239                     var terminus = dojo.mixin(
240                         end, {
241                             "simple_name": column,
242                             "isfilter": this.mapClause[column].filter
243                         }
244                     );
245                     if (this.mapClause[column]._label)
246                         terminus.label = this.mapClause[column]._label;
247
248                     this.mapTerminii.push(terminus);
249                 }
250             },
251
252             "_supplementHeaderNames": function() {
253                 /* If we didn't give a particular header cell
254                  * (<th>) a display name (the innerHTML of that <th>), then
255                  * use the IDL to provide the label of the terminus of the
256                  * flattener path for that column. It may be better than using
257                  * the raw field name. */
258                 var self = this;
259                 this.structure[0].cells[0].forEach(
260                     function(header) {
261                         if (!header.name) {
262                             header.name = self.mapTerminii.filter(
263                                 function(t) {
264                                     return t.simple_name == header.field;
265                                 }
266                             )[0].label;
267                         }
268                     }
269                 );
270             },
271
272             "_columnOrderingAndLabels": function() {
273                 var labels = [];
274                 var columns = [];
275
276                 this.views.views[0].structure.cells[0].forEach(
277                     function(c) {
278                         if (!c.field.match(/^\+/)) {
279                             labels.push(c.name);
280                             columns.push(c.field);
281                         }
282                     }
283                 );
284
285                 return {"labels": labels, "columns": columns};
286             },
287
288             "_getAutoFieldFields": function(fmclass) {
289                 return dojo.clone(
290                     fieldmapper.IDL.fmclasses[fmclass].fields)
291                 .filter(
292                     function(field) {
293                         return !field.virtual && field.datatype != "link";
294                     }
295                 ).sort(
296                     function(a, b) { return a.label > b.label ? 1 : -1; }
297                 );
298             },
299
300             /* Take our core class (this.fmClass) and add table columns for
301              * any field we don't already have covered by actual hard-coded
302              * <th> columns. */
303             "_addAutoCoreFields": function() {
304                 var cell_list = this.structure[0].cells[0];
305                 var fields = dojo.clone(
306                     fieldmapper.IDL.fmclasses[this.fmClass].fields
307                 ).sort(
308                     function(a, b) { return a.label > b.label ? 1 : -1; }
309                 );
310
311                 dojo.forEach(
312                     fields, function(f) {
313                         if (f.datatype == "link" || f.virtual)
314                             return;
315
316                         if (cell_list.filter(
317                             function(c) {
318                                 if (!c.fpath) return false;
319                                 return c.fpath.split(/\./)[0] == f.name;
320                             }
321                         ).length)
322                             return;
323
324                         cell_list.push({
325                             "field": f.name,
326                             "name": f.label,
327                             "fsort": true,
328                             "_visible": false
329                         });
330                     }
331                 );
332             },
333
334             "_addAutoFieldFields": function(paths) {
335                 var self = this;
336                 var n = 0;
337
338                 dojo.forEach(
339                     paths, function(path) {
340                         /* The beginning is the end. */
341                         var beginning = self._followPathToEnd(
342                             self.fmClass, path.split(/\./), false
343                         );
344                         if (!beginning) {
345                             return;
346                         } else {
347                             dojo.forEach(
348                                 self._getAutoFieldFields(beginning.fmClass),
349                                 function(field) {
350                                     var would_be_path =
351                                         path + "." + field.name;
352                                     var wbp_re =
353                                         new RegExp("^" + would_be_path);
354                                     if (!self.structure[0].cells[0].filter(
355                                         function(c) {
356                                             return c.fpath &&
357                                                 c.fpath.match(wbp_re);
358                                         }
359                                     ).length) {
360                                         self.structure[0].cells[0].push({
361                                             "field": "AUTO_" + beginning.name +
362                                                 "_" + field.name,
363                                             "name": beginning.label + " - " +
364                                                 field.label,
365                                             "fsort": true,
366                                             "fpath": would_be_path,
367                                             "_visible": false
368                                         });
369                                     }
370                                 }
371                             );
372                         }
373                     }
374                 );
375             },
376
377             "_addAutoFields": function() {
378                 if (this.autoCoreFields)
379                     this._addAutoCoreFields();
380
381                 if (dojo.isArray(this.autoFieldFields))
382                     this._addAutoFieldFields(this.autoFieldFields);
383
384                 this.setStructure(this.structure);
385             },
386
387             "constructor": function(args) {
388                 dojo.mixin(this, args);
389
390                 this.fmIdentifier = this.fmIdentifier ||
391                     fieldmapper.IDL.fmclasses[this.fmClass].pkey;
392
393                 this.overrideEditWidgets = {};
394                 this.overrideEditWidgetClass = {};
395                 this.overrideWidgetArgs = {};
396             },
397
398             "startup": function() {
399                 /* Save original query for further filtering later, unless
400                  * we've already defined baseQuery from the outside, in
401                  * which case it persists. */
402                 if (!this.baseQuery)
403                     this.baseQuery = dojo.clone(this.query);
404
405                 this._addAutoFields();
406
407                 this._startupGridHelperColumns();
408
409                 this._generateMap();
410
411                 if (!this.columnPicker) {
412                     this.columnPicker =
413                         new openils.widget.GridColumnPicker(
414                             null, this.columnPersistKey, this);
415                     this.columnPicker.onLoad = dojo.hitch(
416                         this, function(opts) { this._finishStartup(opts.sortFields) });
417
418                     this.columnPicker.onSortChange = dojo.hitch(this,
419                         /* directly after, this.update() is called by the
420                            column picker, causing a re-fetch */
421                         function(fields) {
422                             this.store.baseSort = this._mapCPSortFields(fields)
423                         }
424                     );
425
426                     this.columnPicker.load();
427                 }
428
429                 this.inherited(arguments);
430             },
431
432             "canSort": function(idx, skip_structure /* API abuse */) {
433                 var initial = this.inherited(arguments);
434
435                 /* idx is one-based instead of zero-based for a reason. */
436                 var view_idx = Math.abs(idx) - 1;
437                 return initial && (
438                     skip_structure ||
439                         this.views.views[0].structure.cells[0][view_idx].fsort
440                 );
441             },
442
443             /*  Maps ColumnPicker sort fields to the correct format.
444                 If no sort fields specified, falls back to defaultSort */
445             "_mapCPSortFields": function(sortFields) {
446                 var sort = this.defaultSort;
447                 if (sortFields.length) {
448                     sort = sortFields.map(function(f) {
449                         a = {};
450                         a[f.field] = f.direction;
451                         return a;
452                     });
453                 }
454                 return sort;
455             },
456
457             "_finishStartup": function(sortFields) {
458
459                 this._setStore( /* Seriously, let's leave this as _setStore. */
460                     new openils.FlattenerStore({
461                         "fmClass": this.fmClass,
462                         "fmIdentifier": this.fmIdentifier,
463                         "mapClause": this._cleanMapForStore(this.mapClause),
464                         "baseSort": this.baseSort,
465                         "defaultSort": this._mapCPSortFields(sortFields),
466                         "sortFieldReMap": this.sortFieldReMap
467
468                     }), this.query
469                 );
470
471                 // pick up any column label changes
472                 this.columnPicker.reloadStructure();
473
474                 if (!this.fetchLock)
475                     this._refresh(true);
476
477                 this._showing_create_pane = false;
478
479                 if (this.editOnEnter)
480                     this._applyEditOnEnter();
481                 else if (this.singleEditStyle)
482                     this._applySingleEditStyle();
483
484                 /* Like AutoGrid's paginator, but we'll never have Back/Next
485                  * links.  Just a place to hold misc links */
486                 this._setupLinks();
487             },
488
489
490             "_setupLinks": function() {
491                 this.linkHolder = new dijit.layout.ContentPane();
492                 dojo.place(this.linkHolder.domNode, this.domNode, "before");
493
494                 if (this.showLoadFilter) {
495                     var which_filter_ui = this.filterAlwaysInDiv ?
496                         "FlattenerFilterPane" : "FlattenerFilterDialog";
497
498                     dojo.require("openils.widget." + which_filter_ui);
499                     this.filterUi =
500                         new openils.widget[which_filter_ui]({
501                             "fmClass": this.fmClass,
502                             "mapTerminii": this.mapTerminii,
503                             "useDiv": this.filterAlwaysInDiv,
504                             "compact": true,
505                             "initializers": this.filterInitializers,
506                             "widgetBuilders": this.filterWidgetBuilders,
507                             "suppressFilterFields": this.suppressFilterFields
508                         });
509
510                     this.filterUi.onApply = dojo.hitch(
511                         this, function(filter) {
512                             this.filter(
513                                 dojo.mixin(filter, this.baseQuery),
514                                 true    /* re-render */
515                             );
516                         }
517                     );
518
519                     this.filterUi.startup();
520
521                     if (this.filterSemaphore && this.filterSemaphore()) {
522                         if (this.filterSemaphoreCallback)
523                             this.filterSemaphoreCallback();
524                     }
525                     if (!this.filterAlwaysInDiv) {
526                         dojo.create(
527                             "a", {
528                                 "innerHTML": "Filter",  /* XXX i18n */
529                                 "href": "javascript:void(0);",
530                                 "onclick": dojo.hitch(this, function() {
531                                     this.filterUi.show();
532                                 })
533                             }, this.linkHolder.domNode
534                         );
535                     }
536                 }
537             },
538
539             "refresh": function() {
540                 this.fetchLock = false;
541                 this._refresh(/* isRender */ true);
542             },
543
544             "_fetch": function() {
545                 if (this.fetchLock)
546                     return;
547                 else
548                     return this.inherited(arguments);
549             },
550
551             /* ******** below are methods mostly copied but
552              * slightly changed from AutoGrid ******** */
553
554             "_applySingleEditStyle": function() {
555                 this.onMouseOverRow = function(e) {};
556                 this.onMouseOutRow = function(e) {};
557                 this.onCellFocus = function(cell, rowIndex) {
558                     this.selection.deselectAll();
559                     this.selection.select(this.focus.rowIndex);
560                 };
561             },
562
563             /* capture keydown and launch edit dialog on enter */
564             "_applyEditOnEnter": function() {
565                 this._applySingleEditStyle();
566
567                 dojo.connect(
568                     this, "onRowDblClick", function(e) {
569                         if (this.editStyle == "pane")
570                             this._drawEditPane(
571                                 this.selection.getFirstSelected(),
572                                 this.focus.rowIndex
573                             );
574                         else
575                             this._drawEditDialog(
576                                 this.selection.getFirstSelected(),
577                                 this.focus.rowIndex
578                             );
579                     }
580                 );
581
582                 dojo.connect(
583                     this, "onKeyDown", function(e) {
584                         if (e.keyCode == dojo.keys.ENTER) {
585                             this.selection.deselectAll();
586                             this.selection.select(this.focus.rowIndex);
587                             if (this.editStyle == "pane")
588                                 this._drawEditPane(
589                                     this.selection.getFirstSelected(),
590                                     this.focus.rowIndex
591                                 );
592                             else
593                                 this._drawEditDialog(
594                                     this.selection.getFirstSelected(),
595                                     this.focus.rowIndex
596                                 );
597                         }
598                     }
599                 );
600             },
601
602             "_makeEditPane": function(storeItem, rowIndex, onPostSubmit, onCancel) {
603                 var grid = this;
604                 var fmObject = (new openils.PermaCrud()).retrieve(
605                     this.fmClass,
606                     this.store.getIdentity(storeItem)
607                 );
608
609                 var pane = new openils.widget.EditPane({
610                     "fmObject": fmObject,
611                     "hideSaveButton": this.editReadOnly,
612                     "readOnly": this.editReadOnly,
613                     "overrideWidgets": this.overrideEditWidgets,
614                     "overrideWidgetClass": this.overrideEditWidgetClass,
615                     "overrideWidgetArgs": this.overrideWidgetArgs,
616                     "disableWidgetTest": this.disableWidgetTest,
617                     "requiredFields": this.requiredFields,
618                     "suppressFields": this.suppressEditFields,
619                     "onPostSubmit": function() {
620                         /* ask the store to call flattener specially to get
621                          * the flat row related to only this fmobj */
622                         grid.store.loadItem({"force": true, "item": storeItem});
623
624                         if (grid.onPostUpdate)
625                             grid.onPostUpdate(storeItem, rowIndex);
626
627                         setTimeout(
628                             function() {
629                                 try {
630                                     grid.views.views[0].getCellNode(
631                                         rowIndex, 0
632                                     ).focus();
633                                 } catch (E) { }
634                             }, 200
635                         );
636                         if (onPostSubmit)
637                             onPostSubmit();
638                     },
639                     "onCancel": function() {
640                         setTimeout(
641                             function() {
642                                 grid.views.views[0].getCellNode(
643                                     rowIndex, 0
644                                 ).focus();
645                             }, 200
646                         );
647                         if (onCancel)
648                             onCancel();
649                     }
650                 });
651
652                 if (typeof this.editPaneOnSubmit == "function")
653                     pane.onSubmit = this.editPaneOnSubmit;
654
655                 pane.fieldOrder = this.fieldOrder;
656                 pane.mode = "update";
657                 return pane;
658             },
659
660             "_makeCreatePane": function(onPostSubmit, onCancel) {
661                 var grid = this;
662                 var pane = new openils.widget.EditPane({
663                     "fmClass": this.fmClass,
664                     "overrideWidgets": this.overrideEditWidgets,
665                     "overrideWidgetClass": this.overrideEditWidgetClass,
666                     "overrideWidgetArgs": this.overrideWidgetArgs,
667                     "disableWidgetTest": this.disableWidgetTest,
668                     "requiredFields": this.requiredFields,
669                     "suppressFields": this.suppressEditFields,
670                     "onPostSubmit": function(req, cudResults) {
671                         var fmObject = cudResults[0];
672                         if (grid.onPostCreate)
673                             grid.onPostCreate(fmObject);
674                         if (fmObject) {
675                             grid.store.fetchItemByIdentity({
676                                 "identity": fmObject[grid.fmIdentifier](),
677                                 "onItem": function(item) {
678                                     grid.store.onNew(item);
679                                 }
680                             });
681                         }
682
683                         setTimeout(
684                             function() {
685                                 try {
686                                     grid.selection.select(grid.rowCount - 1);
687                                     grid.views.views[0].getCellNode(
688                                         grid.rowCount - 1, 1
689                                     ).focus();
690                                 } catch (E) { }
691                             }, 200
692                         );
693
694                         if (onPostSubmit)
695                             onPostSubmit(fmObject);
696                     },
697                     "onCancel": function() { if (onCancel) onCancel(); }
698                 });
699
700                 if (typeof this.createPaneOnSubmit == "function")
701                     pane.onSubmit = this.createPaneOnSubmit;
702                 pane.fieldOrder = this.fieldOrder;
703                 pane.mode = "create";
704                 return pane;
705             },
706
707             /**
708              * Creates an EditPane with a copy of the data from the provided store
709              * item for cloning said item
710              * @param {Object} storeItem Dojo data item
711              * @param {Number} rowIndex The Grid row index of the item to be cloned
712              * @param {Function} onPostSubmit Optional callback for post-submit behavior
713              * @param {Function} onCancel Optional callback for clone cancelation
714              * @return {Object} The clone EditPane
715              */
716             "_makeClonePane": function(storeItem,rowIndex,onPostSubmit,onCancel) {
717                 var clonePane = this._makeCreatePane(onPostSubmit, onCancel);
718                 var origPane = this._makeEditPane(storeItem, rowIndex);
719                 clonePane.startup();
720                 origPane.startup();
721                 dojo.forEach(
722                     origPane.fieldList, function(field) {
723                         if (field.widget.widget.attr('disabled'))
724                             return;
725
726                         var w = clonePane.fieldList.filter(
727                             function(i) { return (i.name == field.name) }
728                         )[0];
729
730                         // sync widgets
731                         w.widget.baseWidgetValue(field.widget.widget.attr('value'));
732
733                         // async widgets
734                         w.widget.onload = function() {
735                             w.widget.baseWidgetValue(
736                                 field.widget.widget.attr('value')
737                             )
738                         };
739                     }
740                 );
741                 origPane.destroy();
742                 return clonePane;
743             },
744
745
746             "_drawEditDialog": function(storeItem, rowIndex) {
747                 var done = dojo.hitch(this, function() { this.hideDialog(); });
748                 var pane = this._makeEditPane(storeItem, rowIndex, done, done);
749                 this.editDialog = new openils.widget.EditDialog({editPane:pane});
750                 this.editDialog.startup();
751                 this.editDialog.show();
752             },
753
754             /**
755              * Generates an EditDialog for object creation and displays it to the user
756              */
757             "showCreateDialog": function() {
758                 var done = dojo.hitch(this, function() { this.hideDialog(); });
759                 var pane = this._makeCreatePane(done, done);
760                 this.editDialog = new openils.widget.EditDialog({editPane:pane});
761                 this.editDialog.startup();
762                 this.editDialog.show();
763             },
764
765             "_drawEditPane": function(storeItem, rowIndex) {
766                 var done = dojo.hitch(this, function() { this.hidePane(); });
767
768                 dojo.style(this.domNode, "display", "none");
769
770                 this.editPane = this._makeEditPane(storeItem, rowIndex, done, done);
771                 this.editPane.startup();
772                 dojo.place(this.editPane.domNode, this.domNode, "before");
773
774                 if (this.onEditPane)
775                     this.onEditPane(this.editPane);
776             },
777
778             "showClonePane": function(onPostSubmit) {
779                 var done = dojo.hitch(this, function() { this.hidePane(); });
780                 var row = this.getFirstSelectedRow();
781
782                 if (!row)
783                     return;
784
785                 if (onPostSubmit) {
786                     postSubmit = dojo.hitch(
787                         this, function(result) {
788                             onPostSubmit(this.getItem(row), result);
789                             this.hidePane();
790                         }
791                     );
792                 } else {
793                     postSubmit = done;
794                 }
795
796                 dojo.style(this.domNode, "display", "none");
797                 this.editPane = this._makeClonePane(
798                     this.getItem(row), row, postSubmit, done
799                 );
800                 dojo.place(this.editPane.domNode, this.domNode, "before");
801                 if (this.onEditPane)
802                     this.onEditPane(this.editPane);
803             },
804
805             "showCreatePane": function() {
806                 if (this._showing_create_pane)
807                     return;
808                 this._showing_create_pane = true;
809
810                 var done = dojo.hitch(
811                     this, function() {
812                         this._showing_create_pane = false;
813                         this.hidePane();
814                     }
815                 );
816
817                 dojo.style(this.domNode, "display", "none");
818
819                 this.editPane = this._makeCreatePane(done, done);
820                 this.editPane.startup();
821
822                 dojo.place(this.editPane.domNode, this.domNode, "before");
823
824                 if (this.onEditPane)
825                     this.onEditPane(this.editPane);
826             },
827
828             "hideDialog": function() {
829                 this.editDialog.hide();
830                 this.editDialog.destroy();
831                 delete this.editDialog;
832                 this.update();
833             },
834
835             "hidePane": function() {
836                 this.domNode.parentNode.removeChild(this.editPane.domNode);
837                 this.editPane.destroy();
838                 delete this.editPane;
839                 dojo.style(this.domNode, "display", "block");
840                 this.update();
841             },
842
843             "deleteSelected": function() {
844                 var self = this;
845
846                 this.getSelectedItems().forEach(
847                     function(item) {
848                         var fmobj = new fieldmapper[self.fmClass]();
849                         fmobj[self.fmIdentifier](
850                             self.store.getIdentity(item)
851                         );
852                         (new openils.PermaCrud()).eliminate(
853                             fmobj, {
854                                 "oncomplete": function() {
855                                     self.store.deleteItem(item);
856                                 }
857                             }
858                         );
859                     }
860                 );
861             },
862
863             "getSelectedIDs": function() {
864                 return this.getSelectedItems().map(
865                     dojo.hitch(
866                         this,
867                         function(item) { return this.store.getIdentity(item); }
868                     )
869                 );
870             },
871
872             /* Print the same data that the Flattener is feeding to the
873              * grid, sorted the same way too. Remove limit and offset (i.e.,
874              * print it all) unless those are passed in to the print() method.
875              */
876             "print": function(limit, offset, query_mixin) {
877                 var coal = this._columnOrderingAndLabels();
878                 var req = {
879                     "query": dojo.mixin({}, this.query, query_mixin),
880                     "queryOptions": {
881                         "columns": coal.columns,
882                         "labels": coal.labels
883                     },
884                     "onComplete": function(text) {
885                         openils.Util.printHtmlString(text);
886                     }
887                 };
888
889                 if (limit) {
890                     req.count = limit;
891                     req.start = offset || 0;
892                 } else {
893                     req.queryOptions.all = true;
894                 }
895
896                 this.store.fetchToPrint(req);
897             },
898
899             "printSelected": function() {
900                 var id_blob = {};
901                 id_blob[this.store.getIdentityAttributes()[0]] =
902                     this.getSelectedIDs();
903
904                 this.print(null, null, id_blob);
905             }
906         }
907     );
908
909     /* monkey patch so we can get more attributes from each column in the
910      * markup that specifies grid columns (table->thead->tr->[td,...])
911      */
912     (function() {
913         var b = dojox.grid.cells._Base;
914         var orig_mf = b.markupFactory;
915
916         b.markupFactory = function(node, cellDef) {
917             orig_mf(node, cellDef);
918
919             dojo.forEach(
920                 ["fpath", "ffilter"], function(a) {
921                     var value = dojo.attr(node, a);
922                     if (value)
923                         cellDef[a] = value;
924                 }
925             );
926
927             /* fsort and _visible are different. Assume true unless defined. */
928             dojo.forEach(
929                 ["fsort", "_visible"], function(a) {
930                     var val = dojo.attr(node, a);
931                     cellDef[a] = (typeof val == "undefined" || val === null) ?
932                         true : dojo.fromJson(val);
933                 }
934             );
935         };
936     })();
937
938     /* the secret to successfully subclassing dojox.grid.DataGrid */
939     openils.widget.FlattenerGrid.markupFactory =
940         dojox.grid.DataGrid.markupFactory;
941 }