1 if(!dojo._hasResource['openils.widget.AutoGrid']) {
2 dojo.provide('openils.widget.AutoGrid');
3 dojo.require('dojox.grid.DataGrid');
4 dojo.require("dojox.encoding.base64");
5 dojo.require('dijit.layout.ContentPane');
6 dojo.require('openils.widget.AutoWidget');
7 dojo.require('openils.widget.AutoFieldWidget');
8 dojo.require('openils.widget.EditPane');
9 dojo.require('openils.widget.EditDialog');
10 dojo.require('openils.widget.GridColumnPicker');
11 dojo.require('openils.widget._GridHelperColumns');
12 dojo.require('openils.Util');
13 dojo.require('openils.CGI');
14 dojo.requireLocalization('openils.widget', 'AutoFieldWidget');
17 'openils.widget.AutoGrid',
18 [dojox.grid.DataGrid, openils.widget.AutoWidget, openils.widget._GridHelperColumns],
21 /* if true, pop up an edit dialog when user hits Enter on a give row */
22 editPaneOnSubmit : null,
23 onPostSubmit : null, // called after any CRUD actions are complete
24 createPaneOnSubmit : null,
26 defaultCellWidth : null,
29 suppressFields : null,
30 suppressEditFields : null,
31 suppressFilterFields : null,
32 showColumnPicker : false,
33 columnPersistKey : null,
36 requiredFields : null,
37 hidePaginator : false,
38 showLoadFilter : true,
39 onItemReceived : null,
40 suppressLinkedFields : null, // list of fields whose linked display data should not be fetched from the server
41 urlNavigation : false,
43 // When using urlNavigation, this is a stash where the
44 // caller can place arbitrary data to be passed around
48 /* by default, don't show auto-generated (sequence) fields */
49 showSequenceFields : false,
51 startup : function() {
53 this.selectionMode = 'single';
54 this.sequence = openils.widget.AutoGrid.sequence++;
55 openils.widget.AutoGrid.gridCache[this.sequence] = this;
56 this.inherited(arguments);
59 this.setStructure(this._compileStructure());
60 this._startupGridHelperColumns();
61 this.setStructure(this.structure); // required after _startupGridHelper()
63 this.setStore(this.buildAutoStore());
64 this.cachedQueryOpts = {};
65 this._showing_create_pane = false;
66 this.overrideEditWidgets = {};
67 this.overrideEditWidgetClass = {};
68 this.overrideWidgetArgs = {};
71 this.nls = dojo.i18n.getLocalization('openils.widget', 'AutoFieldWidget');
74 this._applyEditOnEnter();
75 else if(this.singleEditStyle)
76 this._applySingleEditStyle();
78 if(!this.hidePaginator) {
80 this.paginator = new dijit.layout.ContentPane();
83 var back = dojo.create('a', {
84 innerHTML : self.nls.BACK,
85 style : 'padding-right:6px;',
86 href : 'javascript:void(0);',
87 onclick : function() {
88 self.cachedQueryOpts.offset = self.displayOffset -= self.displayLimit;
89 if(self.displayOffset < 0)
90 self.cachedQueryOpts.offset = self.displayOffset = 0;
95 // TODO: support self.urlNavigation
96 var forw = dojo.create('a', {
97 innerHTML : self.nls.NEXT,
98 style : 'padding-right:6px;',
99 href : 'javascript:void(0);',
100 onclick : function() {
101 self.cachedQueryOpts.offset = self.displayOffset += self.displayLimit;
102 if (self.urlNavigation) {
103 self.applyAndExecuteUrlOps(
104 self.cachedQueryOpts.offset,
105 self.cachedQuerySearch
113 dojo.place(this.paginator.domNode, this.domNode, 'before');
114 dojo.place(back, this.paginator.domNode);
115 dojo.place(forw, this.paginator.domNode);
117 if(this.showLoadFilter) {
118 dojo.require('openils.widget.PCrudFilterDialog');
121 innerHTML : self.nls.FILTER,
122 style : 'padding-right:6px;',
123 href : 'javascript:void(0);',
124 onclick : function() {
125 if (!self.filterDialog) {
127 self.filterDialog = new openils.widget.PCrudFilterDialog(
128 {fmClass:self.fmClass, suppressFilterFields:self.suppressFilterFields})
130 self.filterDialog.onApply = function(filter) {
131 if (self.urlNavigation) {
132 self.applyAndExecuteUrlOps(0, filter);
135 self.cachedQuerySearch = filter;
138 dojo.mixin( { offset : 0 }, self.cachedQueryOpts ),
139 self.cachedQuerySearch,
145 self.filterDialog.startup();
147 self.filterDialog.show();
150 this.paginator.domNode
155 this.loadProgressIndicator = dojo.create('img', {
156 src:'/opac/images/progressbar_green.gif', // TODO configured path
157 style:'height:16px;width:16px;'
159 dojo.place(this.loadProgressIndicator, this.paginator.domNode);
163 // set URL options then jump to the new URL
164 applyAndExecuteUrlOps : function(offset, filter) {
166 filter : filter, // TODO: load from query cache on offset-only change
167 offset : offset || 0,
168 userData : this.urlUserData
171 var encoded = dojox.encoding.base64.encode(
174 .map(function(c) { return c.charCodeAt(0); })
177 var cgi = new openils.CGI();
178 cgi.param('djgridops', encoded);
179 location.href = cgi.url();
182 // extract options encoded in the URL
183 extractUrlOps : function() {
184 var ops = new openils.CGI().param('djgridops');
188 dojox.encoding.base64.decode(ops)
189 .map(function(b) {return String.fromCharCode(b)})
194 this.urlUserData = ops.userData;
197 hideLoadProgressIndicator : function() {
198 dojo.style(this.loadProgressIndicator, 'visibility', 'hidden');
201 showLoadProgressIndicator : function() {
202 dojo.style(this.loadProgressIndicator, 'visibility', 'visible');
205 _compileStructure : function() {
206 var existing = (this.structure && this.structure[0].cells[0]) ?
207 this.structure[0].cells[0] : [];
211 function pushEntry(entry) {
212 if(self.suppressFields) {
213 if(dojo.indexOf(self.suppressFields, entry.field) != -1)
217 entry.get = openils.widget.AutoGrid.defaultGetter
218 if(!entry.width && self.defaultCellWidth)
219 entry.width = self.defaultCellWidth;
223 if(!this.fieldOrder) {
224 /* no order defined, start with any explicit grid fields */
225 for(var e in existing) {
226 var entry = existing[e];
227 var field = this.fmIDL.fields.filter(
228 function(i){return (i.name == entry.field)})[0];
229 if(field) entry.name = entry.name || field.label;
234 for(var f in this.sortedFieldList) {
235 var field = this.sortedFieldList[f];
236 if(!field || field.virtual) continue;
238 // field was already added above
239 if(fields.filter(function(i){return (i.field == field.name)})[0])
242 var entry = existing.filter(function(i){return (i.field == field.name)})[0];
244 entry.name = entry.name || field.label;
246 // unless specifically requested, hide sequence fields
247 if(!this.showSequenceFields && field.name == this.fmIDL.pkey && this.fmIDL.pkey_sequence)
250 entry = {field:field.name, name:field.label};
255 if(this.fieldOrder) {
256 /* append any explicit non-IDL grid fields to the end */
257 for(var e in existing) {
258 var entry = existing[e];
259 var field = fields.filter(
260 function(i){return (i.field == entry.field)})[0];
261 if(field) continue; // don't duplicate
266 return [{cells: [fields]}];
270 * @return {Array} List of every fieldmapper object in the data store
272 getAllObjects : function() {
276 onComplete : function(list) {
279 objs.push(new fieldmapper[self.fmClass]().fromStoreItem(item));
288 * Deletes the underlying object for all selected rows
290 deleteSelected : function() {
291 var items = this.getSelectedItems();
292 var total = items.length;
294 dojo.require('openils.PermaCrud');
297 var fmObject = new fieldmapper[self.fmClass]().fromStoreItem(item);
298 new openils.PermaCrud()['eliminate'](
300 oncomplete : function(r) {
301 self.store.deleteItem(item);
302 if (--total < 1 && self.onPostSubmit) {
312 _applySingleEditStyle : function() {
313 this.onMouseOverRow = function(e) {};
314 this.onMouseOutRow = function(e) {};
315 this.onCellFocus = function(cell, rowIndex) {
316 this.selection.deselectAll();
317 this.selection.select(this.focus.rowIndex);
321 /* capture keydown and launch edit dialog on enter */
322 _applyEditOnEnter : function() {
323 this._applySingleEditStyle();
325 dojo.connect(this, 'onRowDblClick',
327 if(this.editStyle == 'pane')
328 this._drawEditPane(this.selection.getFirstSelected(), this.focus.rowIndex);
330 this._drawEditDialog(this.selection.getFirstSelected(), this.focus.rowIndex);
334 dojo.connect(this, 'onKeyDown',
336 if(e.keyCode == dojo.keys.ENTER) {
337 this.selection.deselectAll();
338 this.selection.select(this.focus.rowIndex);
339 if(this.editStyle == 'pane')
340 this._drawEditPane(this.selection.getFirstSelected(), this.focus.rowIndex);
342 this._drawEditDialog(this.selection.getFirstSelected(), this.focus.rowIndex);
348 _makeEditPane : function(storeItem, rowIndex, onPostSubmit, onCancel) {
350 var fmObject = new fieldmapper[this.fmClass]().fromStoreItem(storeItem);
351 var idents = grid.store.getIdentityAttributes();
353 var pane = new openils.widget.EditPane({
355 hideSaveButton : this.editReadOnly,
356 readOnly : this.editReadOnly,
357 overrideWidgets : this.overrideEditWidgets,
358 overrideWidgetClass : this.overrideEditWidgetClass,
359 overrideWidgetArgs : this.overrideWidgetArgs,
360 disableWidgetTest : this.disableWidgetTest,
361 requiredFields : this.requiredFields,
362 suppressFields : this.suppressEditFields,
363 onPostSubmit : function() {
364 for(var i in fmObject._fields) {
365 var field = fmObject._fields[i];
366 if(idents.filter(function(j){return (j == field)})[0])
367 continue; // don't try to edit an identifier field
368 grid.store.setValue(storeItem, field, fmObject[field]());
370 if(grid.onPostUpdate)
371 grid.onPostUpdate(storeItem, rowIndex);
375 grid.views.views[0].getCellNode(rowIndex, 0).focus();
381 if (grid.onPostSubmit)
385 onCancel : function() {
386 setTimeout(function(){
387 grid.views.views[0].getCellNode(rowIndex, 0).focus();},200);
388 if(onCancel) onCancel();
392 if (typeof this.editPaneOnSubmit == "function")
393 pane.onSubmit = this.editPaneOnSubmit;
394 pane.fieldOrder = this.fieldOrder;
395 pane.mode = 'update';
399 _makeCreatePane : function(onPostSubmit, onCancel) {
401 var pane = new openils.widget.EditPane({
402 fmClass : this.fmClass,
403 overrideWidgets : this.overrideEditWidgets,
404 overrideWidgetClass : this.overrideEditWidgetClass,
405 overrideWidgetArgs : this.overrideWidgetArgs,
406 disableWidgetTest : this.disableWidgetTest,
407 requiredFields : this.requiredFields,
408 suppressFields : this.suppressEditFields,
409 onPostSubmit : function(req, cudResults) {
410 var fmObject = cudResults[0];
411 if(grid.onPostCreate)
412 grid.onPostCreate(fmObject);
414 grid.store.newItem(fmObject.toStoreItem());
415 setTimeout(function(){
417 grid.selection.select(grid.rowCount-1);
418 grid.views.views[0].getCellNode(grid.rowCount-1, 1).focus();
422 onPostSubmit(fmObject);
423 if (grid.onPostSubmit)
426 onCancel : function() {
427 if(onCancel) onCancel();
430 if (typeof this.createPaneOnSubmit == "function")
431 pane.onSubmit = this.createPaneOnSubmit;
432 pane.fieldOrder = this.fieldOrder;
433 pane.mode = 'create';
438 * Creates an EditPane with a copy of the data from the provided store
439 * item for cloning said item
440 * @param {Object} storeItem Dojo data item
441 * @param {Number} rowIndex The Grid row index of the item to be cloned
442 * @param {Function} onPostSubmit Optional callback for post-submit behavior
443 * @param {Function} onCancel Optional callback for clone cancelation
444 * @return {Object} The clone EditPane
446 _makeClonePane : function(storeItem, rowIndex, onPostSubmit, onCancel) {
447 var clonePane = this._makeCreatePane(onPostSubmit, onCancel);
448 var origPane = this._makeEditPane(storeItem, rowIndex);
451 dojo.forEach(origPane.fieldList,
453 if(field.widget.widget.attr('disabled')) return;
454 var w = clonePane.fieldList.filter(
455 function(i) { return (i.name == field.name) })[0];
456 w.widget.baseWidgetValue(field.widget.widget.attr('value')); // sync widgets
457 w.widget.onload = function(){w.widget.baseWidgetValue(field.widget.widget.attr('value'))}; // async widgets
465 _drawEditDialog : function(storeItem, rowIndex) {
467 var done = function() { self.hideDialog(); };
468 var pane = this._makeEditPane(storeItem, rowIndex, done, done);
469 this.editDialog = new openils.widget.EditDialog({editPane:pane});
470 this.editDialog.startup();
471 this.editDialog.show();
472 if(this.onEditPane) this.onEditPane(pane);
476 * Generates an EditDialog for object creation and displays it to the user
478 showCreateDialog : function() {
480 var done = function() { self.hideDialog(); };
481 var pane = this._makeCreatePane(done, done);
482 this.editDialog = new openils.widget.EditDialog({editPane:pane});
483 this.editDialog.startup();
484 this.editDialog.show();
487 _drawEditPane : function(storeItem, rowIndex) {
489 var done = function() { self.hidePane(); };
490 dojo.style(this.domNode, 'display', 'none');
491 this.editPane = this._makeEditPane(storeItem, rowIndex, done, done);
492 this.editPane.startup();
493 this.domNode.parentNode.insertBefore(this.editPane.domNode, this.domNode);
494 if(this.onEditPane) this.onEditPane(this.editPane);
497 showClonePane : function(onPostSubmit) {
499 var done = function() { self.hidePane(); };
502 var row = this.getFirstSelectedRow();
505 var postSubmit = (onPostSubmit) ?
506 function(result) { onPostSubmit(self.getItem(row), result); self.hidePane(); } :
509 dojo.style(this.domNode, 'display', 'none');
510 this.editPane = this._makeClonePane(this.getItem(row), row, postSubmit, done);
511 this.domNode.parentNode.insertBefore(this.editPane.domNode, this.domNode);
512 if(this.onEditPane) this.onEditPane(this.editPane);
515 showCreatePane : function() {
516 if (this._showing_create_pane)
518 this._showing_create_pane = true;
521 var done = function() {
522 self._showing_create_pane = false;
525 dojo.style(this.domNode, 'display', 'none');
526 this.editPane = this._makeCreatePane(done, done);
527 this.editPane.startup();
528 this.domNode.parentNode.insertBefore(this.editPane.domNode, this.domNode);
529 if(this.onEditPane) this.onEditPane(this.editPane);
532 hideDialog : function() {
533 this.editDialog.hide();
534 this.editDialog.destroy();
535 delete this.editDialog;
539 hidePane : function() {
540 this.domNode.parentNode.removeChild(this.editPane.domNode);
541 this.editPane.destroy();
542 delete this.editPane;
543 dojo.style(this.domNode, 'display', 'block');
547 resetStore : function() {
548 this.setStore(this.buildAutoStore());
551 refresh : function(opts, search) {
552 opts = opts || this.cachedQueryOpts;
553 search = search || this.cachedQuerySearch;
558 this._loadAll(opts, search);
561 // called after a sort change occurs within the column picker
562 cpSortHandler : function(fields) {
563 console.log("AutoGrod cpSortHandler(): " + js2JSON(fields));
564 // user-defined sort handler
565 if (this.onSortChange) {
566 this.onSortChange(fields)
568 // default sort handler
570 if (!this.cachedQueryOpts)
571 this.cachedQueryOpts = {};
573 dojo.forEach(fields, function(f) {
574 if (order_by) order_by += ',';
575 order_by += f.field + ' ' + f.direction
577 this.cachedQueryOpts.order_by = {};
578 this.cachedQueryOpts.order_by[this.fmClass] = order_by;
583 loadAll : function(opts, search, filter_triggered) {
586 // first we have to load the column picker to determine the sort fields.
588 if(this.showColumnPicker && !this.columnPicker) {
589 if(!this.columnPersistKey) {
590 console.error("No columnPersistKey defined");
591 this.columnPicker = {};
593 this.columnPicker = new openils.widget.GridColumnPicker(
594 openils.User.authtoken, this.columnPersistKey, this);
595 this.columnPicker.onSortChange = function(fields) {_this.cpSortHandler(fields)};
596 this.columnPicker.onLoad = function(cpOpts) {
597 _this.cachedQueryOpts = opts;
598 _this.cachedQuerySearch = search;
599 _this.cpSortHandler(cpOpts.sortFields); // calls refresh() -> _loadAll()
601 this.columnPicker.load();
606 // column picker not wanted or already loaded
607 this._loadAll(opts, search, filter_triggered);
610 _loadAll : function(opts, search, filter_triggered) {
613 dojo.require('openils.PermaCrud');
614 if(this.loadProgressIndicator)
615 dojo.style(this.loadProgressIndicator, 'visibility', 'visible');
617 {limit : this.displayLimit, offset : this.displayOffset},
620 opts = dojo.mixin(opts, {
623 onresponse : function(r) {
624 var item = openils.Util.readResponse(r);
625 if (self.onItemReceived)
626 self.onItemReceived(item);
627 self.store.newItem(item.toStoreItem());
629 oncomplete : function() {
630 if(self.loadProgressIndicator)
631 dojo.style(self.loadProgressIndicator, 'visibility', 'hidden');
635 this.cachedQuerySearch = search;
636 this.cachedQueryOpts = opts;
638 // retain the most recent external loadAll
639 if (!filter_triggered || !this.preFilterSearch)
640 this.preFilterSearch = dojo.clone( this.cachedQuerySearch );
642 if (this.urlNavigation) {
643 if (!this.urlOpsApplied) {
644 this.urlOpsApplied = true;
645 // on the first page load, apply the ops from the URL
647 opts.offset = self.displayOffset = this.urlOps.offset;
648 search = dojo.mixin(search || {}, this.urlOps.filter);
651 // subsequent calls to loadAll() suggest changing
652 // filters / paging / etc, propagate new values
654 return this.applyAndExecuteUrlOps(
656 this.cachedQuerySearch
662 new openils.PermaCrud().search(this.fmClass, search, opts);
664 new openils.PermaCrud().retrieveAll(this.fmClass, opts);
669 // static ID generater seed
670 openils.widget.AutoGrid.sequence = 0;
671 openils.widget.AutoGrid.gridCache = {};
673 openils.widget.AutoGrid.markupFactory = dojox.grid.DataGrid.markupFactory;
675 openils.widget.AutoGrid.defaultGetter = function(rowIndex, item) {
677 if(!this.grid.overrideWidgetArgs[this.field])
678 this.grid.overrideWidgetArgs[this.field] = {};
679 var val = this.grid.store.getValue(item, this.field);
680 var autoWidget = new openils.widget.AutoFieldWidget(dojo.mixin({
681 fmClass: this.grid.fmClass,
685 forceSync : true, // prevents many simultaneous requests for the same data
686 suppressLinkedFields : this.grid.suppressLinkedFields
687 },this.grid.overrideWidgetArgs[this.field]));
692 // With proper caching, this should not be necessary to prevent grid render flickering
697 var node = _this.grid.getCell(_this.index).view.getCellNode(rowIndex, _this.index);
699 node.innerHTML = ww.getDisplayString();
705 return autoWidget.getDisplayString();
708 openils.widget.AutoGrid.orgUnitGetter = function(rowIndex, item) {
709 if (!item) return "";
711 var aou_id = this.grid.store.getValue(item, this.field);
713 return fieldmapper.aou.findOrgUnit(aou_id).shortname();