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