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