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