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