]> git.evergreen-ils.org Git - working/Evergreen.git/blob - Open-ILS/web/js/dojo/openils/widget/FlattenerGrid.js
Link checker: Some UI tweaks suggested by George Duimovich
[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                 dojo.place(this.linkHolder.domNode, this.domNode, "before");
534
535                 if (this.showLoadFilter) {
536                     var which_filter_ui = this.filterAlwaysInDiv ?
537                         "FlattenerFilterPane" : "FlattenerFilterDialog";
538
539                     dojo.require("openils.widget." + which_filter_ui);
540                     this.filterUi =
541                         new openils.widget[which_filter_ui]({
542                             "fmClass": this.fmClass,
543                             "mapTerminii": this.mapTerminii,
544                             "useDiv": this.filterAlwaysInDiv,
545                             "initializers": this.filterInitializers,
546                             "widgetBuilders": this.filterWidgetBuilders,
547                             "suppressFilterFields": this.suppressFilterFields,
548                             "savedFiltersInterface": this.savedFiltersInterface
549                         });
550
551                     this.filterUi.onApply = dojo.hitch(
552                         this, function(filter) {
553                             this.filter(
554                                 dojo.mixin(filter, this.baseQuery),
555                                 true    /* re-render */
556                             );
557                         }
558                     );
559
560                     this.filterUi.startup();
561
562                     if (this.filterSemaphore && this.filterSemaphore()) {
563                         if (this.filterSemaphoreCallback)
564                             this.filterSemaphoreCallback();
565                     }
566                     if (!this.filterAlwaysInDiv) {
567                         new dijit.form.Button(
568                             {
569                                 "label": "Filter", /* XXX i18n */
570                                 "onClick": dojo.hitch(
571                                     this, function() { this.filterUi.show(); }
572                                 )
573                             },
574                             dojo.create("span", null, this.linkHolder.domNode)
575                         );
576                     }
577                 }
578             },
579
580             "refresh": function() {
581                 this.fetchLock = false;
582                 this._refresh(/* isRender */ true);
583             },
584
585             "_fetch": function() {
586                 if (this.fetchLock)
587                     return;
588                 else
589                     return this.inherited(arguments);
590             },
591
592             /* ******** below are methods mostly copied but
593              * slightly changed from AutoGrid ******** */
594
595             "_applySingleEditStyle": function() {
596                 this.onMouseOverRow = function(e) {};
597                 this.onMouseOutRow = function(e) {};
598                 this.onCellFocus = function(cell, rowIndex) {
599                     this.selection.deselectAll();
600                     this.selection.select(this.focus.rowIndex);
601                 };
602             },
603
604             /* capture keydown and launch edit dialog on enter */
605             "_applyEditOnEnter": function() {
606                 this._applySingleEditStyle();
607
608                 dojo.connect(
609                     this, "onRowDblClick", function(e) {
610                         if (this.editStyle == "pane")
611                             this._drawEditPane(
612                                 this.selection.getFirstSelected(),
613                                 this.focus.rowIndex
614                             );
615                         else
616                             this._drawEditDialog(
617                                 this.selection.getFirstSelected(),
618                                 this.focus.rowIndex
619                             );
620                     }
621                 );
622
623                 dojo.connect(
624                     this, "onKeyDown", function(e) {
625                         if (e.keyCode == dojo.keys.ENTER) {
626                             this.selection.deselectAll();
627                             this.selection.select(this.focus.rowIndex);
628                             if (this.editStyle == "pane")
629                                 this._drawEditPane(
630                                     this.selection.getFirstSelected(),
631                                     this.focus.rowIndex
632                                 );
633                             else
634                                 this._drawEditDialog(
635                                     this.selection.getFirstSelected(),
636                                     this.focus.rowIndex
637                                 );
638                         }
639                     }
640                 );
641             },
642
643             "_makeEditPane": function(storeItem, rowIndex, onPostSubmit, onCancel) {
644                 var grid = this;
645                 var fmObject = (new openils.PermaCrud()).retrieve(
646                     this.fmClass,
647                     this.store.getIdentity(storeItem)
648                 );
649
650                 var pane = new openils.widget.EditPane({
651                     "fmObject": fmObject,
652                     "hideSaveButton": this.editReadOnly,
653                     "readOnly": this.editReadOnly,
654                     "overrideWidgets": this.overrideEditWidgets,
655                     "overrideWidgetClass": this.overrideEditWidgetClass,
656                     "overrideWidgetArgs": this.overrideWidgetArgs,
657                     "disableWidgetTest": this.disableWidgetTest,
658                     "requiredFields": this.requiredFields,
659                     "suppressFields": this.suppressEditFields,
660                     "onPostSubmit": function() {
661                         /* ask the store to call flattener specially to get
662                          * the flat row related to only this fmobj */
663                         grid.store.loadItem({"force": true, "item": storeItem});
664
665                         if (grid.onPostUpdate)
666                             grid.onPostUpdate(storeItem, rowIndex);
667
668                         setTimeout(
669                             function() {
670                                 try {
671                                     grid.views.views[0].getCellNode(
672                                         rowIndex, 0
673                                     ).focus();
674                                 } catch (E) { }
675                             }, 200
676                         );
677                         if (onPostSubmit)
678                             onPostSubmit();
679                     },
680                     "onCancel": function() {
681                         setTimeout(
682                             function() {
683                                 grid.views.views[0].getCellNode(
684                                     rowIndex, 0
685                                 ).focus();
686                             }, 200
687                         );
688                         if (onCancel)
689                             onCancel();
690                     }
691                 });
692
693                 if (typeof this.editPaneOnSubmit == "function")
694                     pane.onSubmit = this.editPaneOnSubmit;
695
696                 pane.fieldOrder = this.fieldOrder;
697                 pane.mode = "update";
698                 return pane;
699             },
700
701             "_makeCreatePane": function(onPostSubmit, onCancel) {
702                 var grid = this;
703                 var pane = new openils.widget.EditPane({
704                     "fmClass": this.fmClass,
705                     "overrideWidgets": this.overrideEditWidgets,
706                     "overrideWidgetClass": this.overrideEditWidgetClass,
707                     "overrideWidgetArgs": this.overrideWidgetArgs,
708                     "disableWidgetTest": this.disableWidgetTest,
709                     "requiredFields": this.requiredFields,
710                     "suppressFields": this.suppressEditFields,
711                     "onPostSubmit": function(req, cudResults) {
712                         var fmObject = cudResults[0];
713                         if (grid.onPostCreate)
714                             grid.onPostCreate(fmObject);
715                         if (fmObject) {
716                             grid.store.fetchItemByIdentity({
717                                 "identity": fmObject[grid.fmIdentifier](),
718                                 "onItem": function(item) {
719                                     grid.store.onNew(item);
720                                 }
721                             });
722                         }
723
724                         setTimeout(
725                             function() {
726                                 try {
727                                     grid.selection.select(grid.rowCount - 1);
728                                     grid.views.views[0].getCellNode(
729                                         grid.rowCount - 1, 1
730                                     ).focus();
731                                 } catch (E) { }
732                             }, 200
733                         );
734
735                         if (onPostSubmit)
736                             onPostSubmit(fmObject);
737                     },
738                     "onCancel": function() { if (onCancel) onCancel(); }
739                 });
740
741                 if (typeof this.createPaneOnSubmit == "function")
742                     pane.onSubmit = this.createPaneOnSubmit;
743                 pane.fieldOrder = this.fieldOrder;
744                 pane.mode = "create";
745                 return pane;
746             },
747
748             /**
749              * Creates an EditPane with a copy of the data from the provided store
750              * item for cloning said item
751              * @param {Object} storeItem Dojo data item
752              * @param {Number} rowIndex The Grid row index of the item to be cloned
753              * @param {Function} onPostSubmit Optional callback for post-submit behavior
754              * @param {Function} onCancel Optional callback for clone cancelation
755              * @return {Object} The clone EditPane
756              */
757             "_makeClonePane": function(storeItem,rowIndex,onPostSubmit,onCancel) {
758                 var clonePane = this._makeCreatePane(onPostSubmit, onCancel);
759                 var origPane = this._makeEditPane(storeItem, rowIndex);
760                 clonePane.startup();
761                 origPane.startup();
762                 dojo.forEach(
763                     origPane.fieldList, function(field) {
764                         if (field.widget.widget.attr('disabled'))
765                             return;
766
767                         var w = clonePane.fieldList.filter(
768                             function(i) { return (i.name == field.name) }
769                         )[0];
770
771                         // sync widgets
772                         w.widget.baseWidgetValue(field.widget.widget.attr('value'));
773
774                         // async widgets
775                         w.widget.onload = function() {
776                             w.widget.baseWidgetValue(
777                                 field.widget.widget.attr('value')
778                             )
779                         };
780                     }
781                 );
782                 origPane.destroy();
783                 return clonePane;
784             },
785
786
787             "_drawEditDialog": function(storeItem, rowIndex) {
788                 var done = dojo.hitch(this, function() { this.hideDialog(); });
789                 var pane = this._makeEditPane(storeItem, rowIndex, done, done);
790                 this.editDialog = new openils.widget.EditDialog({editPane:pane});
791                 this.editDialog.startup();
792                 this.editDialog.show();
793             },
794
795             /**
796              * Generates an EditDialog for object creation and displays it to the user
797              */
798             "showCreateDialog": function() {
799                 var done = dojo.hitch(this, function() { this.hideDialog(); });
800                 var pane = this._makeCreatePane(done, done);
801                 this.editDialog = new openils.widget.EditDialog({editPane:pane});
802                 this.editDialog.startup();
803                 this.editDialog.show();
804             },
805
806             "_drawEditPane": function(storeItem, rowIndex) {
807                 var done = dojo.hitch(this, function() { this.hidePane(); });
808
809                 dojo.style(this.domNode, "display", "none");
810
811                 this.editPane = this._makeEditPane(storeItem, rowIndex, done, done);
812                 this.editPane.startup();
813                 dojo.place(this.editPane.domNode, this.domNode, "before");
814
815                 if (this.onEditPane)
816                     this.onEditPane(this.editPane);
817             },
818
819             "showClonePane": function(onPostSubmit) {
820                 var done = dojo.hitch(this, function() { this.hidePane(); });
821                 var row = this.getFirstSelectedRow();
822
823                 if (!row)
824                     return;
825
826                 if (onPostSubmit) {
827                     postSubmit = dojo.hitch(
828                         this, function(result) {
829                             onPostSubmit(this.getItem(row), result);
830                             this.hidePane();
831                         }
832                     );
833                 } else {
834                     postSubmit = done;
835                 }
836
837                 dojo.style(this.domNode, "display", "none");
838                 this.editPane = this._makeClonePane(
839                     this.getItem(row), row, postSubmit, done
840                 );
841                 dojo.place(this.editPane.domNode, this.domNode, "before");
842                 if (this.onEditPane)
843                     this.onEditPane(this.editPane);
844             },
845
846             "showCreatePane": function() {
847                 if (this._showing_create_pane)
848                     return;
849                 this._showing_create_pane = true;
850
851                 var done = dojo.hitch(
852                     this, function() {
853                         this._showing_create_pane = false;
854                         this.hidePane();
855                     }
856                 );
857
858                 dojo.style(this.domNode, "display", "none");
859
860                 this.editPane = this._makeCreatePane(done, done);
861                 this.editPane.startup();
862
863                 dojo.place(this.editPane.domNode, this.domNode, "before");
864
865                 if (this.onEditPane)
866                     this.onEditPane(this.editPane);
867             },
868
869             "hideDialog": function() {
870                 this.editDialog.hide();
871                 this.editDialog.destroy();
872                 delete this.editDialog;
873                 this.update();
874             },
875
876             "hidePane": function() {
877                 this.domNode.parentNode.removeChild(this.editPane.domNode);
878                 this.editPane.destroy();
879                 delete this.editPane;
880                 dojo.style(this.domNode, "display", "block");
881                 this.update();
882             },
883
884             "deleteSelected": function() {
885                 var self = this;
886
887                 this.getSelectedItems().forEach(
888                     function(item) {
889                         var fmobj = new fieldmapper[self.fmClass]();
890                         fmobj[self.fmIdentifier](
891                             self.store.getIdentity(item)
892                         );
893                         (new openils.PermaCrud()).eliminate(
894                             fmobj, {
895                                 "oncomplete": function() {
896                                     self.store.deleteItem(item);
897                                 }
898                             }
899                         );
900                     }
901                 );
902             },
903
904             "getSelectedIDs": function() {
905                 return this.getSelectedItems().map(
906                     dojo.hitch(
907                         this,
908                         function(item) { return this.store.getIdentity(item); }
909                     )
910                 );
911             },
912
913             /* Return true if every row known to the grid is selected. Code
914              * that calls this function will do so when it thinks the user
915              * might actually mean "select everything this grid could show"
916              * even though we don't necessarily know (and the user hasn't
917              * necessarily noticed) whether the grid has been scrolled as far
918              * down as possible and all the possible results have been
919              * fetched by the grid's store. */
920             "everythingSeemsSelected": function() {
921                 return dojo.query(
922                     "[name=autogrid.selector]", this.domNode
923                 ).filter(
924                     function(c) { return (!c.disabled && !c.checked) }
925                 ).length == 0;
926             },
927
928             "downloadCSV": function(filename_prefix, progress_dialog) {
929                 filename_prefix = filename_prefix || "grid";
930                 var localeStrings = this.localeStrings;
931
932                 var mapkey_for_filename =
933                     this.store.mapKey ? this.store.mapKey.substr(-8, 8) : "X";
934
935                 var dispositionArgs = {
936                     "defaultString": filename_prefix + "-" +
937                         mapkey_for_filename + ".csv",
938                     "defaultExtension": ".csv",
939                     "filterName": localeStrings.CSV_FILTER_NAME,
940                     "filterExtension": "*.csv",
941                     "filterAll": true
942                 };
943
944                 var coal = this._columnOrderingAndLabels();
945                 var req = {
946                     "query": this.query,
947                     "queryOptions": {
948                         "columns": coal.columns,
949                         "labels": coal.labels,
950                         "all": true
951                     },
952                     "flattenerOptions": {
953                         "contentType": "text/csv",
954                         "handleAs": "text"
955                     },
956                     "onComplete": function(text) {
957                         if (progress_dialog)
958                             progress_dialog.attr("title", "");
959                             progress_dialog.hide();
960                         openils.XUL.contentToFileSaveDialog(
961                             text, localeStrings.CSV_SAVE_DIALOG, dispositionArgs
962                         );
963                     }
964                 };
965
966                 if (progress_dialog) {
967                     progress_dialog.attr("title", localeStrings.FETCHING_CSV);
968                     progress_dialog.show(true);
969                 }
970                 this.store.fetch(req);
971             },
972
973             /* Print the same data that the Flattener is feeding to the
974              * grid, sorted the same way too. Remove limit and offset (i.e.,
975              * print it all) unless those are passed in to the print() method.
976              */
977             "print": function(limit, offset, query_mixin) {
978                 var coal = this._columnOrderingAndLabels();
979                 var req = {
980                     "query": dojo.mixin({}, this.query, query_mixin),
981                     "queryOptions": {
982                         "columns": coal.columns,
983                         "labels": coal.labels
984                     },
985                     "flattenerOptions": {
986                         "handleAs": "text", "contentType": "text/html"
987                     },
988                     "onComplete": function(text) {
989                         openils.Util.printHtmlString(text);
990                     }
991                 };
992
993                 if (limit) {
994                     req.count = limit;
995                     req.start = offset || 0;
996                 } else {
997                     req.queryOptions.all = true;
998                 }
999
1000                 this.store.fetch(req);
1001             },
1002
1003             "printSelected": function() {
1004                 var id_blob = {};
1005                 id_blob[this.store.getIdentityAttributes()[0]] =
1006                     this.getSelectedIDs();
1007
1008                 this.print(null, null, id_blob);
1009             },
1010
1011             "setBaseQuery": function(query) {   /* sets a persistent query
1012                                                    that always gets mixed in
1013                                                    with whatever you do in the
1014                                                    filter dialog */
1015                 this._baseQuery = dojo.clone(this.query = query);
1016             }
1017         }
1018     );
1019
1020     /* monkey patch so we can get more attributes from each column in the
1021      * markup that specifies grid columns (table->thead->tr->[td,...])
1022      */
1023     (function() {
1024         var b = dojox.grid.cells._Base;
1025         var orig_mf = b.markupFactory;
1026
1027         b.markupFactory = function(node, cellDef) {
1028             orig_mf(node, cellDef);
1029
1030             dojo.forEach(
1031                 ["fpath", "ffilter"], function(a) {
1032                     var value = dojo.attr(node, a);
1033                     if (value)
1034                         cellDef[a] = value;
1035                 }
1036             );
1037
1038             /* fsort and _visible are different. Assume true unless defined. */
1039             dojo.forEach(
1040                 ["fsort", "_visible"], function(a) {
1041                     var val = dojo.attr(node, a);
1042                     cellDef[a] = (typeof val == "undefined" || val === null) ?
1043                         true : dojo.fromJson(val);
1044                 }
1045             );
1046         };
1047     })();
1048
1049     /* the secret to successfully subclassing dojox.grid.DataGrid */
1050     openils.widget.FlattenerGrid.markupFactory =
1051         dojox.grid.DataGrid.markupFactory;
1052 }