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 "autoFieldFields": null,
21 "showLoadFilter": false, /* use FlattenerFilter(Dialog|Pane) */
22 "filterAlwaysInDiv": null, /* use FlattenerFilterPane and put its
23 content in this HTML element */
25 "filterInitializers": null,
26 "filterWidgetBuilders": null,
27 "filterSemaphore": null,
28 "filterSemaphoreCallback": null,
30 /* These potential constructor arguments may be useful to
31 * FlattenerGrid in their own right, and are passed to
36 "sortFieldReMap": null,
37 "defaultSort": null, /* whatever any part of the UI says will
39 "baseSort": null, /* will contains what the columnpicker
40 dictates, and precedes whatever the column
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 */
51 /* _generateMap() lives to interpret the attributes of the
52 * FlattenerGrid dijit itself plus those definined in
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.
60 "_generateMap": function() {
61 var map = this.mapClause = {};
62 var fields = this.structure[0].cells[0];
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
71 * If you need extra fields in the map for which display
72 * should *not* be true, use mapExtras.
75 fields, function(field) {
76 if (field.field.match(/^\+/))
77 return; /* special fields e.g. checkbox/line # */
81 "filter": (field.ffilter || false),
83 "path": field.fpath || field.field
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.*/
89 map[field.field]._label = field.name;
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
101 for (var key in this.mapExtras) {
102 if (typeof this.mapExtras[key] != "object") {
103 this.mapExtras[key] = {
104 "path": this.mapExtras[key],
111 dojo.mixin(map, this.mapExtras);
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();
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. */
133 "_cleanMapForStore": function(map) {
134 var clean = dojo.clone(map);
136 for (var column in clean) {
137 openils.Util.objectProperties(clean[column]).filter(
138 function(k) { return k.match(/^_/); }
140 function(k) { delete clean[column][k]; }
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);
155 var last_field, last_hint;
156 var orig_path = dojo.clone(path);
157 var field, field_def;
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. */
164 fieldmapper.IDL.fmclasses[hint].field_map[field];
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). */
172 "Lost our way in IDL at hint " + hint +
173 ", field " + field + "; may be ok"
178 if (field_def["class"]) {
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. */
187 "_calculateMapTerminii can't parse path " +
188 orig_path + " (at " + field + ")"
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.
199 if (orig_path.length > 1 && allow_selector_backoff &&
200 _fm_is_selector_for_class(hint, field_def.name)) {
206 field = field_def.name;
212 "label": field_def.label,
213 "datatype": datatype,
218 /* The FlattenerStore doesn't need this, but it has at least two
219 * uses: 1) FlattenerFilterDialog, 2) setting column header labels
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(
230 this.mapClause[column].path.split(/\./),
231 true /* allow selector backoff */
235 var terminus = dojo.mixin(
237 "simple_name": column,
238 "isfilter": this.mapClause[column].filter
241 if (this.mapClause[column]._label)
242 terminus.label = this.mapClause[column]._label;
244 this.mapTerminii.push(terminus);
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. */
255 this.structure[0].cells[0].forEach(
258 header.name = self.mapTerminii.filter(
260 return t.simple_name == header.field;
268 "_columnOrderingAndLabels": function() {
272 this.views.views[0].structure.cells[0].forEach(
274 if (!c.field.match(/^\+/)) {
276 columns.push(c.field);
281 return {"labels": labels, "columns": columns};
284 "_getAutoFieldFields": function(fmclass) {
286 fieldmapper.IDL.fmclasses[fmclass].fields)
289 return !field.virtual && field.datatype != "link";
292 function(a, b) { return a.label > b.label ? 1 : -1; }
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
299 "_addAutoCoreFields": function() {
300 var cell_list = this.structure[0].cells[0];
301 var fields = dojo.clone(
302 fieldmapper.IDL.fmclasses[this.fmClass].fields
304 function(a, b) { return a.label > b.label ? 1 : -1; }
308 fields, function(f) {
309 if (f.datatype == "link" || f.virtual)
312 if (cell_list.filter(
314 if (!c.fpath) return false;
315 return c.fpath.split(/\./)[0] == f.name;
330 "_addAutoFieldFields": function(paths) {
335 paths, function(path) {
336 /* The beginning is the end. */
337 var beginning = self._followPathToEnd(
338 self.fmClass, path.split(/\./), false
344 self._getAutoFieldFields(beginning.fmClass),
347 path + "." + field.name;
349 new RegExp("^" + would_be_path);
350 if (!self.structure[0].cells[0].filter(
353 c.fpath.match(wbp_re);
356 self.structure[0].cells[0].push({
357 "field": "AUTO_" + beginning.name +
359 "name": beginning.label + " - " +
362 "fpath": would_be_path,
373 "_addAutoFields": function() {
374 if (this.autoCoreFields)
375 this._addAutoCoreFields();
377 if (dojo.isArray(this.autoFieldFields))
378 this._addAutoFieldFields(this.autoFieldFields);
380 this.setStructure(this.structure);
383 "constructor": function(args) {
384 dojo.mixin(this, args);
386 this.fmIdentifier = this.fmIdentifier ||
387 fieldmapper.IDL.fmclasses[this.fmClass].pkey;
389 this.overrideEditWidgets = {};
390 this.overrideEditWidgetClass = {};
391 this.overrideWidgetArgs = {};
394 "startup": function() {
395 /* Save original query for further filtering later */
396 this._baseQuery = dojo.clone(this.query);
398 this._addAutoFields();
400 this._startupGridHelperColumns();
404 if (!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) });
411 this.columnPicker.onSortChange = dojo.hitch(this,
412 /* directly after, this.update() is called by the
413 column picker, causing a re-fetch */
415 this.store.baseSort = this._mapCPSortFields(fields)
419 this.columnPicker.load();
422 this.inherited(arguments);
425 "canSort": function(idx, skip_structure /* API abuse */) {
426 var initial = this.inherited(arguments);
428 /* idx is one-based instead of zero-based for a reason. */
429 var view_idx = Math.abs(idx) - 1;
432 this.views.views[0].structure.cells[0][view_idx].fsort
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) {
443 a[f.field] = f.direction;
450 "_finishStartup": function(sortFields) {
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
464 // pick up any column label changes
465 this.columnPicker.reloadStructure();
470 this._showing_create_pane = false;
472 if (this.editOnEnter)
473 this._applyEditOnEnter();
474 else if (this.singleEditStyle)
475 this._applySingleEditStyle();
477 /* Like AutoGrid's paginator, but we'll never have Back/Next
478 * links. Just a place to hold misc links */
483 "_setupLinks": function() {
484 this.linkHolder = new dijit.layout.ContentPane();
485 dojo.place(this.linkHolder.domNode, this.domNode, "before");
487 if (this.showLoadFilter) {
488 var which_filter_ui = this.filterAlwaysInDiv ?
489 "FlattenerFilterPane" : "FlattenerFilterDialog";
491 dojo.require("openils.widget." + which_filter_ui);
493 new openils.widget[which_filter_ui]({
494 "fmClass": this.fmClass,
495 "mapTerminii": this.mapTerminii,
496 "useDiv": this.filterAlwaysInDiv,
498 "initializers": this.filterInitializers,
499 "widgetBuilders": this.filterWidgetBuilders,
500 "suppressFilterFields": this.suppressFilterFields
503 this.filterUi.onApply = dojo.hitch(
504 this, function(filter) {
506 dojo.mixin(filter, this._baseQuery),
512 this.filterUi.startup();
514 if (this.filterSemaphore && this.filterSemaphore()) {
515 if (this.filterSemaphoreCallback)
516 this.filterSemaphoreCallback();
518 if (!this.filterAlwaysInDiv) {
521 "innerHTML": "Filter", /* XXX i18n */
522 "href": "javascript:void(0);",
523 "onclick": dojo.hitch(this, function() {
524 this.filterUi.show();
526 }, this.linkHolder.domNode
532 "refresh": function() {
533 this.fetchLock = false;
534 this._refresh(/* isRender */ true);
537 "_fetch": function() {
541 return this.inherited(arguments);
544 /* ******** below are methods mostly copied but
545 * slightly changed from AutoGrid ******** */
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);
556 /* capture keydown and launch edit dialog on enter */
557 "_applyEditOnEnter": function() {
558 this._applySingleEditStyle();
561 this, "onRowDblClick", function(e) {
562 if (this.editStyle == "pane")
564 this.selection.getFirstSelected(),
568 this._drawEditDialog(
569 this.selection.getFirstSelected(),
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")
582 this.selection.getFirstSelected(),
586 this._drawEditDialog(
587 this.selection.getFirstSelected(),
595 "_makeEditPane": function(storeItem, rowIndex, onPostSubmit, onCancel) {
597 var fmObject = (new openils.PermaCrud()).retrieve(
599 this.store.getIdentity(storeItem)
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});
617 if (grid.onPostUpdate)
618 grid.onPostUpdate(storeItem, rowIndex);
623 grid.views.views[0].getCellNode(
632 "onCancel": function() {
635 grid.views.views[0].getCellNode(
645 if (typeof this.editPaneOnSubmit == "function")
646 pane.onSubmit = this.editPaneOnSubmit;
648 pane.fieldOrder = this.fieldOrder;
649 pane.mode = "update";
653 "_makeCreatePane": function(onPostSubmit, onCancel) {
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);
668 grid.store.fetchItemByIdentity({
669 "identity": fmObject[grid.fmIdentifier](),
670 "onItem": function(item) {
671 grid.store.onNew(item);
679 grid.selection.select(grid.rowCount - 1);
680 grid.views.views[0].getCellNode(
688 onPostSubmit(fmObject);
690 "onCancel": function() { if (onCancel) onCancel(); }
693 if (typeof this.createPaneOnSubmit == "function")
694 pane.onSubmit = this.createPaneOnSubmit;
695 pane.fieldOrder = this.fieldOrder;
696 pane.mode = "create";
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
709 "_makeClonePane": function(storeItem,rowIndex,onPostSubmit,onCancel) {
710 var clonePane = this._makeCreatePane(onPostSubmit, onCancel);
711 var origPane = this._makeEditPane(storeItem, rowIndex);
715 origPane.fieldList, function(field) {
716 if (field.widget.widget.attr('disabled'))
719 var w = clonePane.fieldList.filter(
720 function(i) { return (i.name == field.name) }
724 w.widget.baseWidgetValue(field.widget.widget.attr('value'));
727 w.widget.onload = function() {
728 w.widget.baseWidgetValue(
729 field.widget.widget.attr('value')
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();
748 * Generates an EditDialog for object creation and displays it to the user
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();
758 "_drawEditPane": function(storeItem, rowIndex) {
759 var done = dojo.hitch(this, function() { this.hidePane(); });
761 dojo.style(this.domNode, "display", "none");
763 this.editPane = this._makeEditPane(storeItem, rowIndex, done, done);
764 this.editPane.startup();
765 dojo.place(this.editPane.domNode, this.domNode, "before");
768 this.onEditPane(this.editPane);
771 "showClonePane": function(onPostSubmit) {
772 var done = dojo.hitch(this, function() { this.hidePane(); });
773 var row = this.getFirstSelectedRow();
779 postSubmit = dojo.hitch(
780 this, function(result) {
781 onPostSubmit(this.getItem(row), result);
789 dojo.style(this.domNode, "display", "none");
790 this.editPane = this._makeClonePane(
791 this.getItem(row), row, postSubmit, done
793 dojo.place(this.editPane.domNode, this.domNode, "before");
795 this.onEditPane(this.editPane);
798 "showCreatePane": function() {
799 if (this._showing_create_pane)
801 this._showing_create_pane = true;
803 var done = dojo.hitch(
805 this._showing_create_pane = false;
810 dojo.style(this.domNode, "display", "none");
812 this.editPane = this._makeCreatePane(done, done);
813 this.editPane.startup();
815 dojo.place(this.editPane.domNode, this.domNode, "before");
818 this.onEditPane(this.editPane);
821 "hideDialog": function() {
822 this.editDialog.hide();
823 this.editDialog.destroy();
824 delete this.editDialog;
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");
836 "deleteSelected": function() {
839 this.getSelectedItems().forEach(
841 var fmobj = new fieldmapper[self.fmClass]();
842 fmobj[self.fmIdentifier](
843 self.store.getIdentity(item)
845 (new openils.PermaCrud()).eliminate(
847 "oncomplete": function() {
848 self.store.deleteItem(item);
856 "getSelectedIDs": function() {
857 return this.getSelectedItems().map(
860 function(item) { return this.store.getIdentity(item); }
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.
869 "print": function(limit, offset, query_mixin) {
870 var coal = this._columnOrderingAndLabels();
872 "query": dojo.mixin({}, this.query, query_mixin),
874 "columns": coal.columns,
875 "labels": coal.labels
877 "onComplete": function(text) {
878 openils.Util.printHtmlString(text);
884 req.start = offset || 0;
886 req.queryOptions.all = true;
889 this.store.fetchToPrint(req);
892 "printSelected": function() {
894 id_blob[this.store.getIdentityAttributes()[0]] =
895 this.getSelectedIDs();
897 this.print(null, null, id_blob);
902 /* monkey patch so we can get more attributes from each column in the
903 * markup that specifies grid columns (table->thead->tr->[td,...])
906 var b = dojox.grid.cells._Base;
907 var orig_mf = b.markupFactory;
909 b.markupFactory = function(node, cellDef) {
910 orig_mf(node, cellDef);
913 ["fpath", "ffilter"], function(a) {
914 var value = dojo.attr(node, a);
920 /* fsort and _visible are different. Assume true unless defined. */
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);
931 /* the secret to successfully subclassing dojox.grid.DataGrid */
932 openils.widget.FlattenerGrid.markupFactory =
933 dojox.grid.DataGrid.markupFactory;