1 if (!dojo._hasResource["openils.widget.FlattenerGrid"]) {
2 dojo.provide("openils.widget.FlattenerGrid");
4 dojo.requireLocalization("openils.widget", "FlattenerGrid");
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");
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"
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 */
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
46 "savedFiltersInterface": null,
48 /* These potential constructor arguments may be useful to
49 * FlattenerGrid in their own right, and are passed to
54 "sortFieldReMap": null,
55 "defaultSort": null, /* whatever any part of the UI says will
57 "baseSort": null, /* will contains what the columnpicker
58 dictates, and precedes whatever the column
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 */
69 /* _generateMap() lives to interpret the attributes of the
70 * FlattenerGrid dijit itself plus those definined in
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.
78 "_generateMap": function() {
79 var map = this.mapClause = {};
80 var fields = this.structure[0].cells[0];
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
89 * If you need extra fields in the map for which display
90 * should *not* be true, use mapExtras.
93 fields, function(field) {
94 if (field.field.match(/^\+/))
95 return; /* special fields e.g. checkbox/line # */
99 "filter": (field.ffilter || false),
101 "path": field.fpath || field.field
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.*/
107 map[field.field]._label = field.name;
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
119 for (var key in this.mapExtras) {
120 if (typeof this.mapExtras[key] != "object") {
121 this.mapExtras[key] = {
122 "path": this.mapExtras[key],
129 dojo.mixin(map, this.mapExtras);
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();
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. */
151 "_cleanMapForStore": function(map) {
152 var clean = dojo.clone(map);
154 for (var column in clean) {
155 openils.Util.objectProperties(clean[column]).filter(
156 function(k) { return k.match(/^_/); }
158 function(k) { delete clean[column][k]; }
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);
173 var last_field, last_hint;
174 var orig_path = dojo.clone(path);
175 var field, field_def;
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. */
182 fieldmapper.IDL.fmclasses[hint].field_map[field];
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). */
190 "Lost our way in IDL at hint " + hint +
191 ", field " + field + "; may be ok"
196 if (field_def["class"]) {
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. */
205 "_calculateMapTerminii can't parse path " +
206 orig_path + " (at " + field + ")"
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.
217 if (orig_path.length > 1 && allow_selector_backoff &&
218 _fm_is_selector_for_class(hint, field_def.name)) {
224 field = field_def.name;
230 "label": field_def.label,
231 "datatype": datatype,
236 /* The FlattenerStore doesn't need this, but it has at least two
237 * uses: 1) FlattenerFilterDialog, 2) setting column header labels
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(
248 this.mapClause[column].path.split(/\./),
249 true /* allow selector backoff */
253 var terminus = dojo.mixin(
255 "simple_name": column,
256 "isfilter": this.mapClause[column].filter
259 if (this.mapClause[column]._label)
260 terminus.label = this.mapClause[column]._label;
262 this.mapTerminii.push(terminus);
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. */
273 this.structure[0].cells[0].forEach(
276 header.name = self.mapTerminii.filter(
278 return t.simple_name == header.field;
286 "_columnOrderingAndLabels": function() {
290 this.views.views[0].structure.cells[0].forEach(
292 if (!c.field.match(/^\+/)) {
294 columns.push(c.field);
299 return {"labels": labels, "columns": columns};
302 "_getAutoFieldFields": function(fmclass, path) {
303 var field_list = dojo.clone(
304 fieldmapper.IDL.fmclasses[fmclass].fields)
306 function(f) { return !f.virtual && f.datatype != "link"; }
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; }
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
324 "_addAutoCoreFields": function() {
325 var cell_list = this.structure[0].cells[0];
326 var fields = dojo.clone(
327 fieldmapper.IDL.fmclasses[this.fmClass].fields
330 if (!this.autoCoreFieldsUnsorted) {
331 fields = fields.sort(
332 function(a, b) { return a.label > b.label ? 1 : -1; }
337 fields, dojo.hitch(this, function(f) {
338 if (f.datatype == "link" || f.virtual)
341 if (cell_list.filter(
343 if (!c.fpath) return false;
344 return c.fpath.split(/\./)[0] == f.name;
353 "ffilter": this.autoCoreFieldsFilter
359 "_addAutoFieldFields": function(paths) {
364 paths, function(path) {
365 /* The beginning is the end. */
366 var beginning = self._followPathToEnd(
367 self.fmClass, path.split(/\./), false
373 self._getAutoFieldFields(
374 beginning.fmClass, path
378 path + "." + field.name;
380 new RegExp("^" + would_be_path);
381 if (!self.structure[0].cells[0].filter(
384 c.fpath.match(wbp_re);
387 self.structure[0].cells[0].push({
388 "field": "AUTO_" + beginning.name +
390 "name": beginning.label + " - " +
393 "fpath": would_be_path,
404 "_addAutoFields": function() {
405 if (this.autoCoreFields)
406 this._addAutoCoreFields();
408 if (dojo.isArray(this.autoFieldFields))
409 this._addAutoFieldFields(this.autoFieldFields);
411 this.setStructure(this.structure);
414 "constructor": function(args) {
415 dojo.mixin(this, args);
417 this.fmIdentifier = this.fmIdentifier ||
418 fieldmapper.IDL.fmclasses[this.fmClass].pkey;
420 this.overrideEditWidgets = {};
421 this.overrideEditWidgetClass = {};
422 this.overrideWidgetArgs = {};
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. */
430 this.baseQuery = dojo.clone(this.query);
432 this._addAutoFields();
434 this._startupGridHelperColumns();
438 if (!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) });
445 this.columnPicker.onSortChange = dojo.hitch(this,
446 /* directly after, this.update() is called by the
447 column picker, causing a re-fetch */
449 this.store.baseSort = this._mapCPSortFields(fields)
453 this.columnPicker.load();
456 this.inherited(arguments);
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.
467 * There may be a more targeted way to fix the problem,
468 * but this will do. */
469 console.log("focusHeader() suppressed");
473 "canSort": function(idx, skip_structure /* API abuse */) {
474 var initial = this.inherited(arguments);
476 /* idx is one-based instead of zero-based for a reason. */
477 var view_idx = Math.abs(idx) - 1;
480 this.views.views[0].structure.cells[0][view_idx].fsort
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) {
491 a[f.field] = f.direction;
498 "_finishStartup": function(sortFields) {
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
512 // pick up any column label changes
513 this.columnPicker.reloadStructure();
518 this._showing_create_pane = false;
520 if (this.editOnEnter)
521 this._applyEditOnEnter();
522 else if (this.singleEditStyle)
523 this._applySingleEditStyle();
525 /* Like AutoGrid's paginator, but we'll never have Back/Next
526 * links. Just a place to hold misc links */
531 "_setupLinks": function() {
532 this.linkHolder = new dijit.layout.ContentPane();
533 var localeStrings = this.localeStrings;
534 dojo.place(this.linkHolder.domNode, this.domNode, "before");
536 if (this.showLoadFilter) {
537 var which_filter_ui = this.filterAlwaysInDiv ?
538 "FlattenerFilterPane" : "FlattenerFilterDialog";
540 dojo.require("openils.widget." + which_filter_ui);
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
552 this.filterUi.onApply = dojo.hitch(
553 this, function(filter) {
555 dojo.mixin(filter, this.baseQuery),
561 this.filterUi.startup();
563 if (this.filterSemaphore && this.filterSemaphore()) {
564 if (this.filterSemaphoreCallback)
565 this.filterSemaphoreCallback();
567 if (!this.filterAlwaysInDiv) {
568 new dijit.form.Button(
570 "label": localeStrings.FILTER,
571 "onClick": dojo.hitch(
572 this, function() { this.filterUi.show(); }
575 dojo.create("span", null, this.linkHolder.domNode)
581 "refresh": function() {
582 this.fetchLock = false;
583 this._refresh(/* isRender */ true);
586 "_fetch": function() {
590 return this.inherited(arguments);
593 /* ******** below are methods mostly copied but
594 * slightly changed from AutoGrid ******** */
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);
605 /* capture keydown and launch edit dialog on enter */
606 "_applyEditOnEnter": function() {
607 this._applySingleEditStyle();
610 this, "onRowDblClick", function(e) {
611 if (this.editStyle == "pane")
613 this.selection.getFirstSelected(),
617 this._drawEditDialog(
618 this.selection.getFirstSelected(),
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")
631 this.selection.getFirstSelected(),
635 this._drawEditDialog(
636 this.selection.getFirstSelected(),
644 "_makeEditPane": function(storeItem, rowIndex, onPostSubmit, onCancel) {
646 var fmObject = (new openils.PermaCrud()).retrieve(
648 this.store.getIdentity(storeItem)
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});
666 if (grid.onPostUpdate)
667 grid.onPostUpdate(storeItem, rowIndex);
672 grid.views.views[0].getCellNode(
681 "onCancel": function() {
684 grid.views.views[0].getCellNode(
694 if (typeof this.editPaneOnSubmit == "function")
695 pane.onSubmit = this.editPaneOnSubmit;
697 pane.fieldOrder = this.fieldOrder;
698 pane.mode = "update";
702 "_makeCreatePane": function(onPostSubmit, onCancel) {
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);
717 grid.store.fetchItemByIdentity({
718 "identity": fmObject[grid.fmIdentifier](),
719 "onItem": function(item) {
720 grid.store.onNew(item);
728 grid.selection.select(grid.rowCount - 1);
729 grid.views.views[0].getCellNode(
737 onPostSubmit(fmObject);
739 "onCancel": function() { if (onCancel) onCancel(); }
742 if (typeof this.createPaneOnSubmit == "function")
743 pane.onSubmit = this.createPaneOnSubmit;
744 pane.fieldOrder = this.fieldOrder;
745 pane.mode = "create";
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
758 "_makeClonePane": function(storeItem,rowIndex,onPostSubmit,onCancel) {
759 var clonePane = this._makeCreatePane(onPostSubmit, onCancel);
760 var origPane = this._makeEditPane(storeItem, rowIndex);
764 origPane.fieldList, function(field) {
765 if (field.widget.widget.attr('disabled'))
768 var w = clonePane.fieldList.filter(
769 function(i) { return (i.name == field.name) }
773 w.widget.baseWidgetValue(field.widget.widget.attr('value'));
776 w.widget.onload = function() {
777 w.widget.baseWidgetValue(
778 field.widget.widget.attr('value')
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();
797 * Generates an EditDialog for object creation and displays it to the user
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();
807 "_drawEditPane": function(storeItem, rowIndex) {
808 var done = dojo.hitch(this, function() { this.hidePane(); });
810 dojo.style(this.domNode, "display", "none");
812 this.editPane = this._makeEditPane(storeItem, rowIndex, done, done);
813 this.editPane.startup();
814 dojo.place(this.editPane.domNode, this.domNode, "before");
817 this.onEditPane(this.editPane);
820 "showClonePane": function(onPostSubmit) {
821 var done = dojo.hitch(this, function() { this.hidePane(); });
822 var row = this.getFirstSelectedRow();
828 postSubmit = dojo.hitch(
829 this, function(result) {
830 onPostSubmit(this.getItem(row), result);
838 dojo.style(this.domNode, "display", "none");
839 this.editPane = this._makeClonePane(
840 this.getItem(row), row, postSubmit, done
842 dojo.place(this.editPane.domNode, this.domNode, "before");
844 this.onEditPane(this.editPane);
847 "showCreatePane": function() {
848 if (this._showing_create_pane)
850 this._showing_create_pane = true;
852 var done = dojo.hitch(
854 this._showing_create_pane = false;
859 dojo.style(this.domNode, "display", "none");
861 this.editPane = this._makeCreatePane(done, done);
862 this.editPane.startup();
864 dojo.place(this.editPane.domNode, this.domNode, "before");
867 this.onEditPane(this.editPane);
870 "hideDialog": function() {
871 this.editDialog.hide();
872 this.editDialog.destroy();
873 delete this.editDialog;
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");
885 "deleteSelected": function() {
888 this.getSelectedItems().forEach(
890 var fmobj = new fieldmapper[self.fmClass]();
891 fmobj[self.fmIdentifier](
892 self.store.getIdentity(item)
894 (new openils.PermaCrud()).eliminate(
896 "oncomplete": function() {
897 self.store.deleteItem(item);
905 "getSelectedIDs": function() {
906 return this.getSelectedItems().map(
909 function(item) { return this.store.getIdentity(item); }
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() {
923 "[name=autogrid.selector]", this.domNode
925 function(c) { return (!c.disabled && !c.checked) }
929 "downloadCSV": function(filename_prefix, progress_dialog) {
930 filename_prefix = filename_prefix || "grid";
931 var localeStrings = this.localeStrings;
933 var mapkey_for_filename =
934 this.store.mapKey ? this.store.mapKey.substr(-8, 8) : "X";
936 var dispositionArgs = {
937 "defaultString": filename_prefix + "-" +
938 mapkey_for_filename + ".csv",
939 "defaultExtension": ".csv",
940 "filterName": localeStrings.CSV_FILTER_NAME,
941 "filterExtension": "*.csv",
945 var coal = this._columnOrderingAndLabels();
949 "columns": coal.columns,
950 "labels": coal.labels,
953 "flattenerOptions": {
954 "contentType": "text/csv",
957 "onComplete": function(text) {
959 progress_dialog.attr("title", "");
960 progress_dialog.hide();
961 openils.XUL.contentToFileSaveDialog(
962 text, localeStrings.CSV_SAVE_DIALOG, dispositionArgs
967 if (progress_dialog) {
968 progress_dialog.attr("title", localeStrings.FETCHING_CSV);
969 progress_dialog.show(true);
971 this.store.fetch(req);
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.
978 "print": function(limit, offset, query_mixin) {
979 var coal = this._columnOrderingAndLabels();
981 "query": dojo.mixin({}, this.query, query_mixin),
983 "columns": coal.columns,
984 "labels": coal.labels
986 "flattenerOptions": {
987 "handleAs": "text", "contentType": "text/html"
989 "onComplete": function(text) {
990 openils.Util.printHtmlString(text);
996 req.start = offset || 0;
998 req.queryOptions.all = true;
1001 this.store.fetch(req);
1004 "printSelected": function() {
1006 id_blob[this.store.getIdentityAttributes()[0]] =
1007 this.getSelectedIDs();
1009 this.print(null, null, id_blob);
1012 "setBaseQuery": function(query) { /* sets a persistent query
1013 that always gets mixed in
1014 with whatever you do in the
1016 this._baseQuery = dojo.clone(this.query = query);
1021 /* monkey patch so we can get more attributes from each column in the
1022 * markup that specifies grid columns (table->thead->tr->[td,...])
1025 var b = dojox.grid.cells._Base;
1026 var orig_mf = b.markupFactory;
1028 b.markupFactory = function(node, cellDef) {
1029 orig_mf(node, cellDef);
1032 ["fpath", "ffilter"], function(a) {
1033 var value = dojo.attr(node, a);
1039 /* fsort and _visible are different. Assume true unless defined. */
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);
1050 /* the secret to successfully subclassing dojox.grid.DataGrid */
1051 openils.widget.FlattenerGrid.markupFactory =
1052 dojox.grid.DataGrid.markupFactory;