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