1 if (!dojo._hasResource["openils.widget.FlattenerGrid"]) {
2 dojo.provide("openils.widget.FlattenerGrid");
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");
13 "openils.widget.FlattenerGrid",
14 [dojox.grid.DataGrid, openils.widget._GridHelperColumns], {
15 /* These potential constructor arguments are useful to
16 * FlattenerGrid in their own right */
17 "columnReordering": true,
18 "columnPersistKey": null,
19 "autoCoreFields": false,
20 "autoCoreFieldsUnsorted": false,
21 "autoCoreFieldsFilter": false,
22 "autoFieldFields": null,
23 "autoFieldFieldsUnsorted": null, /* array, subset of autoFieldFields */
24 "showLoadFilter": false, /* use FlattenerFilter(Dialog|Pane) */
25 "filterAlwaysInDiv": null, /* use FlattenerFilterPane and put its
26 content in this HTML element */
28 "filterInitializers": null,
29 "filterWidgetBuilders": null,
30 "filterSemaphore": null,
31 "filterSemaphoreCallback": null,
32 "baseQuery": null, /* Good place to mix in data from, say, context
33 OU selectors so that it should get mixed
34 correctly with the generated query from the
36 "savedFiltersInterface": null,
38 /* These potential constructor arguments may be useful to
39 * FlattenerGrid in their own right, and are passed to
44 "sortFieldReMap": null,
45 "defaultSort": null, /* whatever any part of the UI says will
47 "baseSort": null, /* will contains what the columnpicker
48 dictates, and precedes whatever the column
51 /* These potential constructor arguments are for functionality
52 * copied from AutoGrid */
53 "editOnEnter": false, /* also implies edit-on-dblclick */
54 "editStyle": "dialog", /* "dialog" or "pane" */
55 "requiredFields": null, /* affects create/edit dialogs */
56 "suppressEditFields": null, /* affects create/edit dialogs */
57 "suppressFilterFields": null, /* affects filter dialog */
59 /* _generateMap() lives to interpret the attributes of the
60 * FlattenerGrid dijit itself plus those definined in
64 * <th field="foo" ...>
65 * to build the map to hand to the FlattenerStore, which in turn
66 * uses it to query the flattener service.
68 "_generateMap": function() {
69 var map = this.mapClause = {};
70 var fields = this.structure[0].cells[0];
72 /* These are the fields defined in thead -> tr -> [th,th,...].
73 * For purposes of building the map, where each field has
74 * three boolean attributes "display", "sort" and "filter",
75 * assume "display" is always true for these.
76 * That doesn't mean that at the UI level we can't hide a
79 * If you need extra fields in the map for which display
80 * should *not* be true, use mapExtras.
83 fields, function(field) {
84 if (field.field.match(/^\+/))
85 return; /* special fields e.g. checkbox/line # */
89 "filter": (field.ffilter || false),
91 "path": field.fpath || field.field
93 /* The following attribute is not for the flattener
94 * service's benefit, but for other uses. We capture
95 * the hardcoded <th> value (the header label) if any.*/
97 map[field.field]._label = field.name;
101 if (this.mapExtras) {
102 /* It's not particularly useful to add simple fields, i.e.
103 * circ_lib: "circ_lib.name"
104 * to mapExtras, because by convention used elsewhere in
105 * Flattener, that gives all attributes, including
106 * display, a true value. Still, be consistent to avoid
109 for (var key in this.mapExtras) {
110 if (typeof this.mapExtras[key] != "object") {
111 this.mapExtras[key] = {
112 "path": this.mapExtras[key],
119 dojo.mixin(map, this.mapExtras);
122 /* Do this now, since we don't want a silently added
123 * identifier attribute in the terminii list (see its uses). */
124 this._calculateMapTerminii();
125 this._supplementHeaderNames();
127 /* make sure we always have a field for fm identifier */
128 if (!map[this.fmIdentifier]) {
129 map[this.fmIdentifier] = {
130 "path": this.fmIdentifier,
131 "display": true, /* Flattener displays it to us,
132 but we don't display to user. */
141 "_cleanMapForStore": function(map) {
142 var clean = dojo.clone(map);
144 for (var column in clean) {
145 openils.Util.objectProperties(clean[column]).filter(
146 function(k) { return k.match(/^_/); }
148 function(k) { delete clean[column][k]; }
155 /* Given the hint of a class to start at, follow path to the end
156 * and return information on the last field. */
157 "_followPathToEnd": function(hint, path, allow_selector_backoff) {
158 function _fm_is_selector_for_class(h, field) {
159 var cl = fieldmapper.IDL.fmclasses[h];
160 return (cl.field_map[cl.pkey].selector == field);
163 var last_field, last_hint;
164 var orig_path = dojo.clone(path);
165 var field, field_def;
167 while (field = path.shift()) {
168 /* XXX this assumes we have the whole IDL loaded. I
169 * guess we could teach this to work by loading classes
170 * on demand when we don't have the whole IDL loaded. */
172 fieldmapper.IDL.fmclasses[hint].field_map[field];
175 /* This can be ok in some cases. Columns following
176 * IDL paths involving links with a nonempty "map"
177 * attribute can be used for display only (no
178 * sort, no filter). */
180 "Lost our way in IDL at hint " + hint +
181 ", field " + field + "; may be ok"
186 if (field_def["class"]) {
190 hint = field_def["class"];
191 } else if (path.length) {
192 /* There are more fields left but we can't follow
193 * the chain via IDL any further. */
195 "_calculateMapTerminii can't parse path " +
196 orig_path + " (at " + field + ")"
201 var datatype = field_def.datatype;
202 var indirect = false;
203 /* If allowed, back off the last field in the path if it's a
204 * selector for its class, because the preceding field will be
205 * a better thing to hand to AutoFieldWidget.
207 if (orig_path.length > 1 && allow_selector_backoff &&
208 _fm_is_selector_for_class(hint, field_def.name)) {
214 field = field_def.name;
220 "label": field_def.label,
221 "datatype": datatype,
226 /* The FlattenerStore doesn't need this, but it has at least two
227 * uses: 1) FlattenerFilterDialog, 2) setting column header labels
230 * To call these 'Terminii' can be misleading. In certain
231 * (actually probably common) cases, they won't really be the last
232 * field in a path, but the next-to-last. Read on. */
233 "_calculateMapTerminii": function() {
234 this.mapTerminii = [];
235 for (var column in this.mapClause) {
236 var end = this._followPathToEnd(
238 this.mapClause[column].path.split(/\./),
239 true /* allow selector backoff */
243 var terminus = dojo.mixin(
245 "simple_name": column,
246 "isfilter": this.mapClause[column].filter
249 if (this.mapClause[column]._label)
250 terminus.label = this.mapClause[column]._label;
252 this.mapTerminii.push(terminus);
256 "_supplementHeaderNames": function() {
257 /* If we didn't give a particular header cell
258 * (<th>) a display name (the innerHTML of that <th>), then
259 * use the IDL to provide the label of the terminus of the
260 * flattener path for that column. It may be better than using
261 * the raw field name. */
263 this.structure[0].cells[0].forEach(
266 header.name = self.mapTerminii.filter(
268 return t.simple_name == header.field;
276 "_columnOrderingAndLabels": function() {
280 this.views.views[0].structure.cells[0].forEach(
282 if (!c.field.match(/^\+/)) {
284 columns.push(c.field);
289 return {"labels": labels, "columns": columns};
292 "_getAutoFieldFields": function(fmclass, path) {
293 var field_list = dojo.clone(
294 fieldmapper.IDL.fmclasses[fmclass].fields)
296 function(f) { return !f.virtual && f.datatype != "link"; }
299 /* Sort fields unless the path is named in grid property
300 * 'autoFieldFieldsUnsorted' (array). */
301 if (!dojo.isArray(this.autoFieldFieldsUnsorted) ||
302 this.autoFieldFieldsUnsorted.indexOf(path) == -1) {
303 field_list = field_list.sort(
304 function(a, b) { return a.label > b.label ? 1 : -1; }
311 /* Take our core class (this.fmClass) and add table columns for
312 * any field we don't already have covered by actual hard-coded
314 "_addAutoCoreFields": function() {
315 var cell_list = this.structure[0].cells[0];
316 var fields = dojo.clone(
317 fieldmapper.IDL.fmclasses[this.fmClass].fields
320 if (!this.autoCoreFieldsUnsorted) {
321 fields = fields.sort(
322 function(a, b) { return a.label > b.label ? 1 : -1; }
327 fields, dojo.hitch(this, function(f) {
328 if (f.datatype == "link" || f.virtual)
331 if (cell_list.filter(
333 if (!c.fpath) return false;
334 return c.fpath.split(/\./)[0] == f.name;
343 "ffilter": this.autoCoreFieldsFilter
349 "_addAutoFieldFields": function(paths) {
354 paths, function(path) {
355 /* The beginning is the end. */
356 var beginning = self._followPathToEnd(
357 self.fmClass, path.split(/\./), false
363 self._getAutoFieldFields(
364 beginning.fmClass, path
368 path + "." + field.name;
370 new RegExp("^" + would_be_path);
371 if (!self.structure[0].cells[0].filter(
374 c.fpath.match(wbp_re);
377 self.structure[0].cells[0].push({
378 "field": "AUTO_" + beginning.name +
380 "name": beginning.label + " - " +
383 "fpath": would_be_path,
394 "_addAutoFields": function() {
395 if (this.autoCoreFields)
396 this._addAutoCoreFields();
398 if (dojo.isArray(this.autoFieldFields))
399 this._addAutoFieldFields(this.autoFieldFields);
401 this.setStructure(this.structure);
404 "constructor": function(args) {
405 dojo.mixin(this, args);
407 this.fmIdentifier = this.fmIdentifier ||
408 fieldmapper.IDL.fmclasses[this.fmClass].pkey;
410 this.overrideEditWidgets = {};
411 this.overrideEditWidgetClass = {};
412 this.overrideWidgetArgs = {};
415 "startup": function() {
416 /* Save original query for further filtering later, unless
417 * we've already defined baseQuery from the outside, in
418 * which case it persists. */
420 this.baseQuery = dojo.clone(this.query);
422 this._addAutoFields();
424 this._startupGridHelperColumns();
428 if (!this.columnPicker) {
430 new openils.widget.GridColumnPicker(
431 null, this.columnPersistKey, this);
432 this.columnPicker.onLoad = dojo.hitch(
433 this, function(opts) { this._finishStartup(opts.sortFields) });
435 this.columnPicker.onSortChange = dojo.hitch(this,
436 /* directly after, this.update() is called by the
437 column picker, causing a re-fetch */
439 this.store.baseSort = this._mapCPSortFields(fields)
443 this.columnPicker.load();
446 this.inherited(arguments);
449 "canSort": function(idx, skip_structure /* API abuse */) {
450 var initial = this.inherited(arguments);
452 /* idx is one-based instead of zero-based for a reason. */
453 var view_idx = Math.abs(idx) - 1;
456 this.views.views[0].structure.cells[0][view_idx].fsort
460 /* Maps ColumnPicker sort fields to the correct format.
461 If no sort fields specified, falls back to defaultSort */
462 "_mapCPSortFields": function(sortFields) {
463 var sort = this.defaultSort;
464 if (sortFields.length) {
465 sort = sortFields.map(function(f) {
467 a[f.field] = f.direction;
474 "_finishStartup": function(sortFields) {
476 this._setStore( /* Seriously, let's leave this as _setStore. */
477 new openils.FlattenerStore({
478 "fmClass": this.fmClass,
479 "fmIdentifier": this.fmIdentifier,
480 "mapClause": this._cleanMapForStore(this.mapClause),
481 "baseSort": this.baseSort,
482 "defaultSort": this._mapCPSortFields(sortFields),
483 "sortFieldReMap": this.sortFieldReMap
488 // pick up any column label changes
489 this.columnPicker.reloadStructure();
494 this._showing_create_pane = false;
496 if (this.editOnEnter)
497 this._applyEditOnEnter();
498 else if (this.singleEditStyle)
499 this._applySingleEditStyle();
501 /* Like AutoGrid's paginator, but we'll never have Back/Next
502 * links. Just a place to hold misc links */
507 "_setupLinks": function() {
508 this.linkHolder = new dijit.layout.ContentPane();
509 dojo.place(this.linkHolder.domNode, this.domNode, "before");
511 if (this.showLoadFilter) {
512 var which_filter_ui = this.filterAlwaysInDiv ?
513 "FlattenerFilterPane" : "FlattenerFilterDialog";
515 dojo.require("openils.widget." + which_filter_ui);
517 new openils.widget[which_filter_ui]({
518 "fmClass": this.fmClass,
519 "mapTerminii": this.mapTerminii,
520 "useDiv": this.filterAlwaysInDiv,
521 "initializers": this.filterInitializers,
522 "widgetBuilders": this.filterWidgetBuilders,
523 "suppressFilterFields": this.suppressFilterFields,
524 "savedFiltersInterface": this.savedFiltersInterface
527 this.filterUi.onApply = dojo.hitch(
528 this, function(filter) {
530 dojo.mixin(filter, this.baseQuery),
536 this.filterUi.startup();
538 if (this.filterSemaphore && this.filterSemaphore()) {
539 if (this.filterSemaphoreCallback)
540 this.filterSemaphoreCallback();
542 if (!this.filterAlwaysInDiv) {
545 "innerHTML": "Filter", /* XXX i18n */
546 "href": "javascript:void(0);",
547 "onclick": dojo.hitch(this, function() {
548 this.filterUi.show();
550 }, this.linkHolder.domNode
556 "refresh": function() {
557 this.fetchLock = false;
558 this._refresh(/* isRender */ true);
561 "_fetch": function() {
565 return this.inherited(arguments);
568 /* ******** below are methods mostly copied but
569 * slightly changed from AutoGrid ******** */
571 "_applySingleEditStyle": function() {
572 this.onMouseOverRow = function(e) {};
573 this.onMouseOutRow = function(e) {};
574 this.onCellFocus = function(cell, rowIndex) {
575 this.selection.deselectAll();
576 this.selection.select(this.focus.rowIndex);
580 /* capture keydown and launch edit dialog on enter */
581 "_applyEditOnEnter": function() {
582 this._applySingleEditStyle();
585 this, "onRowDblClick", function(e) {
586 if (this.editStyle == "pane")
588 this.selection.getFirstSelected(),
592 this._drawEditDialog(
593 this.selection.getFirstSelected(),
600 this, "onKeyDown", function(e) {
601 if (e.keyCode == dojo.keys.ENTER) {
602 this.selection.deselectAll();
603 this.selection.select(this.focus.rowIndex);
604 if (this.editStyle == "pane")
606 this.selection.getFirstSelected(),
610 this._drawEditDialog(
611 this.selection.getFirstSelected(),
619 "_makeEditPane": function(storeItem, rowIndex, onPostSubmit, onCancel) {
621 var fmObject = (new openils.PermaCrud()).retrieve(
623 this.store.getIdentity(storeItem)
626 var pane = new openils.widget.EditPane({
627 "fmObject": fmObject,
628 "hideSaveButton": this.editReadOnly,
629 "readOnly": this.editReadOnly,
630 "overrideWidgets": this.overrideEditWidgets,
631 "overrideWidgetClass": this.overrideEditWidgetClass,
632 "overrideWidgetArgs": this.overrideWidgetArgs,
633 "disableWidgetTest": this.disableWidgetTest,
634 "requiredFields": this.requiredFields,
635 "suppressFields": this.suppressEditFields,
636 "onPostSubmit": function() {
637 /* ask the store to call flattener specially to get
638 * the flat row related to only this fmobj */
639 grid.store.loadItem({"force": true, "item": storeItem});
641 if (grid.onPostUpdate)
642 grid.onPostUpdate(storeItem, rowIndex);
647 grid.views.views[0].getCellNode(
656 "onCancel": function() {
659 grid.views.views[0].getCellNode(
669 if (typeof this.editPaneOnSubmit == "function")
670 pane.onSubmit = this.editPaneOnSubmit;
672 pane.fieldOrder = this.fieldOrder;
673 pane.mode = "update";
677 "_makeCreatePane": function(onPostSubmit, onCancel) {
679 var pane = new openils.widget.EditPane({
680 "fmClass": this.fmClass,
681 "overrideWidgets": this.overrideEditWidgets,
682 "overrideWidgetClass": this.overrideEditWidgetClass,
683 "overrideWidgetArgs": this.overrideWidgetArgs,
684 "disableWidgetTest": this.disableWidgetTest,
685 "requiredFields": this.requiredFields,
686 "suppressFields": this.suppressEditFields,
687 "onPostSubmit": function(req, cudResults) {
688 var fmObject = cudResults[0];
689 if (grid.onPostCreate)
690 grid.onPostCreate(fmObject);
692 grid.store.fetchItemByIdentity({
693 "identity": fmObject[grid.fmIdentifier](),
694 "onItem": function(item) {
695 grid.store.onNew(item);
703 grid.selection.select(grid.rowCount - 1);
704 grid.views.views[0].getCellNode(
712 onPostSubmit(fmObject);
714 "onCancel": function() { if (onCancel) onCancel(); }
717 if (typeof this.createPaneOnSubmit == "function")
718 pane.onSubmit = this.createPaneOnSubmit;
719 pane.fieldOrder = this.fieldOrder;
720 pane.mode = "create";
725 * Creates an EditPane with a copy of the data from the provided store
726 * item for cloning said item
727 * @param {Object} storeItem Dojo data item
728 * @param {Number} rowIndex The Grid row index of the item to be cloned
729 * @param {Function} onPostSubmit Optional callback for post-submit behavior
730 * @param {Function} onCancel Optional callback for clone cancelation
731 * @return {Object} The clone EditPane
733 "_makeClonePane": function(storeItem,rowIndex,onPostSubmit,onCancel) {
734 var clonePane = this._makeCreatePane(onPostSubmit, onCancel);
735 var origPane = this._makeEditPane(storeItem, rowIndex);
739 origPane.fieldList, function(field) {
740 if (field.widget.widget.attr('disabled'))
743 var w = clonePane.fieldList.filter(
744 function(i) { return (i.name == field.name) }
748 w.widget.baseWidgetValue(field.widget.widget.attr('value'));
751 w.widget.onload = function() {
752 w.widget.baseWidgetValue(
753 field.widget.widget.attr('value')
763 "_drawEditDialog": function(storeItem, rowIndex) {
764 var done = dojo.hitch(this, function() { this.hideDialog(); });
765 var pane = this._makeEditPane(storeItem, rowIndex, done, done);
766 this.editDialog = new openils.widget.EditDialog({editPane:pane});
767 this.editDialog.startup();
768 this.editDialog.show();
772 * Generates an EditDialog for object creation and displays it to the user
774 "showCreateDialog": function() {
775 var done = dojo.hitch(this, function() { this.hideDialog(); });
776 var pane = this._makeCreatePane(done, done);
777 this.editDialog = new openils.widget.EditDialog({editPane:pane});
778 this.editDialog.startup();
779 this.editDialog.show();
782 "_drawEditPane": function(storeItem, rowIndex) {
783 var done = dojo.hitch(this, function() { this.hidePane(); });
785 dojo.style(this.domNode, "display", "none");
787 this.editPane = this._makeEditPane(storeItem, rowIndex, done, done);
788 this.editPane.startup();
789 dojo.place(this.editPane.domNode, this.domNode, "before");
792 this.onEditPane(this.editPane);
795 "showClonePane": function(onPostSubmit) {
796 var done = dojo.hitch(this, function() { this.hidePane(); });
797 var row = this.getFirstSelectedRow();
803 postSubmit = dojo.hitch(
804 this, function(result) {
805 onPostSubmit(this.getItem(row), result);
813 dojo.style(this.domNode, "display", "none");
814 this.editPane = this._makeClonePane(
815 this.getItem(row), row, postSubmit, done
817 dojo.place(this.editPane.domNode, this.domNode, "before");
819 this.onEditPane(this.editPane);
822 "showCreatePane": function() {
823 if (this._showing_create_pane)
825 this._showing_create_pane = true;
827 var done = dojo.hitch(
829 this._showing_create_pane = false;
834 dojo.style(this.domNode, "display", "none");
836 this.editPane = this._makeCreatePane(done, done);
837 this.editPane.startup();
839 dojo.place(this.editPane.domNode, this.domNode, "before");
842 this.onEditPane(this.editPane);
845 "hideDialog": function() {
846 this.editDialog.hide();
847 this.editDialog.destroy();
848 delete this.editDialog;
852 "hidePane": function() {
853 this.domNode.parentNode.removeChild(this.editPane.domNode);
854 this.editPane.destroy();
855 delete this.editPane;
856 dojo.style(this.domNode, "display", "block");
860 "deleteSelected": function() {
863 this.getSelectedItems().forEach(
865 var fmobj = new fieldmapper[self.fmClass]();
866 fmobj[self.fmIdentifier](
867 self.store.getIdentity(item)
869 (new openils.PermaCrud()).eliminate(
871 "oncomplete": function() {
872 self.store.deleteItem(item);
880 "getSelectedIDs": function() {
881 return this.getSelectedItems().map(
884 function(item) { return this.store.getIdentity(item); }
889 /* Return true if every row known to the grid is selected. Code
890 * that calls this function will do so when it thinks the user
891 * might actually mean "select everything this grid could show"
892 * even though we don't necessarily know (and the user hasn't
893 * necessarily noticed) whether the grid has been scrolled as far
894 * down as possible and all the possible results have been
895 * fetched by the grid's store. */
896 "everythingSeemsSelected": function() {
898 "[name=autogrid.selector]", this.domNode
900 function(c) { return (!c.disabled && !c.checked) }
904 /* Print the same data that the Flattener is feeding to the
905 * grid, sorted the same way too. Remove limit and offset (i.e.,
906 * print it all) unless those are passed in to the print() method.
908 "print": function(limit, offset, query_mixin) {
909 var coal = this._columnOrderingAndLabels();
911 "query": dojo.mixin({}, this.query, query_mixin),
913 "columns": coal.columns,
914 "labels": coal.labels
916 "onComplete": function(text) {
917 openils.Util.printHtmlString(text);
923 req.start = offset || 0;
925 req.queryOptions.all = true;
928 this.store.fetchToPrint(req);
931 "printSelected": function() {
933 id_blob[this.store.getIdentityAttributes()[0]] =
934 this.getSelectedIDs();
936 this.print(null, null, id_blob);
939 "setBaseQuery": function(query) { /* sets a persistent query
940 that always gets mixed in
941 with whatever you do in the
943 this._baseQuery = dojo.clone(this.query = query);
948 /* monkey patch so we can get more attributes from each column in the
949 * markup that specifies grid columns (table->thead->tr->[td,...])
952 var b = dojox.grid.cells._Base;
953 var orig_mf = b.markupFactory;
955 b.markupFactory = function(node, cellDef) {
956 orig_mf(node, cellDef);
959 ["fpath", "ffilter"], function(a) {
960 var value = dojo.attr(node, a);
966 /* fsort and _visible are different. Assume true unless defined. */
968 ["fsort", "_visible"], function(a) {
969 var val = dojo.attr(node, a);
970 cellDef[a] = (typeof val == "undefined" || val === null) ?
971 true : dojo.fromJson(val);
977 /* the secret to successfully subclassing dojox.grid.DataGrid */
978 openils.widget.FlattenerGrid.markupFactory =
979 dojox.grid.DataGrid.markupFactory;