73f3509c67c29049e8ba1dc2d1f12f2e31b54c19
[working/Evergreen.git] / Open-ILS / web / js / dojo / openils / widget / AutoGrid.js
1 if(!dojo._hasResource['openils.widget.AutoGrid']) {
2     dojo.provide('openils.widget.AutoGrid');
3     dojo.require('dojox.grid.DataGrid');
4     dojo.require('dijit.layout.ContentPane');
5     dojo.require('openils.widget.AutoWidget');
6     dojo.require('openils.widget.AutoFieldWidget');
7     dojo.require('openils.widget.EditPane');
8     dojo.require('openils.widget.EditDialog');
9     dojo.require('openils.widget.GridColumnPicker');
10     dojo.require('openils.widget._GridHelperColumns');
11     dojo.require('openils.Util');
12
13     dojo.declare(
14         'openils.widget.AutoGrid',
15         [dojox.grid.DataGrid, openils.widget.AutoWidget, openils.widget._GridHelperColumns],
16         {
17
18             /* if true, pop up an edit dialog when user hits Enter on a give row */
19             editPaneOnSubmit : null,
20             onPostSubmit : null, // called after any CRUD actions are complete
21             createPaneOnSubmit : null,
22             editOnEnter : false, 
23             defaultCellWidth : null,
24             editStyle : 'dialog',
25             editReadOnly : false,
26             suppressFields : null,
27             suppressEditFields : null,
28             suppressFilterFields : null,
29             showColumnPicker : false,
30             columnPersistKey : null,
31             displayLimit : 15,
32             displayOffset : 0,
33             requiredFields : null,
34             hidePaginator : false,
35             showLoadFilter : true,
36             onItemReceived : null,
37             suppressLinkedFields : null, // list of fields whose linked display data should not be fetched from the server
38
39             /* by default, don't show auto-generated (sequence) fields */
40             showSequenceFields : false, 
41
42             startup : function() {
43                 var _this = this;
44                 this.selectionMode = 'single';
45                 this.sequence = openils.widget.AutoGrid.sequence++;
46                 openils.widget.AutoGrid.gridCache[this.sequence] = this;
47                 this.inherited(arguments);
48                 this.initAutoEnv();
49
50                 this.setStructure(this._compileStructure());
51                 this._startupGridHelperColumns();
52                 this.setStructure(this.structure); // required after _startupGridHelper()
53
54                 this.setStore(this.buildAutoStore());
55                 this.cachedQueryOpts = {};
56                 this._showing_create_pane = false;
57                 this.overrideEditWidgets = {};
58                 this.overrideEditWidgetClass = {};
59                 this.overrideWidgetArgs = {};
60
61                 if(this.editOnEnter) 
62                     this._applyEditOnEnter();
63                 else if(this.singleEditStyle) 
64                     this._applySingleEditStyle();
65
66                 if(!this.hidePaginator) {
67                     var self = this;
68                     this.paginator = new dijit.layout.ContentPane();
69
70
71                     var back = dojo.create('a', {
72                         innerHTML : 'Back',  // TODO i18n
73                         style : 'padding-right:6px;',
74                         href : 'javascript:void(0);', 
75                         onclick : function() { 
76                             self.cachedQueryOpts.offset = self.displayOffset -= self.displayLimit;
77                             if(self.displayOffset < 0)
78                                 self.cachedQueryOpts.offset = self.displayOffset = 0;
79                             self.refresh();
80                         }
81                     });
82
83                     var forw = dojo.create('a', {
84                         innerHTML : 'Next',  // TODO i18n
85                         style : 'padding-right:6px;',
86                         href : 'javascript:void(0);', 
87                         onclick : function() { 
88                             self.cachedQueryOpts.offset = self.displayOffset += self.displayLimit;
89                             self.refresh();
90                         }
91                     });
92
93                     dojo.place(this.paginator.domNode, this.domNode, 'before');
94                     dojo.place(back, this.paginator.domNode);
95                     dojo.place(forw, this.paginator.domNode);
96
97                     if(this.showLoadFilter) {
98                         dojo.require('openils.widget.PCrudFilterDialog');
99                         dojo.place(
100                             dojo.create('a', {
101                                 innerHTML : 'Filter', // TODO i18n
102                                 style : 'padding-right:6px;',
103                                 href : 'javascript:void(0);', 
104                                 onclick : function() { 
105                                     if (!self.filterDialog) {
106
107                                         self.filterDialog = new openils.widget.PCrudFilterDialog(
108                                             {fmClass:self.fmClass, suppressFilterFields:self.suppressFilterFields})
109
110                                         self.filterDialog.onApply = function(filter) {
111                                             self.cachedQuerySearch = filter;
112                                             self.resetStore();
113                                             self.loadAll(
114                                                 dojo.mixin( { offset : 0 }, self.cachedQueryOpts ),
115                                                 self.cachedQuerySearch,
116                                                 true
117                                             );
118                                         };
119
120                                         self.filterDialog.startup();
121                                     }
122                                     self.filterDialog.show();
123                                 }
124                             }),
125                             this.paginator.domNode
126                         );
127                     }
128
129                     // progress image
130                     this.loadProgressIndicator = dojo.create('img', {
131                         src:'/opac/images/progressbar_green.gif', // TODO configured path
132                         style:'height:16px;width:16px;'
133                     });
134                     dojo.place(this.loadProgressIndicator, this.paginator.domNode);
135                 }
136             },
137
138             hideLoadProgressIndicator : function() {
139                 dojo.style(this.loadProgressIndicator, 'visibility', 'hidden');
140             },
141
142             showLoadProgressIndicator : function() {
143                 dojo.style(this.loadProgressIndicator, 'visibility', 'visible');
144             },
145
146             _compileStructure : function() {
147                 var existing = (this.structure && this.structure[0].cells[0]) ? 
148                     this.structure[0].cells[0] : [];
149                 var fields = [];
150
151                 var self = this;
152                 function pushEntry(entry) {
153                     if(self.suppressFields) {
154                         if(dojo.indexOf(self.suppressFields, entry.field) != -1)
155                             return;
156                     }
157                     if(!entry.get) 
158                         entry.get = openils.widget.AutoGrid.defaultGetter
159                     if(!entry.width && self.defaultCellWidth)
160                         entry.width = self.defaultCellWidth;
161                     fields.push(entry);
162                 }
163
164                 if(!this.fieldOrder) {
165                     /* no order defined, start with any explicit grid fields */
166                     for(var e in existing) {
167                         var entry = existing[e];
168                         var field = this.fmIDL.fields.filter(
169                             function(i){return (i.name == entry.field)})[0];
170                         if(field) entry.name = entry.name || field.label;
171                         pushEntry(entry);
172                     }
173                 }
174
175                 for(var f in this.sortedFieldList) {
176                     var field = this.sortedFieldList[f];
177                     if(!field || field.virtual) continue;
178                     
179                     // field was already added above
180                     if(fields.filter(function(i){return (i.field == field.name)})[0]) 
181                         continue;
182
183                     var entry = existing.filter(function(i){return (i.field == field.name)})[0];
184                     if(entry) {
185                         entry.name = entry.name || field.label;
186                     } else {
187                         // unless specifically requested, hide sequence fields
188                         if(!this.showSequenceFields && field.name == this.fmIDL.pkey && this.fmIDL.pkey_sequence)
189                             continue; 
190
191                         entry = {field:field.name, name:field.label};
192                     }
193                     pushEntry(entry);
194                 }
195
196                 if(this.fieldOrder) {
197                     /* append any explicit non-IDL grid fields to the end */
198                     for(var e in existing) {
199                         var entry = existing[e];
200                         var field = fields.filter(
201                             function(i){return (i.field == entry.field)})[0];
202                         if(field) continue; // don't duplicate
203                         pushEntry(entry);
204                     }
205                 }
206
207                 return [{cells: [fields]}];
208             },
209
210             /**
211              * @return {Array} List of every fieldmapper object in the data store
212              */
213             getAllObjects : function() {
214                 var objs = [];
215                 var self = this;
216                 this.store.fetch({
217                     onComplete : function(list) {
218                         dojo.forEach(list, 
219                             function(item) {
220                                 objs.push(new fieldmapper[self.fmClass]().fromStoreItem(item));
221                             }
222                         )
223                     }
224                 });
225                 return objs;
226             },
227
228             /**
229              * Deletes the underlying object for all selected rows
230              */
231             deleteSelected : function() {
232                 var items = this.getSelectedItems();
233                 var total = items.length;
234                 var self = this;
235                 dojo.require('openils.PermaCrud');
236                 dojo.forEach(items,
237                     function(item) {
238                         var fmObject = new fieldmapper[self.fmClass]().fromStoreItem(item);
239                         new openils.PermaCrud()['eliminate'](
240                             fmObject, {
241                                 oncomplete : function(r) {
242                                     self.store.deleteItem(item);
243                                     if (--total < 1 && self.onPostSubmit) {
244                                         self.onPostSubmit();
245                                     }
246                                 }
247                             }
248                         );
249                     }
250                 );
251             },
252
253             _applySingleEditStyle : function() {
254                 this.onMouseOverRow = function(e) {};
255                 this.onMouseOutRow = function(e) {};
256                 this.onCellFocus = function(cell, rowIndex) { 
257                     this.selection.deselectAll();
258                     this.selection.select(this.focus.rowIndex);
259                 };
260             },
261
262             /* capture keydown and launch edit dialog on enter */
263             _applyEditOnEnter : function() {
264                 this._applySingleEditStyle();
265
266                 dojo.connect(this, 'onRowDblClick',
267                     function(e) {
268                         if(this.editStyle == 'pane')
269                             this._drawEditPane(this.selection.getFirstSelected(), this.focus.rowIndex);
270                         else
271                             this._drawEditDialog(this.selection.getFirstSelected(), this.focus.rowIndex);
272                     }
273                 );
274
275                 dojo.connect(this, 'onKeyDown',
276                     function(e) {
277                         if(e.keyCode == dojo.keys.ENTER) {
278                             this.selection.deselectAll();
279                             this.selection.select(this.focus.rowIndex);
280                             if(this.editStyle == 'pane')
281                                 this._drawEditPane(this.selection.getFirstSelected(), this.focus.rowIndex);
282                             else
283                                 this._drawEditDialog(this.selection.getFirstSelected(), this.focus.rowIndex);
284                         }
285                     }
286                 );
287             },
288
289             _makeEditPane : function(storeItem, rowIndex, onPostSubmit, onCancel) {
290                 var grid = this;
291                 var fmObject = new fieldmapper[this.fmClass]().fromStoreItem(storeItem);
292                 var idents = grid.store.getIdentityAttributes();
293
294                 var pane = new openils.widget.EditPane({
295                     fmObject:fmObject,
296                     hideSaveButton : this.editReadOnly,
297                     readOnly : this.editReadOnly,
298                     overrideWidgets : this.overrideEditWidgets,
299                     overrideWidgetClass : this.overrideEditWidgetClass,
300                     overrideWidgetArgs : this.overrideWidgetArgs,
301                     disableWidgetTest : this.disableWidgetTest,
302                     requiredFields : this.requiredFields,
303                     suppressFields : this.suppressEditFields,
304                     onPostSubmit : function() {
305                         for(var i in fmObject._fields) {
306                             var field = fmObject._fields[i];
307                             if(idents.filter(function(j){return (j == field)})[0])
308                                 continue; // don't try to edit an identifier field
309                             grid.store.setValue(storeItem, field, fmObject[field]());
310                         }
311                         if(grid.onPostUpdate)
312                             grid.onPostUpdate(storeItem, rowIndex);
313                         setTimeout(
314                             function(){
315                                 try { 
316                                     grid.views.views[0].getCellNode(rowIndex, 0).focus(); 
317                                 } catch (E) {}
318                             },200
319                         );
320                         if(onPostSubmit) 
321                             onPostSubmit();
322                         if (grid.onPostSubmit)
323                             grid.onPostSubmit();
324
325                     },
326                     onCancel : function() {
327                         setTimeout(function(){
328                             grid.views.views[0].getCellNode(rowIndex, 0).focus();},200);
329                         if(onCancel) onCancel();
330                     }
331                 });
332
333                 if (typeof this.editPaneOnSubmit == "function")
334                     pane.onSubmit = this.editPaneOnSubmit;
335                 pane.fieldOrder = this.fieldOrder;
336                 pane.mode = 'update';
337                 return pane;
338             },
339
340             _makeCreatePane : function(onPostSubmit, onCancel) {
341                 var grid = this;
342                 var pane = new openils.widget.EditPane({
343                     fmClass : this.fmClass,
344                     overrideWidgets : this.overrideEditWidgets,
345                     overrideWidgetClass : this.overrideEditWidgetClass,
346                     overrideWidgetArgs : this.overrideWidgetArgs,
347                     disableWidgetTest : this.disableWidgetTest,
348                     requiredFields : this.requiredFields,
349                     suppressFields : this.suppressEditFields,
350                     onPostSubmit : function(req, cudResults) {
351                         var fmObject = cudResults[0];
352                         if(grid.onPostCreate)
353                             grid.onPostCreate(fmObject);
354                         if(fmObject) 
355                             grid.store.newItem(fmObject.toStoreItem());
356                         setTimeout(function(){
357                             try {
358                                 grid.selection.select(grid.rowCount-1);
359                                 grid.views.views[0].getCellNode(grid.rowCount-1, 1).focus();
360                             } catch (E) {}
361                         },200);
362                         if(onPostSubmit)
363                             onPostSubmit(fmObject);
364                         if (grid.onPostSubmit)
365                             grid.onPostSubmit();
366                     },
367                     onCancel : function() {
368                         if(onCancel) onCancel();
369                     }
370                 });
371                 if (typeof this.createPaneOnSubmit == "function")
372                     pane.onSubmit = this.createPaneOnSubmit;
373                 pane.fieldOrder = this.fieldOrder;
374                 pane.mode = 'create';
375                 return pane;
376             },
377
378             /**
379              * Creates an EditPane with a copy of the data from the provided store
380              * item for cloning said item
381              * @param {Object} storeItem Dojo data item
382              * @param {Number} rowIndex The Grid row index of the item to be cloned
383              * @param {Function} onPostSubmit Optional callback for post-submit behavior
384              * @param {Function} onCancel Optional callback for clone cancelation
385              * @return {Object} The clone EditPane
386              */
387             _makeClonePane : function(storeItem, rowIndex, onPostSubmit, onCancel) {
388                 var clonePane = this._makeCreatePane(onPostSubmit, onCancel);
389                 var origPane = this._makeEditPane(storeItem, rowIndex);
390                 clonePane.startup();
391                 origPane.startup();
392                 dojo.forEach(origPane.fieldList,
393                     function(field) {
394                         if(field.widget.widget.attr('disabled')) return;
395                         var w = clonePane.fieldList.filter(
396                             function(i) { return (i.name == field.name) })[0];
397                         w.widget.baseWidgetValue(field.widget.widget.attr('value')); // sync widgets
398                         w.widget.onload = function(){w.widget.baseWidgetValue(field.widget.widget.attr('value'))}; // async widgets
399                     }
400                 );
401                 origPane.destroy();
402                 return clonePane;
403             },
404
405
406             _drawEditDialog : function(storeItem, rowIndex) {
407                 var self = this;
408                 var done = function() { self.hideDialog(); };
409                 var pane = this._makeEditPane(storeItem, rowIndex, done, done);
410                 this.editDialog = new openils.widget.EditDialog({editPane:pane});
411                 this.editDialog.startup();
412                 this.editDialog.show();
413                 if(this.onEditPane) this.onEditPane(pane);
414             },
415
416             /**
417              * Generates an EditDialog for object creation and displays it to the user
418              */
419             showCreateDialog : function() {
420                 var self = this;
421                 var done = function() { self.hideDialog(); };
422                 var pane = this._makeCreatePane(done, done);
423                 this.editDialog = new openils.widget.EditDialog({editPane:pane});
424                 this.editDialog.startup();
425                 this.editDialog.show();
426             },
427
428             _drawEditPane : function(storeItem, rowIndex) {
429                 var self = this;
430                 var done = function() { self.hidePane(); };
431                 dojo.style(this.domNode, 'display', 'none');
432                 this.editPane = this._makeEditPane(storeItem, rowIndex, done, done);
433                 this.editPane.startup();
434                 this.domNode.parentNode.insertBefore(this.editPane.domNode, this.domNode);
435                 if(this.onEditPane) this.onEditPane(this.editPane);
436             },
437
438             showClonePane : function(onPostSubmit) {
439                 var self = this;
440                 var done = function() { self.hidePane(); };
441
442                                     
443                 var row = this.getFirstSelectedRow();
444                 if(!row) return;
445
446                 var postSubmit = (onPostSubmit) ? 
447                     function(result) { onPostSubmit(self.getItem(row), result); self.hidePane(); } :
448                     done;
449
450                 dojo.style(this.domNode, 'display', 'none');
451                 this.editPane = this._makeClonePane(this.getItem(row), row, postSubmit, done);
452                 this.domNode.parentNode.insertBefore(this.editPane.domNode, this.domNode);
453                 if(this.onEditPane) this.onEditPane(this.editPane);
454             },
455
456             showCreatePane : function() {
457                 if (this._showing_create_pane)
458                     return;
459                 this._showing_create_pane = true;
460
461                 var self = this;
462                 var done = function() {
463                     self._showing_create_pane = false;
464                     self.hidePane();
465                 };
466                 dojo.style(this.domNode, 'display', 'none');
467                 this.editPane = this._makeCreatePane(done, done);
468                 this.editPane.startup();
469                 this.domNode.parentNode.insertBefore(this.editPane.domNode, this.domNode);
470                 if(this.onEditPane) this.onEditPane(this.editPane);
471             },
472
473             hideDialog : function() {
474                 this.editDialog.hide(); 
475                 this.editDialog.destroy(); 
476                 delete this.editDialog;
477                 this.update();
478             },
479
480             hidePane : function() {
481                 this.domNode.parentNode.removeChild(this.editPane.domNode);
482                 this.editPane.destroy();
483                 delete this.editPane;
484                 dojo.style(this.domNode, 'display', 'block');
485                 this.update();
486             },
487             
488             resetStore : function() {
489                 this.setStore(this.buildAutoStore());
490             },
491
492             refresh : function(opts, search) {
493                 opts = opts || this.cachedQueryOpts;
494                 search = search || this.cachedQuerySearch;
495                 this.resetStore();
496                 if (this.dataLoader)
497                     this.dataLoader()
498                 else
499                     this._loadAll(opts, search);
500             },
501
502             // called after a sort change occurs within the column picker
503             cpSortHandler : function(fields) {
504                 console.log("AutoGrod cpSortHandler(): " + js2JSON(fields));
505                 // user-defined sort handler
506                 if (this.onSortChange) { 
507                     this.onSortChange(fields)
508
509                 // default sort handler
510                 } else { 
511                     if (!this.cachedQueryOpts) 
512                         this.cachedQueryOpts = {};
513                     var order_by = '';
514                     dojo.forEach(fields, function(f) {
515                         if (order_by) order_by += ',';
516                         order_by += f.field + ' ' + f.direction
517                     });
518                     this.cachedQueryOpts.order_by = {};
519                     this.cachedQueryOpts.order_by[this.fmClass] = order_by;
520                     this.refresh();
521                 }
522             },
523
524             loadAll : function(opts, search, filter_triggered) {
525                 var _this = this;
526
527                 // first we have to load the column picker to determine the sort fields.
528                
529                 if(this.showColumnPicker && !this.columnPicker) {
530                     if(!this.columnPersistKey) {
531                         console.error("No columnPersistKey defined");
532                         this.columnPicker = {};
533                     } else {
534                         this.columnPicker = new openils.widget.GridColumnPicker(
535                             openils.User.authtoken, this.columnPersistKey, this);
536                         this.columnPicker.onSortChange = function(fields) {_this.cpSortHandler(fields)};
537                         this.columnPicker.onLoad = function(cpOpts) {
538                             _this.cachedQueryOpts = opts;
539                             _this.cachedQuerySearch = search;
540                             _this.cpSortHandler(cpOpts.sortFields); // calls refresh() -> _loadAll()
541                         };
542                         this.columnPicker.load();
543                         return;
544                     }
545                 }
546
547                 // column picker not wanted or already loaded
548                 this._loadAll(opts, search, filter_triggered);
549             },
550
551             _loadAll : function(opts, search, filter_triggered) {
552                 var self = this;
553
554                 dojo.require('openils.PermaCrud');
555                 if(this.loadProgressIndicator)
556                     dojo.style(this.loadProgressIndicator, 'visibility', 'visible');
557                 opts = dojo.mixin(
558                     {limit : this.displayLimit, offset : this.displayOffset}, 
559                     opts || {}
560                 );
561                 opts = dojo.mixin(opts, {
562                     async : true,
563                     streaming : true,
564                     onresponse : function(r) {
565                         var item = openils.Util.readResponse(r);
566                         if (self.onItemReceived) 
567                             self.onItemReceived(item);
568                         self.store.newItem(item.toStoreItem());
569                     },
570                     oncomplete : function() {
571                         if(self.loadProgressIndicator) 
572                             dojo.style(self.loadProgressIndicator, 'visibility', 'hidden');
573                     }
574                 });
575
576                 this.cachedQuerySearch = search;
577                 this.cachedQueryOpts = opts;
578
579                 // retain the most recent external loadAll 
580                 if (!filter_triggered || !this.preFilterSearch)
581                     this.preFilterSearch = dojo.clone( this.cachedQuerySearch );
582
583                 if(search)
584                     new openils.PermaCrud().search(this.fmClass, search, opts);
585                 else
586                     new openils.PermaCrud().retrieveAll(this.fmClass, opts);
587             }
588         } 
589     );
590
591     // static ID generater seed
592     openils.widget.AutoGrid.sequence = 0;
593     openils.widget.AutoGrid.gridCache = {};
594
595     openils.widget.AutoGrid.markupFactory = dojox.grid.DataGrid.markupFactory;
596
597     openils.widget.AutoGrid.defaultGetter = function(rowIndex, item) {
598         if(!item) return '';
599         if(!this.grid.overrideWidgetArgs[this.field])
600             this.grid.overrideWidgetArgs[this.field] = {};
601         var val = this.grid.store.getValue(item, this.field);
602         var autoWidget = new openils.widget.AutoFieldWidget(dojo.mixin({
603             fmClass: this.grid.fmClass,
604             fmField: this.field,
605             widgetValue : val,
606             readOnly : true,
607             forceSync : true, // prevents many simultaneous requests for the same data
608             suppressLinkedFields : this.grid.suppressLinkedFields
609         },this.grid.overrideWidgetArgs[this.field]));
610
611         autoWidget.build();
612
613         /*
614         // With proper caching, this should not be necessary to prevent grid render flickering
615         var _this = this;
616         autoWidget.build(
617             function(w, ww) {
618                 try {
619                     var node = _this.grid.getCell(_this.index).view.getCellNode(rowIndex, _this.index);
620                     if(node) 
621                         node.innerHTML = ww.getDisplayString();
622                 } catch(E) {}
623             }
624         );
625         */
626
627         return autoWidget.getDisplayString();
628     };
629
630     openils.widget.AutoGrid.orgUnitGetter = function(rowIndex, item) {
631         if (!item) return "";
632
633         var aou_id = this.grid.store.getValue(item, this.field);
634         if (aou_id)
635             return fieldmapper.aou.findOrgUnit(aou_id).shortname();
636         else
637             return "";
638     };
639 }
640