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