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