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