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 "autoFieldFields": null,
22 "showLoadFilter": false, /* use FlattenerFilter(Dialog|Pane) */
23 "filterAlwaysInDiv": null, /* use FlattenerFilterPane and put its
24 content in this HTML element */
26 "filterInitializers": null,
27 "filterWidgetBuilders": null,
28 "filterSemaphore": null,
29 "filterSemaphoreCallback": null,
30 "baseQuery": null, /* Good place to mix in data from, say, context
31 OU selectors so that it should get mixed
32 correctly with the generated query from the
35 /* These potential constructor arguments may be useful to
36 * FlattenerGrid in their own right, and are passed to
41 "sortFieldReMap": null,
42 "defaultSort": null, /* whatever any part of the UI says will
44 "baseSort": null, /* will contains what the columnpicker
45 dictates, and precedes whatever the column
48 /* These potential constructor arguments are for functionality
49 * copied from AutoGrid */
50 "editOnEnter": false, /* also implies edit-on-dblclick */
51 "editStyle": "dialog", /* "dialog" or "pane" */
52 "requiredFields": null, /* affects create/edit dialogs */
53 "suppressEditFields": null, /* affects create/edit dialogs */
54 "suppressFilterFields": null, /* affects filter dialog */
56 /* _generateMap() lives to interpret the attributes of the
57 * FlattenerGrid dijit itself plus those definined in
61 * <th field="foo" ...>
62 * to build the map to hand to the FlattenerStore, which in turn
63 * uses it to query the flattener service.
65 "_generateMap": function() {
66 var map = this.mapClause = {};
67 var fields = this.structure[0].cells[0];
69 /* These are the fields defined in thead -> tr -> [th,th,...].
70 * For purposes of building the map, where each field has
71 * three boolean attributes "display", "sort" and "filter",
72 * assume "display" is always true for these.
73 * That doesn't mean that at the UI level we can't hide a
76 * If you need extra fields in the map for which display
77 * should *not* be true, use mapExtras.
80 fields, function(field) {
81 if (field.field.match(/^\+/))
82 return; /* special fields e.g. checkbox/line # */
86 "filter": (field.ffilter || false),
88 "path": field.fpath || field.field
90 /* The following attribute is not for the flattener
91 * service's benefit, but for other uses. We capture
92 * the hardcoded <th> value (the header label) if any.*/
94 map[field.field]._label = field.name;
99 /* It's not particularly useful to add simple fields, i.e.
100 * circ_lib: "circ_lib.name"
101 * to mapExtras, because by convention used elsewhere in
102 * Flattener, that gives all attributes, including
103 * display, a true value. Still, be consistent to avoid
106 for (var key in this.mapExtras) {
107 if (typeof this.mapExtras[key] != "object") {
108 this.mapExtras[key] = {
109 "path": this.mapExtras[key],
116 dojo.mixin(map, this.mapExtras);
119 /* Do this now, since we don't want a silently added
120 * identifier attribute in the terminii list (see its uses). */
121 this._calculateMapTerminii();
122 this._supplementHeaderNames();
124 /* make sure we always have a field for fm identifier */
125 if (!map[this.fmIdentifier]) {
126 map[this.fmIdentifier] = {
127 "path": this.fmIdentifier,
128 "display": true, /* Flattener displays it to us,
129 but we don't display to user. */
138 "_cleanMapForStore": function(map) {
139 var clean = dojo.clone(map);
141 for (var column in clean) {
142 openils.Util.objectProperties(clean[column]).filter(
143 function(k) { return k.match(/^_/); }
145 function(k) { delete clean[column][k]; }
152 /* Given the hint of a class to start at, follow path to the end
153 * and return information on the last field. */
154 "_followPathToEnd": function(hint, path, allow_selector_backoff) {
155 function _fm_is_selector_for_class(h, field) {
156 var cl = fieldmapper.IDL.fmclasses[h];
157 return (cl.field_map[cl.pkey].selector == field);
160 var last_field, last_hint;
161 var orig_path = dojo.clone(path);
162 var field, field_def;
164 while (field = path.shift()) {
165 /* XXX this assumes we have the whole IDL loaded. I
166 * guess we could teach this to work by loading classes
167 * on demand when we don't have the whole IDL loaded. */
169 fieldmapper.IDL.fmclasses[hint].field_map[field];
172 /* This can be ok in some cases. Columns following
173 * IDL paths involving links with a nonempty "map"
174 * attribute can be used for display only (no
175 * sort, no filter). */
177 "Lost our way in IDL at hint " + hint +
178 ", field " + field + "; may be ok"
183 if (field_def["class"]) {
187 hint = field_def["class"];
188 } else if (path.length) {
189 /* There are more fields left but we can't follow
190 * the chain via IDL any further. */
192 "_calculateMapTerminii can't parse path " +
193 orig_path + " (at " + field + ")"
198 var datatype = field_def.datatype;
199 var indirect = false;
200 /* If allowed, back off the last field in the path if it's a
201 * selector for its class, because the preceding field will be
202 * a better thing to hand to AutoFieldWidget.
204 if (orig_path.length > 1 && allow_selector_backoff &&
205 _fm_is_selector_for_class(hint, field_def.name)) {
211 field = field_def.name;
217 "label": field_def.label,
218 "datatype": datatype,
223 /* The FlattenerStore doesn't need this, but it has at least two
224 * uses: 1) FlattenerFilterDialog, 2) setting column header labels
227 * To call these 'Terminii' can be misleading. In certain
228 * (actually probably common) cases, they won't really be the last
229 * field in a path, but the next-to-last. Read on. */
230 "_calculateMapTerminii": function() {
231 this.mapTerminii = [];
232 for (var column in this.mapClause) {
233 var end = this._followPathToEnd(
235 this.mapClause[column].path.split(/\./),
236 true /* allow selector backoff */
240 var terminus = dojo.mixin(
242 "simple_name": column,
243 "isfilter": this.mapClause[column].filter
246 if (this.mapClause[column]._label)
247 terminus.label = this.mapClause[column]._label;
249 this.mapTerminii.push(terminus);
253 "_supplementHeaderNames": function() {
254 /* If we didn't give a particular header cell
255 * (<th>) a display name (the innerHTML of that <th>), then
256 * use the IDL to provide the label of the terminus of the
257 * flattener path for that column. It may be better than using
258 * the raw field name. */
260 this.structure[0].cells[0].forEach(
263 header.name = self.mapTerminii.filter(
265 return t.simple_name == header.field;
273 "_columnOrderingAndLabels": function() {
277 this.views.views[0].structure.cells[0].forEach(
279 if (!c.field.match(/^\+/)) {
281 columns.push(c.field);
286 return {"labels": labels, "columns": columns};
289 "_getAutoFieldFields": function(fmclass) {
291 fieldmapper.IDL.fmclasses[fmclass].fields)
294 return !field.virtual && field.datatype != "link";
297 function(a, b) { return a.label > b.label ? 1 : -1; }
301 /* Take our core class (this.fmClass) and add table columns for
302 * any field we don't already have covered by actual hard-coded
304 "_addAutoCoreFields": function() {
305 var cell_list = this.structure[0].cells[0];
306 var fields = dojo.clone(
307 fieldmapper.IDL.fmclasses[this.fmClass].fields
310 if (!this.autoCoreFieldsUnsorted) {
311 fields = fields.sort(
312 function(a, b) { return a.label > b.label ? 1 : -1; }
317 fields, function(f) {
318 if (f.datatype == "link" || f.virtual)
321 if (cell_list.filter(
323 if (!c.fpath) return false;
324 return c.fpath.split(/\./)[0] == f.name;
339 "_addAutoFieldFields": function(paths) {
344 paths, function(path) {
345 /* The beginning is the end. */
346 var beginning = self._followPathToEnd(
347 self.fmClass, path.split(/\./), false
353 self._getAutoFieldFields(beginning.fmClass),
356 path + "." + field.name;
358 new RegExp("^" + would_be_path);
359 if (!self.structure[0].cells[0].filter(
362 c.fpath.match(wbp_re);
365 self.structure[0].cells[0].push({
366 "field": "AUTO_" + beginning.name +
368 "name": beginning.label + " - " +
371 "fpath": would_be_path,
382 "_addAutoFields": function() {
383 if (this.autoCoreFields)
384 this._addAutoCoreFields();
386 if (dojo.isArray(this.autoFieldFields))
387 this._addAutoFieldFields(this.autoFieldFields);
389 this.setStructure(this.structure);
392 "constructor": function(args) {
393 dojo.mixin(this, args);
395 this.fmIdentifier = this.fmIdentifier ||
396 fieldmapper.IDL.fmclasses[this.fmClass].pkey;
398 this.overrideEditWidgets = {};
399 this.overrideEditWidgetClass = {};
400 this.overrideWidgetArgs = {};
403 "startup": function() {
404 /* Save original query for further filtering later, unless
405 * we've already defined baseQuery from the outside, in
406 * which case it persists. */
408 this.baseQuery = dojo.clone(this.query);
410 this._addAutoFields();
412 this._startupGridHelperColumns();
416 if (!this.columnPicker) {
418 new openils.widget.GridColumnPicker(
419 null, this.columnPersistKey, this);
420 this.columnPicker.onLoad = dojo.hitch(
421 this, function(opts) { this._finishStartup(opts.sortFields) });
423 this.columnPicker.onSortChange = dojo.hitch(this,
424 /* directly after, this.update() is called by the
425 column picker, causing a re-fetch */
427 this.store.baseSort = this._mapCPSortFields(fields)
431 this.columnPicker.load();
434 this.inherited(arguments);
437 "canSort": function(idx, skip_structure /* API abuse */) {
438 var initial = this.inherited(arguments);
440 /* idx is one-based instead of zero-based for a reason. */
441 var view_idx = Math.abs(idx) - 1;
444 this.views.views[0].structure.cells[0][view_idx].fsort
448 /* Maps ColumnPicker sort fields to the correct format.
449 If no sort fields specified, falls back to defaultSort */
450 "_mapCPSortFields": function(sortFields) {
451 var sort = this.defaultSort;
452 if (sortFields.length) {
453 sort = sortFields.map(function(f) {
455 a[f.field] = f.direction;
462 "_finishStartup": function(sortFields) {
464 this._setStore( /* Seriously, let's leave this as _setStore. */
465 new openils.FlattenerStore({
466 "fmClass": this.fmClass,
467 "fmIdentifier": this.fmIdentifier,
468 "mapClause": this._cleanMapForStore(this.mapClause),
469 "baseSort": this.baseSort,
470 "defaultSort": this._mapCPSortFields(sortFields),
471 "sortFieldReMap": this.sortFieldReMap
476 // pick up any column label changes
477 this.columnPicker.reloadStructure();
482 this._showing_create_pane = false;
484 if (this.editOnEnter)
485 this._applyEditOnEnter();
486 else if (this.singleEditStyle)
487 this._applySingleEditStyle();
489 /* Like AutoGrid's paginator, but we'll never have Back/Next
490 * links. Just a place to hold misc links */
495 "_setupLinks": function() {
496 this.linkHolder = new dijit.layout.ContentPane();
497 dojo.place(this.linkHolder.domNode, this.domNode, "before");
499 if (this.showLoadFilter) {
500 var which_filter_ui = this.filterAlwaysInDiv ?
501 "FlattenerFilterPane" : "FlattenerFilterDialog";
503 dojo.require("openils.widget." + which_filter_ui);
505 new openils.widget[which_filter_ui]({
506 "fmClass": this.fmClass,
507 "mapTerminii": this.mapTerminii,
508 "useDiv": this.filterAlwaysInDiv,
510 "initializers": this.filterInitializers,
511 "widgetBuilders": this.filterWidgetBuilders,
512 "suppressFilterFields": this.suppressFilterFields
515 this.filterUi.onApply = dojo.hitch(
516 this, function(filter) {
518 dojo.mixin(filter, this.baseQuery),
524 this.filterUi.startup();
526 if (this.filterSemaphore && this.filterSemaphore()) {
527 if (this.filterSemaphoreCallback)
528 this.filterSemaphoreCallback();
530 if (!this.filterAlwaysInDiv) {
533 "innerHTML": "Filter", /* XXX i18n */
534 "href": "javascript:void(0);",
535 "onclick": dojo.hitch(this, function() {
536 this.filterUi.show();
538 }, this.linkHolder.domNode
544 "refresh": function() {
545 this.fetchLock = false;
546 this._refresh(/* isRender */ true);
549 "_fetch": function() {
553 return this.inherited(arguments);
556 /* ******** below are methods mostly copied but
557 * slightly changed from AutoGrid ******** */
559 "_applySingleEditStyle": function() {
560 this.onMouseOverRow = function(e) {};
561 this.onMouseOutRow = function(e) {};
562 this.onCellFocus = function(cell, rowIndex) {
563 this.selection.deselectAll();
564 this.selection.select(this.focus.rowIndex);
568 /* capture keydown and launch edit dialog on enter */
569 "_applyEditOnEnter": function() {
570 this._applySingleEditStyle();
573 this, "onRowDblClick", function(e) {
574 if (this.editStyle == "pane")
576 this.selection.getFirstSelected(),
580 this._drawEditDialog(
581 this.selection.getFirstSelected(),
588 this, "onKeyDown", function(e) {
589 if (e.keyCode == dojo.keys.ENTER) {
590 this.selection.deselectAll();
591 this.selection.select(this.focus.rowIndex);
592 if (this.editStyle == "pane")
594 this.selection.getFirstSelected(),
598 this._drawEditDialog(
599 this.selection.getFirstSelected(),
607 "_makeEditPane": function(storeItem, rowIndex, onPostSubmit, onCancel) {
609 var fmObject = (new openils.PermaCrud()).retrieve(
611 this.store.getIdentity(storeItem)
614 var pane = new openils.widget.EditPane({
615 "fmObject": fmObject,
616 "hideSaveButton": this.editReadOnly,
617 "readOnly": this.editReadOnly,
618 "overrideWidgets": this.overrideEditWidgets,
619 "overrideWidgetClass": this.overrideEditWidgetClass,
620 "overrideWidgetArgs": this.overrideWidgetArgs,
621 "disableWidgetTest": this.disableWidgetTest,
622 "requiredFields": this.requiredFields,
623 "suppressFields": this.suppressEditFields,
624 "onPostSubmit": function() {
625 /* ask the store to call flattener specially to get
626 * the flat row related to only this fmobj */
627 grid.store.loadItem({"force": true, "item": storeItem});
629 if (grid.onPostUpdate)
630 grid.onPostUpdate(storeItem, rowIndex);
635 grid.views.views[0].getCellNode(
644 "onCancel": function() {
647 grid.views.views[0].getCellNode(
657 if (typeof this.editPaneOnSubmit == "function")
658 pane.onSubmit = this.editPaneOnSubmit;
660 pane.fieldOrder = this.fieldOrder;
661 pane.mode = "update";
665 "_makeCreatePane": function(onPostSubmit, onCancel) {
667 var pane = new openils.widget.EditPane({
668 "fmClass": this.fmClass,
669 "overrideWidgets": this.overrideEditWidgets,
670 "overrideWidgetClass": this.overrideEditWidgetClass,
671 "overrideWidgetArgs": this.overrideWidgetArgs,
672 "disableWidgetTest": this.disableWidgetTest,
673 "requiredFields": this.requiredFields,
674 "suppressFields": this.suppressEditFields,
675 "onPostSubmit": function(req, cudResults) {
676 var fmObject = cudResults[0];
677 if (grid.onPostCreate)
678 grid.onPostCreate(fmObject);
680 grid.store.fetchItemByIdentity({
681 "identity": fmObject[grid.fmIdentifier](),
682 "onItem": function(item) {
683 grid.store.onNew(item);
691 grid.selection.select(grid.rowCount - 1);
692 grid.views.views[0].getCellNode(
700 onPostSubmit(fmObject);
702 "onCancel": function() { if (onCancel) onCancel(); }
705 if (typeof this.createPaneOnSubmit == "function")
706 pane.onSubmit = this.createPaneOnSubmit;
707 pane.fieldOrder = this.fieldOrder;
708 pane.mode = "create";
713 * Creates an EditPane with a copy of the data from the provided store
714 * item for cloning said item
715 * @param {Object} storeItem Dojo data item
716 * @param {Number} rowIndex The Grid row index of the item to be cloned
717 * @param {Function} onPostSubmit Optional callback for post-submit behavior
718 * @param {Function} onCancel Optional callback for clone cancelation
719 * @return {Object} The clone EditPane
721 "_makeClonePane": function(storeItem,rowIndex,onPostSubmit,onCancel) {
722 var clonePane = this._makeCreatePane(onPostSubmit, onCancel);
723 var origPane = this._makeEditPane(storeItem, rowIndex);
727 origPane.fieldList, function(field) {
728 if (field.widget.widget.attr('disabled'))
731 var w = clonePane.fieldList.filter(
732 function(i) { return (i.name == field.name) }
736 w.widget.baseWidgetValue(field.widget.widget.attr('value'));
739 w.widget.onload = function() {
740 w.widget.baseWidgetValue(
741 field.widget.widget.attr('value')
751 "_drawEditDialog": function(storeItem, rowIndex) {
752 var done = dojo.hitch(this, function() { this.hideDialog(); });
753 var pane = this._makeEditPane(storeItem, rowIndex, done, done);
754 this.editDialog = new openils.widget.EditDialog({editPane:pane});
755 this.editDialog.startup();
756 this.editDialog.show();
760 * Generates an EditDialog for object creation and displays it to the user
762 "showCreateDialog": function() {
763 var done = dojo.hitch(this, function() { this.hideDialog(); });
764 var pane = this._makeCreatePane(done, done);
765 this.editDialog = new openils.widget.EditDialog({editPane:pane});
766 this.editDialog.startup();
767 this.editDialog.show();
770 "_drawEditPane": function(storeItem, rowIndex) {
771 var done = dojo.hitch(this, function() { this.hidePane(); });
773 dojo.style(this.domNode, "display", "none");
775 this.editPane = this._makeEditPane(storeItem, rowIndex, done, done);
776 this.editPane.startup();
777 dojo.place(this.editPane.domNode, this.domNode, "before");
780 this.onEditPane(this.editPane);
783 "showClonePane": function(onPostSubmit) {
784 var done = dojo.hitch(this, function() { this.hidePane(); });
785 var row = this.getFirstSelectedRow();
791 postSubmit = dojo.hitch(
792 this, function(result) {
793 onPostSubmit(this.getItem(row), result);
801 dojo.style(this.domNode, "display", "none");
802 this.editPane = this._makeClonePane(
803 this.getItem(row), row, postSubmit, done
805 dojo.place(this.editPane.domNode, this.domNode, "before");
807 this.onEditPane(this.editPane);
810 "showCreatePane": function() {
811 if (this._showing_create_pane)
813 this._showing_create_pane = true;
815 var done = dojo.hitch(
817 this._showing_create_pane = false;
822 dojo.style(this.domNode, "display", "none");
824 this.editPane = this._makeCreatePane(done, done);
825 this.editPane.startup();
827 dojo.place(this.editPane.domNode, this.domNode, "before");
830 this.onEditPane(this.editPane);
833 "hideDialog": function() {
834 this.editDialog.hide();
835 this.editDialog.destroy();
836 delete this.editDialog;
840 "hidePane": function() {
841 this.domNode.parentNode.removeChild(this.editPane.domNode);
842 this.editPane.destroy();
843 delete this.editPane;
844 dojo.style(this.domNode, "display", "block");
848 "deleteSelected": function() {
851 this.getSelectedItems().forEach(
853 var fmobj = new fieldmapper[self.fmClass]();
854 fmobj[self.fmIdentifier](
855 self.store.getIdentity(item)
857 (new openils.PermaCrud()).eliminate(
859 "oncomplete": function() {
860 self.store.deleteItem(item);
868 "getSelectedIDs": function() {
869 return this.getSelectedItems().map(
872 function(item) { return this.store.getIdentity(item); }
877 /* Return true if every row known to the grid is selected. Code
878 * that calls this function will do so when it thinks the user
879 * might actually mean "select everything this grid could show"
880 * even though we don't necessarily know (and the user hasn't
881 * necessarily noticed) whether the grid has been scrolled as far
882 * down as possible and all the possible results have been
883 * fetched by the grid's store. */
884 "everythingSeemsSelected": function() {
886 "[name=autogrid.selector]", this.domNode
888 function(c) { return (!c.disabled && !c.checked) }
892 /* Print the same data that the Flattener is feeding to the
893 * grid, sorted the same way too. Remove limit and offset (i.e.,
894 * print it all) unless those are passed in to the print() method.
896 "print": function(limit, offset, query_mixin) {
897 var coal = this._columnOrderingAndLabels();
899 "query": dojo.mixin({}, this.query, query_mixin),
901 "columns": coal.columns,
902 "labels": coal.labels
904 "onComplete": function(text) {
905 openils.Util.printHtmlString(text);
911 req.start = offset || 0;
913 req.queryOptions.all = true;
916 this.store.fetchToPrint(req);
919 "printSelected": function() {
921 id_blob[this.store.getIdentityAttributes()[0]] =
922 this.getSelectedIDs();
924 this.print(null, null, id_blob);
929 /* monkey patch so we can get more attributes from each column in the
930 * markup that specifies grid columns (table->thead->tr->[td,...])
933 var b = dojox.grid.cells._Base;
934 var orig_mf = b.markupFactory;
936 b.markupFactory = function(node, cellDef) {
937 orig_mf(node, cellDef);
940 ["fpath", "ffilter"], function(a) {
941 var value = dojo.attr(node, a);
947 /* fsort and _visible are different. Assume true unless defined. */
949 ["fsort", "_visible"], function(a) {
950 var val = dojo.attr(node, a);
951 cellDef[a] = (typeof val == "undefined" || val === null) ?
952 true : dojo.fromJson(val);
958 /* the secret to successfully subclassing dojox.grid.DataGrid */
959 openils.widget.FlattenerGrid.markupFactory =
960 dojox.grid.DataGrid.markupFactory;