Merge branch 'master' of git.evergreen-ils.org:Evergreen
[working/Evergreen.git] / Open-ILS / web / js / dojo / openils / widget / GridColumnPicker.js
1 /* ---------------------------------------------------------------------------
2  * Copyright (C) 2008  Georgia Public Library Service
3  * Bill Erickson <erickson@esilibrary.com>
4  *
5  * This program is free software; you can redistribute it and/or
6  * modify it under the terms of the GNU General Public License
7  * as published by the Free Software Foundation; either version 2
8  * of the License, or (at your option) any later version.
9  *
10  * This program is distributed in the hope that it will be useful,
11  * but WITHOUT ANY WARRANTY; without even the implied warranty of
12  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13  * GNU General Public License for more details.
14  * ---------------------------------------------------------------------------
15  */
16
17
18 if(!dojo._hasResource["openils.widget.GridColumnPicker"]) {
19     dojo.provide('openils.widget.GridColumnPicker');
20
21     dojo.require('dijit.Dialog');
22     dojo.require('dijit.form.Button');
23     dojo.require('dijit.form.NumberSpinner');
24     dojo.require('openils.User');
25     dojo.require('openils.Event');
26     dojo.require('openils.Util');
27     dojo.require('fieldmapper.Fieldmapper');
28
29
30     dojo.declare('openils.widget.GridColumnPicker', null, {
31
32         USER_PERSIST_SETTING : 'ui.grid_columns',
33
34         constructor : function (authtoken, persistKey, grid, structure) {
35             var _this = this;
36             this.grid = grid;
37             this.persistKey = this.USER_PERSIST_SETTING+'.'+persistKey;
38             this.authtoken = authtoken || openils.User.authtoken;
39             this.structure = structure || this.grid.structure;
40             this.cells = this.structure[0].cells[0].slice();
41
42             this.dialog = this.buildDialog();
43             this.dialogTable = this.dialog.containerNode.getElementsByTagName('tbody')[0];
44
45             // replace: called after any sort changes
46             this.onSortChange = function(list) {console.log('onSortChange()')}
47             // replace:  called after user settings are first retrieved
48             this.onLoad = function(opts) {console.log('onLoad()')};
49
50             // internal onload handler
51             this.loaded = false;
52             this._onLoad = function(opts) {_this.loaded = true; _this.onLoad(opts)};
53
54             this.grid.onHeaderContextMenu = function(e) { 
55                 _this.build();
56                 _this.dialog.show(); 
57                 dojo.stopEvent(e);
58             };
59         },
60
61         /** Loads the current grid structure and passes the 
62          *  structure back to the grid to force a UI refresh.
63          *  This is necessary if external forces alter the structure. 
64          */
65         reloadStructure : function() {
66             this.structure = this.grid.structure;
67             this.cells = this.structure[0].cells[0].slice();
68             this.grid.setStructure(this.structure);
69         },
70
71         // determine the visible sorting from the 
72         // view and update our list of cells to match
73         refreshCells : function() {
74             var cells = this.cells;
75             this.cells = [];
76             var _this = this;
77
78             dojo.forEach(
79                  _this.grid.views.views[0].structure.cells[0],
80                  function(vCell) {
81                     for (var i = 0; i < cells.length; i++) {
82                         if (cells[i].field == vCell.field) {
83                             cells[i]._visible = true;
84                             _this.cells.push(cells[i]);
85                             break;
86                         }
87                     }
88                 }
89             );
90
91             // Depending on how the grid structure is built, there may be
92             // cells in the structure that are not yet in the view.  Push
93             // any remaining cells onto the end.
94             dojo.forEach(
95                 cells,
96                 function(cell) {
97                     existing = _this.cells.filter(function(s){return s.field == cell.field})[0]
98                     if (!existing) {
99                         cell._visible = false;
100                         _this.cells.push(cell);
101                     }
102                 }
103             );
104         },
105
106         buildDialog : function() {
107             var self = this;
108             
109             // TODO i18n
110
111             var dialog = new dijit.Dialog({title : 'Column Picker'});
112             var table = dojo.create('table', {'class':'oils-generic-table', innerHTML : 
113                 "<table><thead><tr><th width='30%'>Column</th><th width='23%'>Display</th>" +
114                 "<th width='23%'>Auto Width</th><th width='23%'>Sort Priority</th></tr></thead>" +
115                 "<tbody />"});
116
117             var tDiv = dojo.create('div', {style : 'height:400px; overflow-y:auto;'});
118             tDiv.appendChild(table);
119
120             var bDiv = dojo.create('div', {style : 'text-align:right; width:100%;',
121                 innerHTML : "<span name='cancel_button'></span><span name='save_button'></span>"});
122
123             var textDiv = dojo.create('div', {style : 'padding:5px; margin-top:5px; border-top:1px solid #333', 
124                 innerHTML :
125                     "<i>A Sort Priority of '0' means no sorting is applied.<br/>" +
126                     "<i>Apply a negative Sort Priority for descending sort."});
127             
128             var wrapper = dojo.create('div');
129             wrapper.appendChild(tDiv);
130             wrapper.appendChild(textDiv);
131             wrapper.appendChild(bDiv);
132             dialog.containerNode.appendChild(wrapper);
133
134             var button = new dijit.form.Button({label:'Save'}, 
135                 dojo.query('[name=save_button]', bDiv)[0]);
136             button.onClick = function() { dialog.hide(); self.update(true); };
137
138             button = new dijit.form.Button({label:'Cancel'}, 
139                 dojo.query('[name=cancel_button]', bDiv)[0]);
140             button.onClick = function() { dialog.hide(); };
141
142             return dialog;
143         },
144
145         // builds the column-picker dialog table
146         build : function() {
147             this.refreshCells();
148             var rows = dojo.query('tr', this.dialogTable);
149
150             for(var i = 0; i < rows.length; i++) {
151                 if(rows[i].getAttribute('picker'))
152                     this.dialogTable.removeChild(rows[i]);
153             }
154
155             rows = dojo.query('tr', this.dialogTable);
156             var lastChild = null;
157             if(rows.length > 0)
158                 lastChild = rows[rows.length-1];
159
160             for(var i = 0; i < this.cells.length; i++) {
161                 // setting table.innerHTML breaks stuff, so do it the hard way
162                 var cell = this.cells[i];
163                 tr = document.createElement('tr');
164                 tr.setAttribute('picker', 'picker');
165                 td0 = document.createElement('td');
166                 td1 = document.createElement('td');
167                 td2 = document.createElement('td');
168                 td3 = document.createElement('td');
169
170                 ipt = document.createElement('input');
171                 ipt.setAttribute('type', 'checkbox');
172                 ipt.setAttribute('name', 'selector');
173
174                 ipt2 = document.createElement('input');
175                 ipt2.setAttribute('type', 'checkbox');
176                 ipt2.setAttribute('name', 'width');
177
178                 ipt3 = document.createElement('div');
179
180                 if (cell.nonSelectable) {
181                     ipt.setAttribute('checked', 'checked');
182                     ipt.setAttribute('disabled', true);
183                     ipt2.setAttribute('disabled', true);
184
185                 } else {
186                     if (cell._visible) {
187                         ipt.setAttribute('checked', 'checked');
188                         if (cell.width == 'auto') 
189                             ipt2.setAttribute('checked', 'checked');
190                     } else {
191                         ipt.removeAttribute('checked');
192                     }
193                 }
194
195                 if (cell.field == '+selector') {
196                     // pick up the unescaped unicode checkmark char
197                     td0.innerHTML = cell.name;
198                 } else {
199                     td0.appendChild(document.createTextNode(cell.name));
200                 }
201                 td1.appendChild(ipt);
202                 td2.appendChild(ipt2);
203                 td3.appendChild(ipt3);
204                 tr.appendChild(td0);
205                 tr.appendChild(td1);
206                 tr.appendChild(td2);
207                 tr.appendChild(td3);
208
209                 if(lastChild)
210                     this.dialogTable.insertBefore(tr, lastChild);
211                 else
212                     this.dialogTable.appendChild(tr);
213
214                 if ( this.grid.canSort(i+1) ) { // column index is 1-based
215
216                     // must be added after its parent node is inserted into the DOM.
217                     var ns = new dijit.form.NumberSpinner(
218                         {   constraints : {places : 0}, 
219                             value : cell._sort || 0,
220                             style : 'width:4em',
221                             name : 'sort',
222                         }, ipt3
223                     );
224                 }
225             }
226         },
227
228         // update the grid based on the items selected in the picker dialog
229         update : function(persist) {
230             var rows = dojo.query('[picker=picker]', this.dialogTable);
231             var _this = this;
232             var displayCells = [];
233             var sortUpdated = false;
234
235             for (var i = 0; i < rows.length; i++) {
236                 var row = rows[i];
237                 var selector = dojo.query('[name=selector]', row)[0];
238                 var width = dojo.query('[name=width]', row)[0];
239                 var sort = dojo.query('[name=sort]', row)[0];
240                 var cell = this.cells[i]; // index should match dialog
241
242                 if (sort && cell._sort != sort.value) {
243                     sortUpdated = true;
244                     cell._sort = sort.value;
245                 }
246
247                 if (selector.checked) {
248                     cell._visible = true;
249                     if (width.checked) {
250                         cell.width = 'auto';
251                     } else if(cell.width == 'auto') {
252                         delete cell.width;
253                     }
254                     displayCells.push(cell);
255
256                 } else {
257                     cell._visible = false;
258                     delete cell.width;
259                 }
260             }
261
262             if (sortUpdated && this.onSortChange) 
263                 this.onSortChange(this.buildSortList());
264
265             this.structure[0].cells[0] = displayCells;
266             this.grid.setStructure(this.structure);
267             this.grid.update();
268
269             if (persist) this.persist(true);
270         },
271
272         // extract cells that have sorting applied, order lowest to highest
273         buildSortList : function() {
274             var sortList = this.cells.filter(
275                 function(cella) { return Number(cella._sort) }
276             ).sort( 
277                 function(a, b) { 
278                     if (Math.abs(a._sort) < Math.abs(b._sort)) return -1; 
279                     return 1; 
280                 }
281             );
282
283             return sortList.map(function(f){
284                 var dir = f._sort < 0 ? 'desc' : 'asc';
285                 return {field : f.field, direction : dir};
286             });
287         },
288
289         // save me as a user setting
290         persist : function(noRefresh) {
291             var list = [];
292             var autos = [];
293             if (!noRefresh) this.refreshCells();
294
295             for(var i = 0; i < this.cells.length; i++) {
296                 var cell = this.cells[i];
297                 if (cell._visible) {
298                     list.push(cell.field);
299                     if(cell.width == 'auto')
300                         autos.push(cell.field);
301                 } 
302             }
303
304             var setting = {};
305             setting[this.persistKey] = {
306                 'columns' : list, 
307                 'auto' : autos,
308                 'sort' : this.buildSortList().map(function(f){return f.field})
309             };
310
311             var _this = this;
312             fieldmapper.standardRequest(
313                 ['open-ils.actor', 'open-ils.actor.patron.settings.update'],
314                 {   async: true,
315                     params: [this.authtoken, null, setting],
316                     oncomplete: function(r) {
317                         var stat = openils.Util.readResponse(r);
318                     },
319                     onmethoderror : function() {},
320                     onerror : function() { 
321                         console.log("No user setting '" + _this.persistKey + "' configured.  Cannot persist") 
322                     }
323                 }
324             );
325         }, 
326
327         loadColsFromSetting : function(setting) {
328             var _this = this;
329             this.setting = setting;
330             var displayCells = [];
331             
332             // new component, existing settings may not have this
333             if (!setting.sort) setting.sort = [];
334
335             dojo.forEach(setting.columns,
336                 function(col) {
337                     var cell = _this.cells.filter(function(c){return c.field == col})[0];
338                     if (cell) {
339                         cell._visible = true;
340                         displayCells.push(cell);
341
342                         if(setting.auto.indexOf(cell.field) > -1) {
343                             cell.width = 'auto';
344                         } else {
345                             if(cell.width == 'auto')
346                                 delete cell.width;
347                         }
348                         cell._sort = setting.sort.indexOf(cell.field) + 1;
349
350                     } else {
351                         console.log('Unknown setting column '+col+'.  Ignoring...');
352                     }
353                 }
354             );
355             
356             // any cells not in the setting must be marked as non-visible
357             dojo.forEach(this.cells, function(cell) { 
358                 if (setting.columns.indexOf(cell.field) == -1) {
359                     cell._visible = false;
360                     cell._sort = 0;
361                 }
362             });
363
364             this.structure[0].cells[0] = displayCells;
365             this.grid.setStructure(this.structure);
366             this.grid.update();
367         },
368
369         load : function() {
370             var _this = this;
371
372             // if load is called before the user has logged in,
373             // queue the loading up for after authentication.
374             if (!this.authtoken) {
375                 var _this = this;
376                 openils.Util.addOnLoad(function() {
377                     _this.authtoken = openils.User.authtoken;
378                     _this.load();
379                 }); 
380                 return;
381             }
382
383             if(this.setting) {
384                 this.loadColsFromSetting(this.setting);
385                 this._onLoad({sortFields : this.buildSortList()});
386                 return;
387             }
388
389             fieldmapper.standardRequest(
390                 ['open-ils.actor', 'open-ils.actor.patron.settings.retrieve'],
391                 {   async: true,
392                     params: [this.authtoken, null, this.persistKey],
393                     oncomplete: function(r) {
394                         var set = openils.Util.readResponse(r);
395                         if(set) {
396                             _this.loadColsFromSetting(set);
397                         } else {
398                             _this.grid.setStructure(_this.structure);
399                             _this.grid.update();
400                         }
401                         _this._onLoad({sortFields : _this.buildSortList()});
402                     }
403                 }
404             );
405         },
406     });
407 }
408