8166016a02325ab685cc28972a0715150e5ff2b4
[working/Evergreen.git] / Open-ILS / xul / staff_client / chrome / content / util / list.js
1 dump('entering util.list.js\n');
2
3 if (typeof main == 'undefined') main = {};
4 util.list = function (id) {
5
6     this.node = document.getElementById(id);
7
8     this.row_count = { 'total' : 0, 'fleshed' : 0 };
9
10     this.unique_row_counter = 0;
11
12     this.sub_sorts = [];
13
14     if (!this.node) throw('Could not find element ' + id);
15     switch(this.node.nodeName) {
16         case 'listbox' : 
17         case 'tree' : break;
18         case 'richlistbox' :
19             throw(this.node.nodeName + ' not yet supported'); break;
20         default: throw(this.node.nodeName + ' not supported'); break;
21     }
22
23     JSAN.use('util.error'); this.error = new util.error();
24
25     JSAN.use('OpenILS.data'); this.data = new OpenILS.data(); this.data.stash_retrieve();
26
27     JSAN.use('util.functional');
28     JSAN.use('util.widgets');
29
30     return this;
31 };
32
33 util.list.prototype = {
34
35     'init' : function (params) {
36
37         var obj = this;
38         obj.scratch_data = {};
39         obj.event_listeners = new EventListenerList();
40
41         // If set, save and restore columns as if the tree/list id was the value of columns_saved_under
42         obj.columns_saved_under = params.columns_saved_under;
43
44         JSAN.use('util.widgets');
45
46         obj.printer_context = params.printer_context;
47
48         if (typeof params.map_row_to_column == 'function') obj.map_row_to_column = params.map_row_to_column;
49         if (typeof params.map_row_to_columns == 'function') {
50             obj.map_row_to_columns = params.map_row_to_columns;
51         } else {
52             obj.map_row_to_columns = obj.std_map_row_to_columns();
53         }
54         if (typeof params.retrieve_row == 'function') obj.retrieve_row = params.retrieve_row;
55
56         obj.prebuilt = false;
57         if (typeof params.prebuilt != 'undefined') obj.prebuilt = params.prebuilt;
58
59         if (typeof params.columns == 'undefined') throw('util.list.init: No columns');
60         obj.columns = [
61             {
62                 'id' : 'lineno',
63                 'label' : document.getElementById('offlineStrings').getString('list.line_number'),
64                 'flex' : '0',
65                 'no_sort' : 'true',
66                 'properties' : 'ordinal', // column properties for css styling
67                 'hidden' : 'false',
68                 'editable' : false,
69                 'render' : function(my,scratch) {
70                     // special code will handle this based on the attribute we set
71                     // here.  All cells for this column need to be updated whenever
72                     // a list adds, removes, or sorts rows
73                     return '_';
74                 }
75             }
76         ];
77         for (var i = 0; i < params.columns.length; i++) {
78             if (typeof params.columns[i] == 'object') {
79                 obj.columns.push( params.columns[i] );
80             } else {
81                 var cols = obj.fm_columns( params.columns[i] );
82                 for (var j = 0; j < cols.length; j++) {
83                     obj.columns.push( cols[j] );
84                 }
85             }
86         }
87
88         switch(obj.node.nodeName) {
89             case 'tree' : obj._init_tree(params); break;
90             case 'listbox' : obj._init_listbox(params); break;
91             default: throw('NYI: Need ._init() for ' + obj.node.nodeName); break;
92         }
93     },
94
95     '_init_tree' : function (params) {
96         var obj = this;
97         if (this.prebuilt) {
98         
99             this.treechildren = this.node.lastChild;    
100         
101         } else {
102             var treecols = document.createElement('treecols');
103             this.node.appendChild(treecols);
104             this.treecols = treecols;
105             if (document.getElementById('column_sort_menu')) {
106                 treecols.setAttribute('context','column_sort_menu');
107             }
108
109             var check_for_id_collisions = {};
110             for (var i = 0; i < this.columns.length; i++) {
111                 var treecol = document.createElement('treecol');
112                 for (var j in this.columns[i]) {
113                     var value = this.columns[i][j];
114                     if (j=='id') {
115                         if (typeof check_for_id_collisions[value] == 'undefined') {
116                             check_for_id_collisions[value] = true;
117                         } else {
118                             // Column id's are important for sorting and saving list configuration.  Collisions started happening because
119                             // we were using field names as id's, and then later combining column definitions for multiple objects that
120                             // shared field names.  The downside to this sort of automatic collision prevention is that these generated
121                             // id's can change as we add and remove columns, possibly breaking saved list configurations.
122                             dump('Column collision with id = ' + value + ', renaming to ');
123                             value = value + '_collision_' + i;
124                             dump(value + '\n');
125                         }
126                     }
127                     treecol.setAttribute(j,value);
128                 }
129                 treecols.appendChild(treecol);
130
131                 if (this.columns[i].type == 'checkbox') {
132                     obj.event_listeners.add(
133                         treecol,
134                         'click',
135                         function(ev) {
136                             setTimeout(
137                                 function() {
138                                     var toggle = ev.target.getAttribute('toggleAll') || 'on';
139                                     if (toggle == 'off') toggle = 'on'; else toggle = 'off';
140                                     ev.target.setAttribute('toggleAll',toggle);
141                                     obj._toggle_checkbox_column(ev.target,toggle);
142                                 }, 0
143                             );
144                         },
145                         false
146                     );
147                 } else {
148                     obj.event_listeners.add(
149                         treecol,
150                         'sort_first_asc',
151                         function(ev) {
152                             dump('sort_first_asc\n');
153                             ev.target.setAttribute('sortDir','asc');
154                             obj.first_sort = {
155                                 'target' : ev.target,
156                                 'sortDir' : 'asc'
157                             };
158                             obj.sub_sorts = [];
159                             util.widgets.dispatch('sort',ev.target);
160                         },
161                         false
162                     );
163                     obj.event_listeners.add(
164                         treecol,
165                         'sort_first_desc',
166                         function(ev) {
167                             dump('sort_first_desc\n');
168                             ev.target.setAttribute('sortDir','desc');
169                             obj.first_sort = {
170                                 'target' : ev.target,
171                                 'sortDir' : 'desc'
172                             };
173                             obj.sub_sorts = [];
174                             util.widgets.dispatch('sort',ev.target);
175                         },
176                         false
177                     );
178                     obj.event_listeners.add(
179                         treecol,
180                         'sort_next_asc',
181                         function(ev) {
182                             dump('sort_next_asc\n');
183                             ev.target.setAttribute('sortDir','asc');
184                             obj.sub_sorts.push({
185                                 'target' : ev.target,
186                                 'sortDir' : 'asc'
187                             });
188                             util.widgets.dispatch('sort',ev.target);
189                         },
190                         false
191                     );
192                     obj.event_listeners.add(
193                         treecol,
194                         'sort_next_desc',
195                         function(ev) {
196                             dump('sort_next_desc\n');
197                             ev.target.setAttribute('sortDir','desc');
198                             obj.sub_sorts.push({
199                                 'target' : ev.target,
200                                 'sortDir' : 'desc'
201                             });
202                             util.widgets.dispatch('sort',ev.target);
203                         },
204                         false
205                     );
206
207                     obj.event_listeners.add(
208                         treecol,
209                         'click', 
210                         function(ev) {
211                             if (ev.button == 2 /* context menu click */ || ev.target.getAttribute('no_sort')) {
212                                 return;
213                             }
214
215                             if (ev.ctrlKey) { // sub sort
216                                 var sortDir = 'asc';
217                                 if (ev.shiftKey) {
218                                     sortDir = 'desc';
219                                 }
220                                 ev.target.setAttribute('sortDir',sortDir);
221                                 obj.sub_sorts.push({
222                                     'target' : ev.target,
223                                     'sortDir' : sortDir
224                                 });
225                             } else { // first sort
226                                 var sortDir = ev.target.getAttribute('sortDir') || 'desc';
227                                 if (sortDir == 'desc') sortDir = 'asc'; else sortDir = 'desc';
228                                 if (ev.shiftKey) {
229                                     sortDir = 'desc';
230                                 }
231                                 ev.target.setAttribute('sortDir',sortDir);
232                                 obj.first_sort = {
233                                     'target' : ev.target,
234                                     'sortDir' : sortDir
235                                 };
236                                 obj.sub_sorts = [];
237                             }
238                             util.widgets.dispatch('sort',ev.target);
239                         },
240                         false
241                     );
242
243                     obj.event_listeners.add(
244                         treecol,
245                         'sort',
246                         function(ev) {
247                             if (!obj.first_sort) {
248                                 return;
249                             }
250
251                             function do_it() {
252                                 obj._sort_tree();
253                             }
254
255                             if (obj.row_count.total != obj.row_count.fleshed
256                                 && (obj.row_count.total - obj.row_count.fleshed) > 50
257                             ) {
258                                 var r = window.confirm(
259                                     document.getElementById('offlineStrings').getFormattedString(
260                                         'list.row_fetch_warning',
261                                         [obj.row_count.fleshed,obj.row_count.total]
262                                     )
263                                 );
264
265                                 if (r) {
266                                     setTimeout( do_it, 0 );
267                                 }
268
269                             } else {
270                                     setTimeout( do_it, 0 );
271                             }
272
273                         },
274                         false
275                     );
276                 }
277                 var splitter = document.createElement('splitter');
278                 splitter.setAttribute('class','tree-splitter');
279                 treecols.appendChild(splitter);
280             }
281
282             var treechildren = document.createElement('treechildren');
283             this.node.appendChild(treechildren);
284             this.treechildren = treechildren;
285         }
286         if (typeof params.on_sort == 'function') {
287             this.on_sort = params.on_sort;
288         }
289         if (typeof params.on_checkbox_toggle == 'function') {
290             this.on_checkbox_toggle = params.on_checkbox_toggle;
291         }
292         obj.event_listeners.add(
293             this.node,
294             'select',
295             function(ev) {
296                 if (typeof params.on_select == 'function') {
297                     params.on_select(ev);
298                 }
299                 var x = document.getElementById(obj.node.id + '_clipfield');
300                 if (x) {
301                     var sel = obj.retrieve_selection();
302                     x.setAttribute('disabled', sel.length == 0);
303                 }
304             },
305             false
306         );
307         if (typeof params.on_click == 'function') {
308             obj.event_listeners.add(
309                 this.node,
310                 'click',
311                 params.on_click,
312                 false
313             );
314         }
315         if (typeof params.on_dblclick == 'function') {
316             obj.event_listeners.add(
317                 this.node,
318                 'dblclick',
319                 params.on_dblclick,
320                 false
321             );
322         }
323
324         /*
325         obj.event_listeners.add(
326             this.node,
327             'mousemove',
328             function(ev) { obj.detect_visible(); },
329             false
330         );
331         */
332         obj.event_listeners.add(
333             this.node,
334             'keypress',
335             function(ev) { obj.auto_retrieve(); },
336             false
337         );
338         obj.event_listeners.add(
339             this.node,
340             'click',
341             function(ev) { obj.auto_retrieve(); },
342             false
343         );
344         obj.event_listeners.add(
345             window,
346             'resize',
347             function(ev) { obj.auto_retrieve(); },
348             false
349         );
350         /* FIXME -- find events on scrollbar to trigger this */
351         obj.detect_visible_polling();    
352         /*
353         var scrollbar = document.getAnonymousNodes( document.getAnonymousNodes(this.node)[1] )[1];
354         var slider = document.getAnonymousNodes( scrollbar )[2];
355         alert('scrollbar = ' + scrollbar.nodeName + ' grippy = ' + slider.nodeName);
356         scrollbar.addEventListener('click',function(){alert('sb click');},false);
357         scrollbar.addEventListener('command',function(){alert('sb command');},false);
358         scrollbar.addEventListener('scroll',function(){alert('sb scroll');},false);
359         slider.addEventListener('click',function(){alert('slider click');},false);
360         slider.addEventListener('command',function(){alert('slider command');},false);
361         slider.addEventListener('scroll',function(){alert('slider scroll');},false);
362         */
363         obj.event_listeners.add(this.node, 'scroll',function(){ obj.auto_retrieve(); },false);
364
365         this.restores_columns(params);
366     },
367
368     '_init_listbox' : function (params) {
369         if (this.prebuilt) {
370         } else {
371             var listhead = document.createElement('listhead');
372             this.node.appendChild(listhead);
373
374             var listcols = document.createElement('listcols');
375             this.node.appendChild(listcols);
376
377             for (var i = 0; i < this.columns.length; i++) {
378                 var listheader = document.createElement('listheader');
379                 listhead.appendChild(listheader);
380                 var listcol = document.createElement('listcol');
381                 listcols.appendChild(listcol);
382                 for (var j in this.columns[i]) {
383                     listheader.setAttribute(j,this.columns[i][j]);
384                     listcol.setAttribute(j,this.columns[i][j]);
385                 };
386             }
387         }
388     },
389
390     'cleanup' : function () {
391         var obj = this;
392         obj.event_listeners.removeAll();
393     },
394
395     'save_columns' : function (params) {
396         var obj = this;
397         if (obj.data.hash.aous['gui.disable_local_save_columns']) {
398             alert(document.getElementById('offlineStrings').getString('list.column_save_disabled'));
399         } else {
400             switch (this.node.nodeName) {
401                 case 'tree' : this._save_columns_tree(params); break;
402                 default: throw('NYI: Need .save_columns() for ' + this.node.nodeName); break;
403             }
404         }
405     },
406
407     '_save_columns_tree' : function (params) {
408         var obj = this;
409         try {
410             var id = obj.node.getAttribute('id');
411             if (obj.columns_saved_under) { id = obj.columns_saved_under; }
412             if (!id) {
413                 alert("FIXME: The columns for this list cannot be saved because the list has no id.");
414                 return;
415             }
416             var my_cols = {};
417             var nl = obj.node.getElementsByTagName('treecol');
418             for (var i = 0; i < nl.length; i++) {
419                 var col = nl[i];
420                 var col_id = col.getAttribute('id');
421                 if (!col_id) {
422                     alert('FIXME: A column in this list does not have an id and cannot be saved');
423                     continue;
424                 }
425                 var col_hidden = col.getAttribute('hidden'); 
426                 var col_width = col.getAttribute('width'); 
427                 var col_ordinal = col.getAttribute('ordinal'); 
428                 my_cols[ col_id ] = { 'hidden' : col_hidden, 'width' : col_width, 'ordinal' : col_ordinal };
429             }
430             JSAN.use('util.file'); var file = new util.file('tree_columns_for_'+window.escape(id));
431             file.set_object(my_cols);
432             file.close();
433             alert(document.getElementById('offlineStrings').getString('list.columns_saved'));
434         } catch(E) {
435             obj.error.standard_unexpected_error_alert('_save_columns_tree',E);
436         }
437     },
438
439     'restores_columns' : function (params) {
440         var obj = this;
441         switch (this.node.nodeName) {
442             case 'tree' : this._restores_columns_tree(params); break;
443             default: throw('NYI: Need .restores_columns() for ' + this.node.nodeName); break;
444         }
445     },
446
447     '_restores_columns_tree' : function (params) {
448         var obj = this;
449         try {
450             var id = obj.node.getAttribute('id');
451             if (obj.columns_saved_under) { id = obj.columns_saved_under; }
452             if (!id) {
453                 alert("FIXME: The columns for this list cannot be restored because the list has no id.");
454                 return;
455             }
456
457             var my_cols;
458             if (! obj.data.hash.aous) { obj.data.hash.aous = {}; }
459             if (! obj.data.hash.aous['gui.disable_local_save_columns']) {
460                 JSAN.use('util.file'); var file = new util.file('tree_columns_for_'+window.escape(id));
461                 if (file._file.exists()) {
462                     my_cols = file.get_object(); file.close();
463                 }
464             }
465             /* local file will trump remote file if allowed, so save ourselves an http request if this is the case */
466             if (obj.data.hash.aous['url.remote_column_settings'] && ! my_cols ) {
467                 try {
468                     var x = new XMLHttpRequest();
469                     var url = obj.data.hash.aous['url.remote_column_settings'] + '/tree_columns_for_' + window.escape(id);
470                     x.open("GET", url, false);
471                     x.send(null);
472                     if (x.status == 200) {
473                         my_cols = JSON2js( x.responseText );
474                     }
475                 } catch(E) {
476                     // This can happen in the offline interface if you logged in previously and url.remote_column_settings is set.
477                     // 1) You may be really "offline" now
478                     // 2) the URL may just be a path component without a hostname (ie "/xul/column_settings/"), which won't work
479                     // when appended to chrome://open_ils_staff_client/
480                     dump('Error retrieving column settings from ' + url + ': ' + E + '\n');
481                 }
482             }
483
484             if (my_cols) {
485                 var nl = obj.node.getElementsByTagName('treecol');
486                 for (var i = 0; i < nl.length; i++) {
487                     var col = nl[i];
488                     var col_id = col.getAttribute('id');
489                     if (!col_id) {
490                         alert('FIXME: A column in this list does not have an id and cannot be saved');
491                         continue;
492                     }
493                     if (typeof my_cols[col_id] != 'undefined') {
494                         col.setAttribute('hidden',my_cols[col_id].hidden); 
495                         col.setAttribute('width',my_cols[col_id].width); 
496                         col.setAttribute('ordinal',my_cols[col_id].ordinal); 
497                     } else {
498                         obj.error.sdump('D_ERROR','WARNING: Column ' + col_id + ' did not have a saved state.');
499                     }
500                 }
501             }
502         } catch(E) {
503             obj.error.standard_unexpected_error_alert('_restore_columns_tree',E);
504         }
505     },
506
507     'clear' : function (params) {
508         var obj = this;
509         switch (this.node.nodeName) {
510             case 'tree' : this._clear_tree(params); break;
511             case 'listbox' : this._clear_listbox(params); break;
512             default: throw('NYI: Need .clear() for ' + this.node.nodeName); break;
513         }
514         this.error.sdump('D_LIST','Clearing list ' + this.node.getAttribute('id') + '\n');
515         this.row_count.total = 0;
516         this.row_count.fleshed = 0;
517         setTimeout( function() { obj.exec_on_all_fleshed(); }, 0 );
518     },
519
520     '_clear_tree' : function(params) {
521         var obj = this;
522         if (obj.error.sdump_levels.D_LIST_DUMP_ON_CLEAR) {
523             obj.error.sdump('D_LIST_DUMP_ON_CLEAR',obj.dump());
524         }
525         if (obj.error.sdump_levels.D_LIST_DUMP_WITH_KEYS_ON_CLEAR) {
526             obj.error.sdump('D_LIST_DUMP_WITH_KEYS_ON_CLEAR',obj.dump_with_keys());
527         }
528         while (obj.treechildren.lastChild) obj.treechildren.removeChild( obj.treechildren.lastChild );
529     },
530
531     '_clear_listbox' : function(params) {
532         var obj = this;
533         var items = [];
534         var nl = this.node.getElementsByTagName('listitem');
535         for (var i = 0; i < nl.length; i++) {
536             items.push( nl[i] );
537         }
538         for (var i = 0; i < items.length; i++) {
539             this.node.removeChild(items[i]);
540         }
541     },
542
543     'append' : function (params) {
544         var rnode;
545         var obj = this;
546         switch (this.node.nodeName) {
547             case 'tree' : rparams = this._append_to_tree(params); break;
548             case 'listbox' : rparams = this._append_to_listbox(params); break;
549             default: throw('NYI: Need .append() for ' + this.node.nodeName); break;
550         }
551         if (rparams && params.attributes) {
552             for (var i in params.attributes) {
553                 rparams.treeitem_node.setAttribute(i,params.attributes[i]);
554             }
555         }
556         this.row_count.total++;
557         if (this.row_count.fleshed == this.row_count.total) {
558             setTimeout( function() { obj.exec_on_all_fleshed(); }, 0 );
559         }
560         rparams.treeitem_node.setAttribute('unique_row_counter',obj.unique_row_counter);
561         rparams.unique_row_counter = obj.unique_row_counter++;
562         if (typeof params.on_append == 'function') {
563             params.on_append(rparams);
564         }
565         return rparams;
566     },
567     
568     'refresh_row' : function (params) {
569         var rnode;
570         var obj = this;
571         switch (this.node.nodeName) {
572             case 'tree' : rparams = this._refresh_row_in_tree(params); break;
573             default: throw('NYI: Need .refresh_row() for ' + this.node.nodeName); break;
574         }
575         if (rparams && params.attributes) {
576             for (var i in params.attributes) {
577                 rparams.treeitem_node.setAttribute(i,params.attributes[i]);
578             }
579         }
580         this.row_count.fleshed--;
581         return rparams;
582     },
583
584
585     '_append_to_tree' : function (params) {
586
587         var obj = this;
588
589         if (typeof params.row == 'undefined') throw('util.list.append: Object must contain a row');
590
591         var s = ('util.list.append: params = ' + (params) + '\n');
592
593         var treechildren_node = this.treechildren;
594
595         if (params.node && params.node.nodeName == 'treeitem') {
596             params.node.setAttribute('container','true'); /* params.node.setAttribute('open','true'); */
597             if (params.node.lastChild.nodeName == 'treechildren') {
598                 treechildren_node = params.node.lastChild;
599             } else {
600                 treechildren_node = document.createElement('treechildren');
601                 params.node.appendChild(treechildren_node);
602             }
603         }
604
605         var treeitem = document.createElement('treeitem');
606         treeitem.setAttribute('retrieve_id',params.retrieve_id);
607         if (typeof params.to_bottom != 'undefined') {
608             treechildren_node.appendChild( treeitem );
609             if (typeof params.no_auto_select == 'undefined') {
610                 if (!obj.auto_select_pending) {
611                     obj.auto_select_pending = true;
612                     setTimeout(function() {
613                         dump('auto-selecting\n');
614                         var idx = Number(obj.node.view.rowCount)-1;
615                         try { obj.node.view.selection.select(idx); } catch(E) { obj.error.sdump('D_WARN','tree auto select: ' + E + '\n'); }
616                         try { if (typeof params.on_select == 'function') params.on_select(); } catch(E) { obj.error.sdump('D_WARN','tree auto select, on_select: ' + E + '\n'); }
617                         obj.auto_select_pending = false;
618                         try { util.widgets.dispatch('flesh',obj.node.contentView.getItemAtIndex(idx).firstChild); } catch(E) { obj.error.sdump('D_WARN','tree auto select, flesh: ' + E + '\n'); }
619                     }, 1000);
620                 }
621             }
622         } else {
623             if (treechildren_node.firstChild) {
624                 treechildren_node.insertBefore( treeitem, treechildren_node.firstChild );
625             } else {
626                 treechildren_node.appendChild( treeitem );
627             }
628             if (typeof params.no_auto_select == 'undefined') {
629                 if (!obj.auto_select_pending) {
630                     obj.auto_select_pending = true;
631                     setTimeout(function() {
632                         try { obj.node.view.selection.select(0); } catch(E) { obj.error.sdump('D_WARN','tree auto select: ' + E + '\n'); }
633                         try { if (typeof params.on_select == 'function') params.on_select(); } catch(E) { obj.error.sdump('D_WARN','tree auto select, on_select: ' + E + '\n'); }
634                         obj.auto_select_pending = false;
635                         try { util.widgets.dispatch('flesh',obj.node.contentView.getItemAtIndex(0).firstChild); } catch(E) { obj.error.sdump('D_WARN','tree auto select, flesh: ' + E + '\n'); }
636                     }, 1000);
637                 }
638             }
639         }
640         var treerow = document.createElement('treerow');
641         treeitem.appendChild( treerow );
642         treerow.setAttribute('retrieve_id',params.retrieve_id);
643         if (params.row_properties) treerow.setAttribute('properties',params.row_properties);
644
645         s += ('tree = ' + this.node + '  treechildren = ' + treechildren_node + '\n');
646         s += ('treeitem = ' + treeitem + '  treerow = ' + treerow + '\n');
647
648         obj.put_retrieving_label(treerow);
649
650         if (typeof params.retrieve_row == 'function' || typeof this.retrieve_row == 'function') {
651             obj.event_listeners.add(
652                 treerow,
653                 'flesh',
654                 function() {
655
656                     if (treerow.getAttribute('retrieved') == 'true') return; /* already running */
657
658                     treerow.setAttribute('retrieved','true');
659
660                     //dump('fleshing = ' + params.retrieve_id + '\n');
661
662                     function inc_fleshed() {
663                         if (treerow.getAttribute('fleshed') == 'true') return; /* already fleshed */
664                         treerow.setAttribute('fleshed','true');
665                         obj.row_count.fleshed++;
666                         if (obj.row_count.fleshed >= obj.row_count.total) {
667                             setTimeout( function() { obj.exec_on_all_fleshed(); }, 0 );
668                         }
669                     }
670
671                     params.treeitem_node = treeitem;
672                     params.on_retrieve = function(p) {
673                         try {
674                             p.row = params.row;
675                             obj._map_row_to_treecell(p,treerow);
676                             inc_fleshed();
677                             var idx = obj.node.contentView.getIndexOfItem( params.treeitem_node );
678                             dump('idx = ' + idx + '\n');
679                             // if current row is selected, send another select event to re-sync data that the client code fetches on selects
680                             if ( obj.node.view.selection.isSelected( idx ) ) {
681                                 dump('dispatching select event for on_retrieve for idx = ' + idx + '\n');
682                                 util.widgets.dispatch('select',obj.node);
683                             }
684                         } catch(E) {
685                             // Let's not alert on this for now.  Getting contentView has no properties in record buckets under certain conditions
686                             dump('fixme2: ' + E + '\n');
687                         }
688                     }
689
690                     if (typeof params.retrieve_row == 'function') {
691
692                         params.retrieve_row( params );
693
694                     } else if (typeof obj.retrieve_row == 'function') {
695
696                             obj.retrieve_row( params );
697
698                     } else {
699                     
700                             inc_fleshed();
701                     }
702                     obj.refresh_ordinals();
703                 },
704                 false
705             );
706             if (typeof params.flesh_immediately != 'undefined') {
707                 if (params.flesh_immediately) {
708                     setTimeout(
709                         function() {
710                             util.widgets.dispatch('flesh',treerow);
711                         }, 0
712                     );
713                 }
714             }
715         } else {
716             obj.event_listeners.add(
717                 treerow,
718                 'flesh',
719                 function() {
720                     //dump('fleshing anon\n');
721                     if (treerow.getAttribute('fleshed') == 'true') return; /* already fleshed */
722                     obj._map_row_to_treecell(params,treerow);
723                     treerow.setAttribute('retrieved','true');
724                     treerow.setAttribute('fleshed','true');
725                     obj.row_count.fleshed++;
726                     if (obj.row_count.fleshed >= obj.row_count.total) {
727                         setTimeout( function() { obj.exec_on_all_fleshed(); }, 0 );
728                     }
729                     obj.refresh_ordinals();
730                 },
731                 false
732             );
733             if (typeof params.flesh_immediately != 'undefined') {
734                 if (params.flesh_immediately) {
735                     setTimeout(
736                         function() {
737                             util.widgets.dispatch('flesh',treerow);
738                         }, 0
739                     );
740                 }
741             }
742         }
743         this.error.sdump('D_LIST',s);
744
745             try {
746
747                 if (obj.trim_list && obj.row_count.total >= obj.trim_list) {
748                     // Remove oldest row
749                     //if (typeof params.to_bottom != 'undefined') 
750                     if (typeof params.to_top == 'undefined') {
751                         if (typeof params.on_delete == 'function') { params.on_delete( treechildren_node.firstChild.getAttribute('unique_row_counter') ); }
752                         treechildren_node.removeChild( treechildren_node.firstChild );
753                     } else {
754                         if (typeof params.on_delete == 'function') { params.on_delete( treechildren_node.lastChild.getAttribute('unique_row_counter') ); }
755                         treechildren_node.removeChild( treechildren_node.lastChild );
756                     }
757                 }
758             } catch(E) {
759             }
760
761         setTimeout( function() { obj.auto_retrieve(); obj.refresh_ordinals(); }, 0 );
762
763         params.treeitem_node = treeitem;
764         return params;
765     },
766
767     '_refresh_row_in_tree' : function (params) {
768
769         var obj = this;
770
771         if (typeof params.row == 'undefined') throw('util.list.refresh_row: Object must contain a row');
772         if (typeof params.treeitem_node == 'undefined') throw('util.list.refresh_row: Object must contain a treeitem_node');
773         if (params.treeitem_node.nodeName != 'treeitem') throw('util.list.refresh_rwo: treeitem_node must be a treeitem');
774
775         var s = ('util.list.refresh_row: params = ' + (params) + '\n');
776
777         var treeitem = params.treeitem_node;
778         treeitem.setAttribute('retrieve_id',params.retrieve_id);
779         if (typeof params.to_bottom != 'undefined') {
780             if (typeof params.no_auto_select == 'undefined') {
781                 if (!obj.auto_select_pending) {
782                     obj.auto_select_pending = true;
783                     setTimeout(function() {
784                         dump('auto-selecting\n');
785                         var idx = Number(obj.node.view.rowCount)-1;
786                         try { obj.node.view.selection.select(idx); } catch(E) { obj.error.sdump('D_WARN','tree auto select: ' + E + '\n'); }
787                         try { if (typeof params.on_select == 'function') params.on_select(); } catch(E) { obj.error.sdump('D_WARN','tree auto select, on_select: ' + E + '\n'); }
788                         obj.auto_select_pending = false;
789                         try { util.widgets.dispatch('flesh',obj.node.contentView.getItemAtIndex(idx).firstChild); } catch(E) { obj.error.sdump('D_WARN','tree auto select, flesh: ' + E + '\n'); }
790                     }, 1000);
791                 }
792             }
793         }
794         //var delete_me = [];
795         //for (var i in treeitem.childNodes) if (treeitem.childNodes[i].nodeName == 'treerow') delete_me.push(treeitem.childNodes[i]);
796         //for (var i = 0; i < delete_me.length; i++) treeitem.removeChild(delete_me[i]);
797         var prev_treerow = treeitem.firstChild; /* FIXME: worry about hierarchal lists like copy_browser? */
798         var treerow = document.createElement('treerow');
799         while (prev_treerow.firstChild) {
800             treerow.appendChild( prev_treerow.removeChild( prev_treerow.firstChild ) );
801         }
802         treeitem.replaceChild( treerow, prev_treerow );
803         treerow.setAttribute('retrieve_id',params.retrieve_id);
804         if (params.row_properties) treerow.setAttribute('properties',params.row_properties);
805
806         s += ('tree = ' + this.node.nodeName + '\n');
807         s += ('treeitem = ' + treeitem.nodeName + '  treerow = ' + treerow.nodeName + '\n');
808
809         obj.put_retrieving_label(treerow);
810
811         if (typeof params.retrieve_row == 'function' || typeof this.retrieve_row == 'function') {
812
813             s += 'found a retrieve_row function\n';
814
815             obj.event_listeners.add(
816                 treerow,
817                 'flesh',
818                 function() {
819
820                     if (treerow.getAttribute('retrieved') == 'true') return; /* already running */
821
822                     treerow.setAttribute('retrieved','true');
823
824                     //dump('fleshing = ' + params.retrieve_id + '\n');
825
826                     function inc_fleshed() {
827                         if (treerow.getAttribute('fleshed') == 'true') return; /* already fleshed */
828                         treerow.setAttribute('fleshed','true');
829                         obj.row_count.fleshed++;
830                         if (obj.row_count.fleshed >= obj.row_count.total) {
831                             setTimeout( function() { obj.exec_on_all_fleshed(); }, 0 );
832                         }
833                     }
834
835                     params.treeitem_node = treeitem;
836                     params.on_retrieve = function(p) {
837                         try {
838                             p.row = params.row;
839                             obj._map_row_to_treecell(p,treerow);
840                             inc_fleshed();
841                             var idx = obj.node.contentView.getIndexOfItem( params.treeitem_node );
842                             dump('idx = ' + idx + '\n');
843                             // if current row is selected, send another select event to re-sync data that the client code fetches on selects
844                             if ( obj.node.view.selection.isSelected( idx ) ) {
845                                 dump('dispatching select event for on_retrieve for idx = ' + idx + '\n');
846                                 util.widgets.dispatch('select',obj.node);
847                             }
848                         } catch(E) {
849                             // Let's not alert on this for now.  Getting contentView has no properties in record buckets under certain conditions
850                             dump('fixme2: ' + E + '\n');
851                         }
852                     }
853
854                     if (typeof params.retrieve_row == 'function') {
855
856                         params.retrieve_row( params );
857
858                     } else if (typeof obj.retrieve_row == 'function') {
859
860                             obj.retrieve_row( params );
861
862                     } else {
863                     
864                             inc_fleshed();
865                     }
866                     obj.refresh_ordinals();
867                 },
868                 false
869             );
870             if (typeof params.flesh_immediately != 'undefined') {
871                 if (params.flesh_immediately) {
872                     setTimeout(
873                         function() {
874                             util.widgets.dispatch('flesh',treerow);
875                         }, 0
876                     );
877                 }
878             }
879
880         } else {
881
882             s += 'did not find a retrieve_row function\n';
883
884             obj.event_listeners.add(
885                 treerow,
886                 'flesh',
887                 function() {
888                     //dump('fleshing anon\n');
889                     if (treerow.getAttribute('fleshed') == 'true') return; /* already fleshed */
890                     obj._map_row_to_treecell(params,treerow);
891                     treerow.setAttribute('retrieved','true');
892                     treerow.setAttribute('fleshed','true');
893                     obj.row_count.fleshed++;
894                     if (obj.row_count.fleshed >= obj.row_count.total) {
895                         setTimeout( function() { obj.exec_on_all_fleshed(); }, 0 );
896                     }
897                     obj.refresh_ordinals();
898                 },
899                 false
900             );
901             if (typeof params.flesh_immediately != 'undefined') {
902                 if (params.flesh_immediately) {
903                     setTimeout(
904                         function() {
905                             util.widgets.dispatch('flesh',treerow);
906                         }, 0
907                     );
908                 }
909             }
910
911         }
912
913             try {
914
915                 if (obj.trim_list && obj.row_count.total >= obj.trim_list) {
916                     // Remove oldest row
917                     //if (typeof params.to_bottom != 'undefined') 
918                     if (typeof params.to_top == 'undefined') {
919                         treechildren_node.removeChild( treechildren_node.firstChild );
920                     } else {
921                         treechildren_node.removeChild( treechildren_node.lastChild );
922                     }
923                 }
924             } catch(E) {
925             }
926
927         setTimeout( function() { obj.auto_retrieve(); obj.refresh_ordinals(); }, 0 );
928
929         JSAN.use('util.widgets'); util.widgets.dispatch('select',obj.node);
930
931         this.error.sdump('D_LIST',s);
932
933         return params;
934     },
935
936     'refresh_ordinals' : function() {
937         var obj = this;
938         try {
939             if (obj.refresh_ordinals_timeout_id) { return; }
940
941             function _refresh_ordinals(clear) {
942                 var nl = obj.node.getElementsByAttribute('label','_');
943                 for (var i = 0; i < nl.length; i++) {
944                     nl[i].setAttribute(
945                         'ord_col',
946                         'true'
947                     );
948                     nl[i].setAttribute( // treecell properties for css styling
949                         'properties',
950                         'ordinal'
951                     );
952                 }
953                 nl = obj.node.getElementsByAttribute('ord_col','true');
954                 for (var i = 0; i < nl.length; i++) {
955                     nl[i].setAttribute(
956                         'label',
957                         // we could just use 'i' here if we trust the order of elements
958                         1 + obj.node.contentView.getIndexOfItem(nl[i].parentNode.parentNode) // treeitem
959                     );
960                 }
961                 if (clear) { obj.refresh_ordinals_timeout_id = null; }
962             }
963
964             // spamming this to cover race conditions
965             setTimeout(_refresh_ordinals, 500); // for speedy looking UI updates
966             setTimeout(_refresh_ordinals, 2000); // for most uses
967             obj.refresh_ordinals_timeout_id = setTimeout(
968                 function() {
969                     _refresh_ordinals(true);
970                 },
971                 4000 // just in case, say with a slow rendering list
972             );
973
974         } catch(E) {
975             alert('Error in list.js, refresh_ordinals(): ' + E);
976         }
977     },
978
979     'put_retrieving_label' : function(treerow) {
980         var obj = this;
981         try {
982             for (var i = 0; i < obj.columns.length; i++) {
983                 var treecell;
984                 if (typeof treerow.childNodes[i] == 'undefined') {
985                     treecell = document.createElement('treecell');
986                     treerow.appendChild(treecell);
987                 } else {
988                     treecell = treerow.childNodes[i];
989                 }
990                 treecell.setAttribute('label',document.getElementById('offlineStrings').getString('list.row_retrieving'));
991             }
992         } catch(E) {
993             alert('Error in list.js, put_retrieving_label(): ' + E);
994         }
995     },
996
997     'detect_visible' : function() {
998         var obj = this;
999         try {
1000             //dump('detect_visible  obj.node = ' + obj.node + '\n');
1001             /* FIXME - this is a hack.. if the implementation of tree changes, this could break */
1002             try {
1003                 /*var s = ''; var A = document.getAnonymousNodes(obj.node);
1004                 for (var i in A) {
1005                     var B = A[i];
1006                     s += '\t' + (typeof B.nodeName != 'undefined' ? B.nodeName : B ) + '\n'; 
1007                     if (typeof B.childNodes != 'undefined') for (var j = 0; j < B.childNodes.length; j++) {
1008                         var C = B.childNodes[j];
1009                         s += '\t\t' + C.nodeName + '\n';
1010                     }
1011                 }
1012                 obj.error.sdump('D_XULRUNNER','document.getAnonymousNodes(' + obj.node.nodeName + ') = \n' + s + '\n');*/
1013                 var scrollbar = document.getAnonymousNodes(obj.node)[2].firstChild;
1014                 var curpos = scrollbar.getAttribute('curpos');
1015                 var maxpos = scrollbar.getAttribute('maxpos');
1016                 //alert('curpos = ' + curpos + ' maxpos = ' + maxpos + ' obj.curpos = ' + obj.curpos + ' obj.maxpos = ' + obj.maxpos + '\n');
1017                 if ((curpos != obj.curpos) || (maxpos != obj.maxpos)) {
1018                     if ( obj.auto_retrieve() > 0 ) {
1019                         obj.curpos = curpos; obj.maxpos = maxpos;
1020                     }
1021                 }
1022             } catch(E) {
1023                 obj.error.sdump('D_XULRUNNER', 'List implementation changed? ' + E);
1024             }
1025         } catch(E) { obj.error.sdump('D_ERROR',E); }
1026     },
1027
1028     'detect_visible_polling' : function() {
1029         try {
1030             //alert('detect_visible_polling');
1031             var obj = this;
1032             obj.detect_visible();
1033             setTimeout(function() { try { obj.detect_visible_polling(); } catch(E) { alert(E); } },2000);
1034         } catch(E) {
1035             alert(E);
1036         }
1037     },
1038
1039
1040     'auto_retrieve' : function(params) {
1041         var obj = this;
1042         switch (this.node.nodeName) {
1043             case 'tree' : obj._auto_retrieve_tree(params); break;
1044             default: throw('NYI: Need .auto_retrieve() for ' + obj.node.nodeName); break;
1045         }
1046     },
1047
1048     '_auto_retrieve_tree' : function (params) {
1049         var obj = this;
1050         if (!obj.auto_retrieve_in_progress) {
1051             obj.auto_retrieve_in_progress = true;
1052             setTimeout(
1053                 function() {
1054                     try {
1055                             //alert('auto_retrieve\n');
1056                             var count = 0;
1057                             var startpos = obj.node.treeBoxObject.getFirstVisibleRow();
1058                             var endpos = obj.node.treeBoxObject.getLastVisibleRow();
1059                             if (startpos > endpos) endpos = obj.node.treeBoxObject.getPageLength();
1060                             //dump('startpos = ' + startpos + ' endpos = ' + endpos + '\n');
1061                             for (var i = startpos; i < endpos + 4; i++) {
1062                                 try {
1063                                     //dump('trying index ' + i + '\n');
1064                                     var item = obj.node.contentView.getItemAtIndex(i).firstChild;
1065                                     if (item && item.getAttribute('retrieved') != 'true' ) {
1066                                         //dump('\tgot an unfleshed item = ' + item + ' = ' + item.nodeName + '\n');
1067                                         util.widgets.dispatch('flesh',item); count++;
1068                                     }
1069                                 } catch(E) {
1070                                     //dump(i + ' : ' + E + '\n');
1071                                 }
1072                             }
1073                             obj.auto_retrieve_in_progress = false;
1074                             return count;
1075                     } catch(E) { alert(E); }
1076                 }, 1
1077             );
1078         }
1079     },
1080
1081     'exec_on_all_fleshed' : function() {
1082         var obj = this;
1083         try {
1084             if (obj.on_all_fleshed) {
1085                 if (typeof obj.on_all_fleshed == 'function') {
1086                     dump('exec_on_all_fleshed == function\n');
1087                     setTimeout( 
1088                         function() { 
1089                             try { obj.on_all_fleshed(); } catch(E) { obj.error.standard_unexpected_error_alert('_full_retrieve_tree callback',obj.on_all_fleshed); }
1090                         }, 0 
1091                     );
1092                 } else if (typeof obj.on_all_fleshed.length != 'undefined') {
1093                     dump('exec_on_all_fleshed == array\n');
1094                     setTimeout(
1095                         function() {
1096                             try {
1097                                 dump('exec_on_all_fleshed, processing on_all_fleshed array, length = ' + obj.on_all_fleshed.length + '\n');
1098                                 var f = obj.on_all_fleshed.pop();
1099                                 if (typeof f == 'function') { 
1100                                     try { f(); } catch(E) { obj.error.standard_unexpected_error_alert('_full_retrieve_tree callback',E); }
1101                                 }
1102                                 if (obj.on_all_fleshed.length > 0) arguments.callee(); 
1103                             } catch(E) {
1104                                 obj.error.standard_unexpected_error_alert('exec_on_all_fleshed callback error',E);
1105                             }
1106                         }, 0
1107                     ); 
1108                 } else {
1109                     obj.error.standard_unexpected_error_alert('unexpected on_all_fleshed object: ', obj.on_all_fleshed);
1110                 }
1111             }
1112         } catch(E) {
1113             obj.error.standard_unexpected_error_alert('exec_on_all-fleshed error',E);
1114         }
1115     },
1116
1117     'full_retrieve' : function(params) {
1118         var obj = this;
1119         switch (this.node.nodeName) {
1120             case 'tree' : obj._full_retrieve_tree(params); break;
1121             default: throw('NYI: Need .full_retrieve() for ' + obj.node.nodeName); break;
1122         }
1123         obj.refresh_ordinals();
1124     },
1125
1126     '_full_retrieve_tree' : function(params) {
1127         var obj = this;
1128         try {
1129             if (obj.row_count.fleshed >= obj.row_count.total) {
1130                 dump('Full retrieve... tree seems to be in sync\n' + js2JSON(obj.row_count) + '\n');
1131                 obj.exec_on_all_fleshed();
1132             } else {
1133                 dump('Full retrieve... syncing tree' + js2JSON(obj.row_count) + '\n');
1134                 JSAN.use('util.widgets');
1135                 var nodes = obj.treechildren.childNodes;
1136                 for (var i = 0; i < nodes.length; i++) {
1137                     util.widgets.dispatch('flesh',nodes[i].firstChild);
1138                 }
1139             }
1140         } catch(E) {
1141             obj.error.standard_unexpected_error_alert('_full_retrieve_tree',E);
1142         }
1143     },
1144
1145     '_append_to_listbox' : function (params) {
1146
1147         var obj = this;
1148
1149         if (typeof params.row == 'undefined') throw('util.list.append: Object must contain a row');
1150
1151         var s = ('util.list.append: params = ' + (params) + '\n');
1152
1153         var listitem = document.createElement('listitem');
1154
1155         s += ('listbox = ' + this.node + '  listitem = ' + listitem + '\n');
1156
1157         if (typeof params.retrieve_row == 'function' || typeof this.retrieve_row == 'function') {
1158
1159             setTimeout(
1160                 function() {
1161                     listitem.setAttribute('retrieve_id',params.retrieve_id);
1162                     //FIXME//Make async and fire when row is visible in list
1163                     var row;
1164
1165                     params.treeitem_node = listitem;
1166                     params.on_retrieve = function(row) {
1167                         params.row = row;
1168                         obj._map_row_to_listcell(params,listitem);
1169                         obj.node.appendChild( listitem );
1170                         util.widgets.dispatch('select',obj.node);
1171                     }
1172
1173                     if (typeof params.retrieve_row == 'function') {
1174
1175                         row = params.retrieve_row( params );
1176
1177                     } else {
1178
1179                         if (typeof obj.retrieve_row == 'function') {
1180
1181                             row = obj.retrieve_row( params );
1182
1183                         }
1184                     }
1185                 }, 0
1186             );
1187         } else {
1188             this._map_row_to_listcell(params,listitem);
1189             this.node.appendChild( listitem );
1190         }
1191
1192         this.error.sdump('D_LIST',s);
1193         params.treeitem_node = listitem;
1194         return params;
1195
1196     },
1197
1198     '_map_row_to_treecell' : function(params,treerow) {
1199         var obj = this;
1200         var s = '';
1201
1202         if (typeof params.map_row_to_column == 'function' || typeof this.map_row_to_column == 'function') {
1203
1204             for (var i = 0; i < this.columns.length; i++) {
1205                 var treecell;
1206                 if (typeof treerow.childNodes[i] == 'undefined') {
1207                     treecell = document.createElement('treecell');
1208                     treerow.appendChild( treecell );
1209                 } else {
1210                     treecell = treerow.childNodes[i];
1211                 }
1212                 
1213                 if ( this.columns[i].editable == false ) { treecell.setAttribute('editable','false'); }
1214                 var label = '';
1215                 var sort_value = '';
1216
1217                 // What skip columns is doing is rendering the treecells as blank/empty
1218                 if (params.skip_columns && (params.skip_columns.indexOf(i) != -1)) {
1219                     treecell.setAttribute('label',label);
1220                     s += ('treecell = ' + treecell + ' with label = ' + label + '\n');
1221                     continue;
1222                 }
1223                 if (params.skip_all_columns_except && (params.skip_all_columns_except.indexOf(i) == -1)) {
1224                     treecell.setAttribute('label',label);
1225                     s += ('treecell = ' + treecell + ' with label = ' + label + '\n');
1226                     continue;
1227                 }
1228     
1229                 if (typeof params.map_row_to_column == 'function')  {
1230     
1231                     label = params.map_row_to_column(params.row,this.columns[i],this.scratch_data);
1232     
1233                 } else if (typeof this.map_row_to_column == 'function') {
1234     
1235                     label = this.map_row_to_column(params.row,this.columns[i],this.scratch_data);
1236     
1237                 }
1238                 if (this.columns[i].type == 'checkbox') { treecell.setAttribute('value',label); } else { treecell.setAttribute('label',label ? label : ''); }
1239                 s += ('treecell = ' + treecell + ' with label = ' + label + '\n');
1240             }
1241         } else if (typeof params.map_row_to_columns == 'function' || typeof this.map_row_to_columns == 'function') {
1242
1243             var labels = [];
1244             var sort_values = [];
1245
1246             if (typeof params.map_row_to_columns == 'function') {
1247
1248                 var values = params.map_row_to_columns(params.row,this.columns,this.scratch_data);
1249                 if (typeof values.values == 'undefined') {
1250                     labels = values;
1251                 } else {
1252                     labels = values.values;
1253                     sort_values = values.sort_values;
1254                 }
1255
1256             } else if (typeof this.map_row_to_columns == 'function') {
1257
1258                 var values = this.map_row_to_columns(params.row,this.columns,this.scratch_data);
1259                 if (typeof values.values == 'undefined') {
1260                     labels = values;
1261                 } else {
1262                     labels = values.values;
1263                     sort_values = values.sort_values;
1264                 }
1265             }
1266             for (var i = 0; i < labels.length; i++) {
1267                 var treecell;
1268                 if (typeof treerow.childNodes[i] == 'undefined') {
1269                     treecell = document.createElement('treecell');
1270                     treerow.appendChild(treecell);
1271                 } else {
1272                     treecell = treerow.childNodes[i];
1273                 }
1274                 if ( this.columns[i].editable == false ) { treecell.setAttribute('editable','false'); }
1275                 if ( this.columns[i].type == 'checkbox') {
1276                     treecell.setAttribute('value', labels[i]);
1277                 } else {
1278                     treecell.setAttribute('label',typeof labels[i] == 'string' || typeof labels[i] == 'number' ? labels[i] : '');
1279                 }
1280                 if (sort_values[i]) {
1281                     treecell.setAttribute('sort_value',js2JSON(sort_values[i]));
1282                 }
1283                 s += ('treecell = ' + treecell + ' with label = ' + labels[i] + '\n');
1284             }
1285
1286         } else {
1287
1288             throw('No row to column mapping function.');
1289         }
1290         this.error.sdump('D_LIST',s);
1291     },
1292
1293     '_map_row_to_listcell' : function(params,listitem) {
1294         var obj = this;
1295         var s = '';
1296         for (var i = 0; i < this.columns.length; i++) {
1297             var value = '';
1298             if (typeof params.map_row_to_column == 'function')  {
1299
1300                 value = params.map_row_to_column(params.row,this.columns[i],this.scratch_data);
1301
1302             } else {
1303
1304                 if (typeof this.map_row_to_column == 'function') {
1305
1306                     value = this.map_row_to_column(params.row,this.columns[i],this.scratch_data);
1307                 }
1308             }
1309             if (typeof value == 'string' || typeof value == 'number') {
1310                 var listcell = document.createElement('listcell');
1311                 listcell.setAttribute('label',value);
1312                 listitem.appendChild(listcell);
1313                 s += ('listcell = ' + listcell + ' with label = ' + value + '\n');
1314             } else {
1315                 listitem.appendChild(value);
1316                 s += ('listcell = ' + value + ' is really a ' + value.nodeName + '\n');
1317             }
1318         }
1319         this.error.sdump('D_LIST',s);
1320     },
1321
1322     'select_all' : function(params) {
1323         var obj = this;
1324         switch(this.node.nodeName) {
1325             case 'tree' : return this._select_all_from_tree(params); break;
1326             default: throw('NYI: Need ._select_all_from_() for ' + this.node.nodeName); break;
1327         }
1328     },
1329
1330     '_select_all_from_tree' : function(params) {
1331         var obj = this;
1332         this.node.view.selection.selectAll();
1333     },
1334
1335     'retrieve_selection' : function(params) {
1336         var obj = this;
1337         switch(this.node.nodeName) {
1338             case 'tree' : return this._retrieve_selection_from_tree(params); break;
1339             default: throw('NYI: Need ._retrieve_selection_from_() for ' + this.node.nodeName); break;
1340         }
1341     },
1342
1343     '_retrieve_selection_from_tree' : function(params) {
1344         var obj = this;
1345         var list = [];
1346         var start = new Object();
1347         var end = new Object();
1348         var numRanges = this.node.view.selection.getRangeCount();
1349         for (var t=0; t<numRanges; t++){
1350             this.node.view.selection.getRangeAt(t,start,end);
1351             for (var v=start.value; v<=end.value; v++){
1352                 var i = this.node.contentView.getItemAtIndex(v);
1353                 list.push( i );
1354             }
1355         }
1356         return list;
1357     },
1358
1359     'dump' : function(params) {
1360         var obj = this;
1361         switch(this.node.nodeName) {
1362             case 'tree' : return this._dump_tree(params); break;
1363             default: throw('NYI: Need .dump() for ' + this.node.nodeName); break;
1364         }
1365     },
1366
1367     '_dump_tree' : function(params) {
1368         var obj = this;
1369         var dump = [];
1370         for (var i = 0; i < this.treechildren.childNodes.length; i++) {
1371             var row = [];
1372             var treeitem = this.treechildren.childNodes[i];
1373             var treerow = treeitem.firstChild;
1374             for (var j = 0; j < treerow.childNodes.length; j++) {
1375                 row.push( treerow.childNodes[j].getAttribute('label') );
1376             }
1377             dump.push( row );
1378         }
1379         return dump;
1380     },
1381
1382     'dump_with_keys' : function(params) {
1383         var obj = this;
1384         switch(this.node.nodeName) {
1385             case 'tree' : return this._dump_tree_with_keys(params); break;
1386             default: throw('NYI: Need .dump_with_keys() for ' + this.node.nodeName); break;
1387         }
1388
1389     },
1390
1391     '_dump_tree_with_keys' : function(params) {
1392         var obj = this;
1393         var dump = [];
1394
1395         function process_tree(treechildren) {
1396             for (var i = 0; i < treechildren.childNodes.length; i++) {
1397                 var row = {};
1398                 var treeitem = treechildren.childNodes[i];
1399                 var treerow = treeitem.firstChild;
1400                 for (var j = 0; j < treerow.childNodes.length; j++) {
1401                     if (typeof obj.columns[j] == 'undefined') {
1402                         dump('=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=\n');
1403                         dump('_dump_tree_with_keys @ ' + location.href + '\n');
1404                         dump('\ttreerow.childNodes.length='+treerow.childNodes.length+' j='+j+' obj.columns.length='+obj.columns.length+'\n');
1405                         debugger;
1406                     } else {
1407                         row[ obj.columns[j].id ] = treerow.childNodes[j].getAttribute('label');
1408                         var sort = treerow.childNodes[j].getAttribute('sort_value');
1409                         if(sort) {
1410                             row[ obj.columns[j].id + '_sort_value' ] = sort;
1411                         }
1412                     }
1413                 }
1414                 dump.push( row );
1415                 if (treeitem.childNodes.length > 1) {
1416                     process_tree(treeitem.lastChild);
1417                 }
1418             }
1419         }
1420
1421         process_tree(this.treechildren);
1422
1423         return dump;
1424     },
1425
1426     'dump_csv' : function(params) {
1427         var obj = this;
1428         switch(this.node.nodeName) {
1429             case 'tree' : return this._dump_tree_csv(params); break;
1430             default: throw('NYI: Need .dump_csv() for ' + this.node.nodeName); break;
1431         }
1432
1433     },
1434
1435     '_dump_tree_csv' : function(params) {
1436         var obj = this;
1437         var _dump = '';
1438         var ord_cols = [];
1439         for (var j = 0; j < obj.columns.length; j++) {
1440             if (obj.node.treeBoxObject.columns.getColumnAt(j).element.getAttribute('hidden') == 'true') {
1441                 /* skip */
1442             } else {
1443                 ord_cols.push( [ obj.node.treeBoxObject.columns.getColumnAt(j).element.getAttribute('ordinal'), j ] );
1444             }
1445         }
1446         ord_cols.sort( function(a,b) { 
1447             if ( Number( a[0] ) < Number( b[0] ) ) return -1; 
1448             if ( Number( a[0] ) > Number( b[0] ) ) return 1; 
1449             return 0;
1450         } );
1451         for (var j = 0; j < ord_cols.length; j++) {
1452             if (_dump) _dump += ',';
1453             _dump += '"' + obj.columns[ ord_cols[j][1] ].label.replace(/"/g, '""') + '"';
1454         }
1455         _dump += '\r\n';
1456
1457         function process_tree(treechildren) {
1458             for (var i = 0; i < treechildren.childNodes.length; i++) {
1459                 var row = '';
1460                 var treeitem = treechildren.childNodes[i];
1461                 var treerow = treeitem.firstChild;
1462                 for (var j = 0; j < ord_cols.length; j++) {
1463                     if (row) row += ',';
1464                     row += '"' + treerow.childNodes[ ord_cols[j][1] ].getAttribute('label').replace(/"/g, '""') + '"';
1465                 }
1466                 _dump +=  row + '\r\n';
1467                 if (treeitem.childNodes.length > 1) {
1468                     process_tree(treeitem.lastChild);
1469                 }
1470             }
1471         }
1472
1473         process_tree(this.treechildren);
1474
1475         return _dump;
1476     },
1477
1478     'dump_extended_format' : function(params) {
1479         var obj = this;
1480         switch(this.node.nodeName) {
1481             case 'tree' : return this._dump_tree_extended_format(params); break;
1482             default: throw('NYI: Need .dump_extended_format() for ' + this.node.nodeName); break;
1483         }
1484
1485     },
1486
1487     '_dump_tree_extended_format' : function(params) {
1488         var obj = this;
1489         var _dump = '';
1490         var ord_cols = [];
1491         for (var j = 0; j < obj.columns.length; j++) {
1492             if (obj.node.treeBoxObject.columns.getColumnAt(j).element.getAttribute('hidden') == 'true') {
1493                 /* skip */
1494             } else {
1495                 ord_cols.push( [ obj.node.treeBoxObject.columns.getColumnAt(j).element.getAttribute('ordinal'), j ] );
1496             }
1497         }
1498         ord_cols.sort( function(a,b) { 
1499             if ( Number( a[0] ) < Number( b[0] ) ) return -1; 
1500             if ( Number( a[0] ) > Number( b[0] ) ) return 1; 
1501             return 0;
1502         } );
1503
1504         function process_tree(treechildren) {
1505             for (var i = 0; i < treechildren.childNodes.length; i++) {
1506                 var row = document.getElementById('offlineStrings').getString('list.dump_extended_format.record_separator') + '\r\n';
1507                 var treeitem = treechildren.childNodes[i];
1508                 var treerow = treeitem.firstChild;
1509                 for (var j = 0; j < ord_cols.length; j++) {
1510                     row += obj.columns[ ord_cols[j][1] ].label + ': ' + treerow.childNodes[ ord_cols[j][1] ].getAttribute('label') + '\r\n';
1511                 }
1512                 _dump +=  row + '\r\n';
1513                 if (treeitem.childNodes.length > 1) {
1514                     process_tree(treeitem.lastChild);
1515                 }
1516             }
1517         }
1518
1519         process_tree(this.treechildren);
1520
1521         return _dump;
1522     },
1523
1524     'dump_csv_to_clipboard' : function(params) {
1525         var obj = this;
1526         if (typeof params == 'undefined') params = {};
1527         if (params.no_full_retrieve) {
1528             copy_to_clipboard( obj.dump_csv( params ) );
1529         } else {
1530             obj.wrap_in_full_retrieve( function() { copy_to_clipboard( obj.dump_csv( params ) ); } );
1531         }
1532     },
1533
1534     'dump_csv_to_printer' : function(params) {
1535         var obj = this;
1536         if (typeof params == 'undefined') params = {};
1537         JSAN.use('util.print'); var print = new util.print(params.printer_context || obj.printer_context);
1538         if (params.no_full_retrieve) {
1539             print.simple( obj.dump_csv( params ), {'content_type':'text/plain'} );
1540         } else {
1541             obj.wrap_in_full_retrieve( 
1542                 function() { 
1543                     print.simple( obj.dump_csv( params ), {'content_type':'text/plain'} );
1544                 }
1545             );
1546         }
1547     },
1548
1549     'dump_extended_format_to_printer' : function(params) {
1550         var obj = this;
1551         if (typeof params == 'undefined') params = {};
1552         JSAN.use('util.print'); var print = new util.print(params.printer_context || obj.printer_context);
1553         if (params.no_full_retrieve) {
1554             print.simple( obj.dump_extended_format( params ), {'content_type':'text/plain'} );
1555         } else {
1556             obj.wrap_in_full_retrieve( 
1557                 function() { 
1558                     print.simple( obj.dump_extended_format( params ), {'content_type':'text/plain'} );
1559                 }
1560             );
1561         }
1562     },
1563
1564     'dump_csv_to_file' : function(params) {
1565         var obj = this;
1566         JSAN.use('util.file'); var f = new util.file();
1567         if (typeof params == 'undefined') params = {};
1568         if (params.no_full_retrieve) {
1569             params.data = obj.dump_csv( params );
1570             params.not_json = true;
1571             if (!params.title) params.title = document.getElementById('offlineStrings').getString('list.save_csv_as');
1572             f.export_file( params );
1573         } else {
1574             obj.wrap_in_full_retrieve( 
1575                 function() { 
1576                     params.data = obj.dump_csv( params );
1577                     params.not_json = true;
1578                     if (!params.title) params.title = document.getElementById('offlineStrings').getString('list.save_csv_as');
1579                     f.export_file( params );
1580                 }
1581             );
1582         }
1583     },
1584
1585     'print' : function(params) {
1586         if (!params) params = {};
1587         switch(this.node.nodeName) {
1588             case 'tree' : return this._print_tree(params); break;
1589             default: throw('NYI: Need ._print() for ' + this.node.nodeName); break;
1590         }
1591     },
1592
1593     '_print_tree' : function(params) {
1594         var obj = this;
1595         try {
1596             var data = obj.data; data.stash_retrieve();
1597             if (!params.staff && data.list.au && data.list.au[0]) {
1598                 params.staff = data.list.au[0];
1599             }
1600             if (!params.lib && data.list.au && data.list.au[0] && data.list.au[0].ws_ou() && data.hash.aou && data.hash.aou[ data.list.au[0].ws_ou() ]) {
1601                 params.lib = JSON2js( js2JSON( data.hash.aou[ data.list.au[0].ws_ou() ] ) ); // clone this sucker
1602                 params.lib.children(null); // since we're modifying it
1603             }
1604             if (params.template && data.print_list_templates[ params.template ]) {
1605                 var template = data.print_list_templates[ params.template ];
1606                 if (template.inherit) {
1607                     template = data.print_list_templates[ template.inherit ];
1608                     // if someone wants to implement recursion later, feel free
1609                 }
1610                 for (var i in template) params[i] = template[i];
1611             }
1612             obj.wrap_in_full_retrieve(
1613                 function() {
1614                     try {
1615                         if (!params.list) params.list = obj.dump_with_keys();
1616                         JSAN.use('util.print'); var print = new util.print(params.printer_context || obj.printer_context);
1617                         print.tree_list( params );
1618                         if (typeof params.callback == 'function') params.callback();
1619                     } catch(E) {
1620                         obj.error.standard_unexpected_error_alert('inner _print_tree',E);
1621                     }
1622                 }
1623             );
1624             
1625         } catch(E) {
1626             obj.error.standard_unexpected_error_alert('_print_tree',E);
1627         }
1628     },
1629
1630     'dump_selected_with_keys' : function(params) {
1631         var obj = this;
1632         switch(this.node.nodeName) {
1633             case 'tree' : return this._dump_tree_selection_with_keys(params); break;
1634             default: throw('NYI: Need .dump_selection_with_keys() for ' + this.node.nodeName); break;
1635         }
1636
1637     },
1638
1639     '_dump_tree_selection_with_keys' : function(params) {
1640         var obj = this;
1641         var dump = [];
1642         var list = obj._retrieve_selection_from_tree();
1643         for (var i = 0; i < list.length; i++) {
1644             var row = {};
1645             var treeitem = list[i];
1646             var treerow = treeitem.firstChild;
1647             for (var j = 0; j < treerow.childNodes.length; j++) {
1648                 var value = treerow.childNodes[j].getAttribute('label');
1649                 if (params.skip_hidden_columns) if (obj.node.treeBoxObject.columns.getColumnAt(j).element.getAttribute('hidden') == 'true') continue;
1650                 if (typeof obj.columns[j] == 'undefined') {
1651                     dump('=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=\n');
1652                     dump('_dump_tree_selection_with_keys @ ' + location.href + '\n');
1653                     dump('\ttreerow.childNodes.length='+treerow.childNodes.length+' j='+j+' obj.columns.length='+obj.columns.length+'\n');
1654                     debugger;
1655                 } else {
1656                     var id = obj.columns[j].id; if (params.labels_instead_of_ids) id = obj.columns[j].label;
1657                     row[ id ] = value;
1658                 }
1659             }
1660             dump.push( row );
1661         }
1662         return dump;
1663     },
1664
1665     'clipboard' : function(params) {
1666         try {
1667             var obj = this;
1668             var dump = obj.dump_selected_with_keys({'skip_hidden_columns':true,'labels_instead_of_ids':true});
1669             obj.data.stash_retrieve();
1670             obj.data.list_clipboard = dump; obj.data.stash('list_clipboard');
1671             JSAN.use('util.window'); var win = new util.window();
1672             win.open(urls.XUL_LIST_CLIPBOARD,'list_clipboard','chrome,resizable,modal');
1673             window.focus(); // sometimes the main window will lower after a clipboard action
1674         } catch(E) {
1675             this.error.standard_unexpected_error_alert('clipboard',E);
1676         }
1677     },
1678
1679     'dump_retrieve_ids' : function(params) {
1680         var obj = this;
1681         switch(this.node.nodeName) {
1682             case 'tree' : return this._dump_retrieve_ids_tree(params); break;
1683             default: throw('NYI: Need .dump_retrieve_ids() for ' + this.node.nodeName); break;
1684         }
1685     },
1686
1687     '_dump_retrieve_ids_tree' : function(params) {
1688         var obj = this;
1689         var dump = [];
1690         for (var i = 0; i < this.treechildren.childNodes.length; i++) {
1691             var treeitem = this.treechildren.childNodes[i];
1692             dump.push( treeitem.getAttribute('retrieve_id') );
1693         }
1694         return dump;
1695     },
1696
1697     'wrap_in_full_retrieve' : function(f) {
1698         var obj = this;
1699         if (typeof obj.on_all_fleshed == 'function') { // legacy
1700             obj.on_all_fleshed = [ obj.on_all_fleshed ];
1701         }
1702         if (! obj.on_all_fleshed) obj.on_all_fleshed = [];
1703         obj.on_all_fleshed.push(f);
1704         obj.full_retrieve();
1705     },
1706
1707     '_sort_tree' : function() {
1708         var obj = this;
1709         try {
1710             if (obj.node.getAttribute('no_sort')) {
1711                 return;
1712             }
1713
1714             var sorts = [ obj.first_sort ].concat( obj.sub_sorts );
1715             var columns = util.functional.map_list(
1716                 sorts,
1717                 function(e,idx) {
1718                     return e.target;
1719                 }
1720             );
1721             var column_positions = [];
1722             for (var i = 0; i < columns.length; i++) {
1723                 for (var j = 0; j < obj.columns.length; j++) {
1724                     if (obj.columns[j].id == columns[i].id) {
1725                         column_positions.push( function(a){return a;}(j) );
1726                     }
1727                 }
1728             }
1729             obj.wrap_in_full_retrieve(
1730                 function() {
1731                     try {
1732                         JSAN.use('util.money');
1733                         var rows = [];
1734                         var treeitems = obj.treechildren.childNodes;
1735                         for (var i = 0; i < treeitems.length; i++) {
1736                             var treeitem = treeitems[i];
1737                             var treerow = treeitem.firstChild;
1738
1739                             function get_value(treecell) {
1740                                 value = ( {
1741                                     'value' : treecell
1742                                         ? treecell.getAttribute('label')
1743                                         : '',
1744                                     'sort_value' : treecell ? treecell.hasAttribute('sort_value')
1745                                         ? JSON2js(
1746                                             treecell.getAttribute('sort_value'))
1747                                         : '' : ''
1748                                 } );
1749                                 return value;
1750                             }
1751
1752                             var values = [];
1753                             for (var j = 0; j < column_positions.length; j++) {
1754                                 var treecell = treerow.childNodes[ column_positions[j] ];
1755                                 values.push({
1756                                     'position' : column_positions[j],
1757                                     'value' : get_value(treecell)
1758                                 });
1759                             }
1760
1761                             rows.push({
1762                                 'values' : values,
1763                                 'node' : treeitem
1764                             });
1765                         }
1766                         rows = rows.sort( function(A,B) {
1767                             function normalize(a,b,p) {
1768                                 if (a.sort_value) {
1769                                     a = a.sort_value;
1770                                     b = b.sort_value;
1771                                 } else {
1772                                     a = a.value;
1773                                     b = b.value;
1774                                     if (obj.columns[p].sort_type) {
1775                                         switch(obj.columns[p].sort_type) {
1776                                             case 'date' :
1777                                                 JSAN.use('util.date'); // to pull in dojo.date.locale
1778                                                 a = dojo.date.locale.parse(a,{});
1779                                                 b = dojo.date.locale.parse(b,{});
1780                                             break;
1781                                             case 'number' :
1782                                                 a = Number(a); b = Number(b);
1783                                             break;
1784                                             case 'money' :
1785                                                 a = util.money.dollars_float_to_cents_integer(a);
1786                                                 b = util.money.dollars_float_to_cents_integer(b);
1787                                             break;
1788                                             case 'title' : /* special case for "a" and "the".  doesn't use marc 245 indicator */
1789                                                 a = String( a ).toUpperCase().replace( /^\s*(THE|A|AN)\s+/, '' );
1790                                                 b = String( b ).toUpperCase().replace( /^\s*(THE|A|AN)\s+/, '' );
1791                                             break;
1792                                             default:
1793                                                 a = String( a ).toUpperCase();
1794                                                 b = String( b ).toUpperCase();
1795                                             break;
1796                                         }
1797                                     } else {
1798                                         if (typeof a == 'string' || typeof b == 'string') {
1799                                             a = String( a ).toUpperCase();
1800                                             b = String( b ).toUpperCase();
1801                                         }
1802                                     }
1803                                 }
1804                                 return [ a, b ];
1805                             }
1806
1807                             for (var i = 0; i < sorts.length; i++) {
1808                                 var values;
1809                                 if (sorts[i].sortDir == 'asc') {
1810                                     values = normalize(
1811                                         A['values'][i]['value'],
1812                                         B['values'][i]['value'],
1813                                         A['values'][i]['position']
1814                                     );
1815                                 } else {
1816                                     values = normalize(
1817                                         B['values'][i]['value'],
1818                                         A['values'][i]['value'],
1819                                         A['values'][i]['position']
1820                                     );
1821                                 }
1822                                 if (values[0] < values[1] ) {
1823                                     return -1;
1824                                 }
1825                                 if (values[0] > values[1] ) {
1826                                     return 1;
1827                                 }
1828                             }
1829                             return 0; 
1830                         } );
1831                         while(obj.treechildren.lastChild) obj.treechildren.removeChild( obj.treechildren.lastChild );
1832                         for (var i = 0; i < rows.length; i++) {
1833                             obj.treechildren.appendChild( rows[i].node );
1834                         }
1835                         if (typeof obj.on_sort == 'function') obj.on_sort();
1836                     } catch(E) {
1837                         obj.error.standard_unexpected_error_alert('sorting',E); 
1838                     }
1839                     obj.refresh_ordinals();
1840                 }
1841             );
1842         } catch(E) {
1843             obj.error.standard_unexpected_error_alert('pre sorting', E);
1844         }
1845     },
1846
1847     '_toggle_checkbox_column' : function(col,toggle) {
1848         var obj = this;
1849         try {
1850             if (obj.node.getAttribute('no_toggle')) {
1851                 return;
1852             }
1853             var col_pos;
1854             for (var i = 0; i < obj.columns.length; i++) { 
1855                 if (obj.columns[i].id == col.id) col_pos = function(a){return a;}(i); 
1856             }
1857             var treeitems = obj.treechildren.childNodes;
1858             for (var i = 0; i < treeitems.length; i++) {
1859                 var treeitem = treeitems[i];
1860                 var treerow = treeitem.firstChild;
1861                 var treecell = treerow.childNodes[ col_pos ];
1862                 treecell.setAttribute('value',(toggle == 'on'));
1863             }
1864             if (typeof obj.on_checkbox_toggle == 'function') obj.on_checkbox_toggle(toggle);
1865         } catch(E) {
1866             obj.error.standard_unexpected_error_alert('pre toggle', E);
1867         }
1868     },
1869
1870     'render_list_actions' : function(params) {
1871         var obj = this;
1872         switch(this.node.nodeName) {
1873             case 'tree' : return this._render_list_actions_for_tree(params); break;
1874             default: throw('NYI: Need ._render_list_actions() for ' + this.node.nodeName); break;
1875         }
1876     },
1877
1878     '_render_list_actions_for_tree' : function(params) {
1879         var obj = this;
1880         try {
1881             var btn = document.createElement('button');
1882             btn.setAttribute('id',obj.node.id + '_list_actions');
1883             btn.setAttribute('type','menu');
1884             btn.setAttribute('allowevents','true');
1885             //btn.setAttribute('oncommand','this.firstChild.showPopup();');
1886             btn.setAttribute('label',document.getElementById('offlineStrings').getString('list.actions.menu.label'));
1887             btn.setAttribute('accesskey',document.getElementById('offlineStrings').getString('list.actions.menu.accesskey'));
1888             var mp = document.createElement('menupopup');
1889             btn.appendChild(mp);
1890             var mi = document.createElement('menuitem');
1891             mi.setAttribute('id',obj.node.id + '_clipfield');
1892             mi.setAttribute('disabled','true');
1893             mi.setAttribute('label',document.getElementById('offlineStrings').getString('list.actions.field_to_clipboard.label'));
1894             mi.setAttribute('accesskey',document.getElementById('offlineStrings').getString('list.actions.field_to_clipboard.accesskey'));
1895             mp.appendChild(mi);
1896             mi = document.createElement('menuitem');
1897             mi.setAttribute('id',obj.node.id + '_csv_to_clipboard');
1898             mi.setAttribute('label',document.getElementById('offlineStrings').getString('list.actions.csv_to_clipboard.label'));
1899             mi.setAttribute('accesskey',document.getElementById('offlineStrings').getString('list.actions.csv_to_clipboard.accesskey'));
1900             mp.appendChild(mi);
1901             mi = document.createElement('menuitem');
1902             mi.setAttribute('id',obj.node.id + '_csv_to_printer');
1903             mi.setAttribute('label',document.getElementById('offlineStrings').getString('list.actions.csv_to_printer.label'));
1904             mi.setAttribute('accesskey',document.getElementById('offlineStrings').getString('list.actions.csv_to_printer.accesskey'));
1905             mp.appendChild(mi);
1906             mi = document.createElement('menuitem');
1907             mi.setAttribute('id',obj.node.id + '_extended_to_printer');
1908             mi.setAttribute('label',document.getElementById('offlineStrings').getString('list.actions.extended_to_printer.label'));
1909             mi.setAttribute('accesskey',document.getElementById('offlineStrings').getString('list.actions.extended_to_printer.accesskey'));
1910             mp.appendChild(mi);
1911             mi = document.createElement('menuitem');
1912             mi.setAttribute('id',obj.node.id + '_csv_to_file');
1913             mi.setAttribute('label',document.getElementById('offlineStrings').getString('list.actions.csv_to_file.label'));
1914             mi.setAttribute('accesskey',document.getElementById('offlineStrings').getString('list.actions.csv_to_file.accesskey'));
1915             mp.appendChild(mi);
1916             mi = document.createElement('menuitem');
1917             mi.setAttribute('id',obj.node.id + '_save_columns');
1918             mi.setAttribute('label',document.getElementById('offlineStrings').getString('list.actions.save_column_configuration.label'));
1919             mi.setAttribute('accesskey',document.getElementById('offlineStrings').getString('list.actions.save_column_configuration.accesskey'));
1920             if (obj.data.hash.aous['gui.disable_local_save_columns']) {
1921                 mi.setAttribute('disabled','true');
1922             }
1923             mp.appendChild(mi);
1924             return btn;
1925         } catch(E) {
1926             obj.error.standard_unexpected_error_alert('rendering list actions',E);
1927         }
1928     },
1929
1930     'set_list_actions' : function(params) {
1931         var obj = this;
1932         switch(this.node.nodeName) {
1933             case 'tree' : return this._set_list_actions_for_tree(params); break;
1934             default: throw('NYI: Need ._set_list_actions() for ' + this.node.nodeName); break;
1935         }
1936     },
1937
1938     '_set_list_actions_for_tree' : function(params) {
1939         // This should be called after the button element from render_list_actions has been appended to the DOM
1940         var obj = this;
1941         try {
1942             var x = document.getElementById(obj.node.id + '_clipfield');
1943             if (x) {
1944                 obj.event_listeners.add(
1945                     x,
1946                     'command',
1947                     function() {
1948                         obj.clipboard(params);
1949                         if (params && typeof params.on_complete == 'function') {
1950                             params.on_complete(params);
1951                         }
1952                     },
1953                     false
1954                 );
1955             }
1956             x = document.getElementById(obj.node.id + '_csv_to_clipboard');
1957             if (x) {
1958                 obj.event_listeners.add(
1959                     x,
1960                     'command',
1961                     function() {
1962                         obj.dump_csv_to_clipboard(params);
1963                         if (params && typeof params.on_complete == 'function') {
1964                             params.on_complete(params);
1965                         }
1966                     },
1967                     false
1968                 );
1969             }
1970             x = document.getElementById(obj.node.id + '_csv_to_printer');
1971             if (x) {
1972                 obj.event_listeners.add(
1973                     x,
1974                     'command',
1975                     function() {
1976                         obj.dump_csv_to_printer(params);
1977                         if (params && typeof params.on_complete == 'function') {
1978                             params.on_complete(params);
1979                         }
1980                     },
1981                     false
1982                 );
1983             }
1984             x = document.getElementById(obj.node.id + '_extended_to_printer');
1985             if (x) {
1986                 obj.event_listeners.add(
1987                     x,
1988                     'command',
1989                     function() {
1990                         obj.dump_extended_format_to_printer(params);
1991                         if (params && typeof params.on_complete == 'function') {
1992                             params.on_complete(params);
1993                         }
1994                     },
1995                     false
1996                 );
1997             }
1998
1999             x = document.getElementById(obj.node.id + '_csv_to_file');
2000             if (x) {
2001                 obj.event_listeners.add(
2002                     x,
2003                     'command',
2004                     function() {
2005                         obj.dump_csv_to_file(params);
2006                         if (params && typeof params.on_complete == 'function') {
2007                             params.on_complete(params);
2008                         }
2009                     },
2010                     false
2011                 );
2012             }
2013             x = document.getElementById(obj.node.id + '_save_columns');
2014             if (x) {
2015                 obj.event_listeners.add(
2016                     x,
2017                     'command',
2018                     function() {
2019                         obj.save_columns(params);
2020                         if (params && typeof params.on_complete == 'function') {
2021                             params.on_complete(params);
2022                         }
2023                     },
2024                     false
2025                 );
2026             }
2027
2028         } catch(E) {
2029             obj.error.standard_unexpected_error_alert('setting list actions',E);
2030         }
2031     },
2032
2033     // Takes fieldmapper class name and attempts to spit out column definitions suitable for .init
2034     'fm_columns' : function(hint,column_extras,prefix) {
2035         var obj = this;
2036         var columns = [];
2037         if (!prefix) { prefix = ''; }
2038         try {
2039             // requires the dojo library fieldmapper.autoIDL
2040             if (typeof fieldmapper == 'undefined') { throw 'fieldmapper undefined'; }
2041             if (typeof fieldmapper.IDL == 'undefined') { throw 'fieldmapper.IDL undefined'; }
2042             if (typeof fieldmapper.IDL.fmclasses == 'undefined') { throw 'fieldmapper.IDL.fmclasses undefined'; }
2043             if (typeof fieldmapper.IDL.fmclasses[hint] == 'undefined') { throw 'fieldmapper.IDL.fmclasses.' + hint + ' undefined'; }
2044             var my_class = fieldmapper.IDL.fmclasses[hint]; 
2045             var data = obj.data; data.stash_retrieve();
2046
2047             function col_def(my_field) {
2048                 var col_id = prefix + hint + '_' + my_field.name;
2049                 var dataobj = hint;
2050                 var datafield = my_field.name;
2051                 var fleshed_display_field;
2052                 if (column_extras) {
2053                     if (column_extras['*']) {
2054                         if (column_extras['*']['dataobj']) {
2055                             dataobj = column_extras['*']['dataobj'];
2056                         }
2057                     }
2058                     if (column_extras[col_id]) {
2059                         if (column_extras[col_id]['dataobj']) {
2060                             dataobj = column_extras[col_id]['dataobj'];
2061                         }
2062                         if (column_extras[col_id]['datafield']) {
2063                             datafield = column_extras[col_id]['datafield'];
2064                         }
2065                         if (column_extras[col_id]['fleshed_display_field']) {
2066                             fleshed_display_field = column_extras[col_id]['fleshed_display_field'];
2067                         }
2068                     }
2069                 }
2070                 var def = {
2071                     'id' : col_id,
2072                     'label' : my_field.label || my_field.name,
2073                     'sort_type' : [ 'int', 'float', 'id', 'number' ].indexOf(my_field.datatype) > -1 ? 'number' : 
2074                         ( my_field.datatype == 'money' ? 'money' : 
2075                         ( my_field.datatype == 'timestamp' ? 'date' : 'default')),
2076                     'hidden' : my_field.virtual || my_field.datatype == 'link',
2077                     'flex' : 1
2078                 };                    
2079                 // my_field.datatype => bool float id int interval link money number org_unit text timestamp
2080                 if (my_field.datatype == 'link') {
2081                     def.render = function(my) { 
2082                         // is the object fleshed?
2083                         return my[dataobj][datafield]() && typeof my[dataobj][datafield]() == 'object'
2084                             // yes, show the display field
2085                             ? my[dataobj][datafield]()[fleshed_display_field||my_field.key]()
2086                             // no, do we have its class in data.hash?
2087                             : ( typeof data.hash[ my[dataobj].Structure.field_map[datafield].class ] != 'undefined'
2088                                 // yes, do we have this particular object cached?
2089                                 ? ( data.hash[ my[dataobj].Structure.field_map[datafield].class ][ my[dataobj][datafield]() ]
2090                                     // yes, show the display field
2091                                     ? data.hash[ my[dataobj].Structure.field_map[datafield].class ][ my[dataobj][datafield]() ][
2092                                         fleshed_display_field||my_field.key
2093                                     ]()
2094                                     // no, just show the raw value
2095                                     : my[dataobj][datafield]()
2096                                 )
2097                                 // no, just show the raw value
2098                                 : my[dataobj][datafield]()
2099                             ); 
2100                     }
2101                 } else {
2102                     def.render = function(my) { return my[dataobj][datafield](); }
2103                 }
2104                 if (my_field.datatype == 'timestamp') {
2105                     JSAN.use('util.date');
2106                     def.render = function(my) {
2107                         return util.date.formatted_date( my[dataobj][datafield](), '%{localized}' );
2108                     }
2109                     def.sort_value = function(my) {
2110                         return util.date.db_date2Date( my[dataobj][datafield]() ).getTime();
2111                     }
2112                 }
2113                 if (my_field.datatype == 'org_unit') {
2114                     def.render = function(my) {
2115                         return typeof my[dataobj][datafield]() == 'object' ? my[dataobj][datafield]().shortname() : data.hash.aou[ my[dataobj][datafield]() ].shortname();
2116                     }
2117                 }
2118                 if (my_field.datatype == 'money') {
2119                     JSAN.use('util.money');
2120                     def.render = function(my) {
2121                         return util.money.sanitize( my[dataobj][datafield]() );
2122                     }
2123                     def.sort_value = function(my) {
2124                         return util.money.dollars_float_to_cents_integer( my[dataobj][datafield]() );
2125                     }
2126                 }
2127                 if (column_extras) {
2128                     if (column_extras['*']) {
2129                         for (var attr in column_extras['*']) {
2130                             def[attr] = column_extras['*'][attr];
2131                         }
2132                         if (column_extras['*']['expanded_label']) {
2133                             def.label = my_class.label + ': ' + def.label;
2134                         }
2135                         if (column_extras['*']['label_prefix']) {
2136                             def.label = column_extras['*']['label_prefix'] + def.label;
2137                         }
2138                         if (column_extras['*']['remove_virtual']) {
2139                             if (my_field.virtual) {
2140                                 def.remove_me = true;
2141                             }
2142                         }
2143                     }
2144                     if (column_extras[col_id]) {
2145                         for (var attr in column_extras[col_id]) {
2146                             def[attr] = column_extras[col_id][attr];
2147                         }
2148                         if (column_extras[col_id]['keep_me']) {
2149                             def.remove_me = false;
2150                         }
2151                         if (column_extras[col_id]['label_prefix']) {
2152                             def.label = column_extras[col_id]['label_prefix'] + def.label;
2153                         }
2154                     }
2155                 }
2156                 if (def.remove_me) {
2157                     dump('Skipping ' + def.label + '\n');
2158                     return null;
2159                 } else {
2160                     dump('Defining ' + def.label + '\n');
2161                     return def;
2162                 }
2163             }
2164  
2165             for (var i = 0; i < my_class.fields.length; i++) {
2166                 var my_field = my_class.fields[i];
2167                 var def = col_def(my_field);
2168                 if (def) {
2169                     columns.push( def );
2170                 }
2171             }
2172
2173         } catch(E) {
2174             obj.error.standard_unexpected_error_alert('fm_columns()',E);
2175         }
2176         return columns;
2177     },
2178     // Default for the map_row_to_columns function for .init
2179     'std_map_row_to_columns' : function(error_value) {
2180         return function(row,cols,scratch) {
2181             // row contains { 'my' : { 'acp' : {}, 'circ' : {}, 'mvr' : {} } }
2182             // cols contains all of the objects listed above in columns
2183             // scratch is a temporary space shared by all cells/rows (or just per row if not explicitly passed in)
2184             if (!scratch) { scratch = {}; }
2185
2186             var obj = {};
2187             JSAN.use('util.error'); obj.error = new util.error();
2188             JSAN.use('OpenILS.data'); obj.data = new OpenILS.data(); obj.data.init({'via':'stash'});
2189             JSAN.use('util.network'); obj.network = new util.network();
2190             JSAN.use('util.money');
2191
2192             // FIXME: backwards compatability with server/patron code and the old patron.util.std_map_row_to_columns.
2193             // Will remove in a separate commit and change all instances of obj.OpenILS.data to obj.data at the same time.
2194             obj.OpenILS = { 'data' : obj.data };
2195
2196             var my = row.my;
2197             var values = [];
2198             var sort_values = [];
2199             var cmd = '';
2200             try {
2201                 for (var i = 0; i < cols.length; i++) {
2202                     switch (typeof cols[i].render) {
2203                         case 'function': try { values[i] = cols[i].render(my,scratch); } catch(E) { values[i] = error_value; obj.error.sdump('D_COLUMN_RENDER_ERROR',E); } break;
2204                         case 'string' : cmd += 'try { ' + cols[i].render + '; values['+i+'] = v; } catch(E) { values['+i+'] = error_value; }'; break;
2205                         default: cmd += 'values['+i+'] = "??? '+(typeof cols[i].render)+'"; ';
2206                     }
2207                     switch (typeof cols[i].sort_value) {
2208                         case 'function':
2209                             try {
2210                                 sort_values[i] = cols[i].sort_value(my,scratch);
2211                             } catch(E) {
2212                                 sort_values[i] = error_value;
2213                                 obj.error.sdump('D_COLUMN_RENDER_ERROR',E);
2214                             }
2215                         break;
2216                         case 'string' :
2217                             sort_values[i] = JSON2js(cols[i].sort_value);
2218                         break;
2219                         default:
2220                             cmd += 'sort_values['+i+'] = values[' + i + '];';
2221                     }
2222                 }
2223                 if (cmd) eval( cmd );
2224             } catch(E) {
2225                 obj.error.sdump('D_WARN','map_row_to_column: ' + E);
2226                 if (error_value) { value = error_value; } else { value = '   ' };
2227             }
2228             return { 'values' : values, 'sort_values' : sort_values };
2229         }
2230     }
2231 }
2232 dump('exiting util.list.js\n');