Fixed bug in staff client offline mode.
[working/Evergreen.git] / Open-ILS / xul / staff_client / chrome / content / util / list.js
index fb46564..2d4c33a 100644 (file)
@@ -9,6 +9,10 @@ util.list = function (id) {
 
     this.unique_row_counter = 0;
 
+    this.sub_sorts = [];
+
+    this.count_for_display = 0;
+
     if (!this.node) throw('Could not find element ' + id);
     switch(this.node.nodeName) {
         case 'listbox' : 
@@ -22,6 +26,9 @@ util.list = function (id) {
 
     JSAN.use('OpenILS.data'); this.data = new OpenILS.data(); this.data.stash_retrieve();
 
+    JSAN.use('util.functional');
+    JSAN.use('util.widgets');
+
     return this;
 };
 
@@ -31,6 +38,10 @@ util.list.prototype = {
 
         var obj = this;
         obj.scratch_data = {};
+        obj.event_listeners = new EventListenerList();
+
+        // If set, save and restore columns as if the tree/list id was the value of columns_saved_under
+        obj.columns_saved_under = params.columns_saved_under;
 
         JSAN.use('util.widgets');
 
@@ -48,7 +59,23 @@ util.list.prototype = {
         if (typeof params.prebuilt != 'undefined') obj.prebuilt = params.prebuilt;
 
         if (typeof params.columns == 'undefined') throw('util.list.init: No columns');
-        obj.columns = [];
+        obj.columns = [
+            {
+                'id' : 'lineno',
+                'label' : document.getElementById('offlineStrings').getString('list.line_number'),
+                'flex' : '0',
+                'no_sort' : 'true',
+                'properties' : 'ordinal', // column properties for css styling
+                'hidden' : 'false',
+                'editable' : false,
+                'render' : function(my,scratch) {
+                    // special code will handle this based on the attribute we set
+                    // here.  All cells for this column need to be updated whenever
+                    // a list adds, removes, or sorts rows
+                    return '_';
+                }
+            }
+        ];
         for (var i = 0; i < params.columns.length; i++) {
             if (typeof params.columns[i] == 'object') {
                 obj.columns.push( params.columns[i] );
@@ -77,6 +104,9 @@ util.list.prototype = {
             var treecols = document.createElement('treecols');
             this.node.appendChild(treecols);
             this.treecols = treecols;
+            if (document.getElementById('column_sort_menu')) {
+                treecols.setAttribute('context','column_sort_menu');
+            }
 
             var check_for_id_collisions = {};
             for (var i = 0; i < this.columns.length; i++) {
@@ -99,8 +129,10 @@ util.list.prototype = {
                     treecol.setAttribute(j,value);
                 }
                 treecols.appendChild(treecol);
+
                 if (this.columns[i].type == 'checkbox') {
-                    treecol.addEventListener(
+                    obj.event_listeners.add(
+                        treecol,
                         'click',
                         function(ev) {
                             setTimeout(
@@ -115,25 +147,131 @@ util.list.prototype = {
                         false
                     );
                 } else {
-                    treecol.addEventListener(
+                    obj.event_listeners.add(
+                        treecol,
+                        'sort_first_asc',
+                        function(ev) {
+                            dump('sort_first_asc\n');
+                            ev.target.setAttribute('sortDir','asc');
+                            obj.first_sort = {
+                                'target' : ev.target,
+                                'sortDir' : 'asc'
+                            };
+                            obj.sub_sorts = [];
+                            util.widgets.dispatch('sort',ev.target);
+                        },
+                        false
+                    );
+                    obj.event_listeners.add(
+                        treecol,
+                        'sort_first_desc',
+                        function(ev) {
+                            dump('sort_first_desc\n');
+                            ev.target.setAttribute('sortDir','desc');
+                            obj.first_sort = {
+                                'target' : ev.target,
+                                'sortDir' : 'desc'
+                            };
+                            obj.sub_sorts = [];
+                            util.widgets.dispatch('sort',ev.target);
+                        },
+                        false
+                    );
+                    obj.event_listeners.add(
+                        treecol,
+                        'sort_next_asc',
+                        function(ev) {
+                            dump('sort_next_asc\n');
+                            ev.target.setAttribute('sortDir','asc');
+                            obj.sub_sorts.push({
+                                'target' : ev.target,
+                                'sortDir' : 'asc'
+                            });
+                            util.widgets.dispatch('sort',ev.target);
+                        },
+                        false
+                    );
+                    obj.event_listeners.add(
+                        treecol,
+                        'sort_next_desc',
+                        function(ev) {
+                            dump('sort_next_desc\n');
+                            ev.target.setAttribute('sortDir','desc');
+                            obj.sub_sorts.push({
+                                'target' : ev.target,
+                                'sortDir' : 'desc'
+                            });
+                            util.widgets.dispatch('sort',ev.target);
+                        },
+                        false
+                    );
+
+                    obj.event_listeners.add(
+                        treecol,
                         'click', 
                         function(ev) {
-                            function do_it() {
+                            if (ev.button == 2 /* context menu click */ || ev.target.getAttribute('no_sort')) {
+                                return;
+                            }
+
+                            if (ev.ctrlKey) { // sub sort
+                                var sortDir = 'asc';
+                                if (ev.shiftKey) {
+                                    sortDir = 'desc';
+                                }
+                                ev.target.setAttribute('sortDir',sortDir);
+                                obj.sub_sorts.push({
+                                    'target' : ev.target,
+                                    'sortDir' : sortDir
+                                });
+                            } else { // first sort
                                 var sortDir = ev.target.getAttribute('sortDir') || 'desc';
                                 if (sortDir == 'desc') sortDir = 'asc'; else sortDir = 'desc';
+                                if (ev.shiftKey) {
+                                    sortDir = 'desc';
+                                }
                                 ev.target.setAttribute('sortDir',sortDir);
-                                obj._sort_tree(ev.target,sortDir);
+                                obj.first_sort = {
+                                    'target' : ev.target,
+                                    'sortDir' : sortDir
+                                };
+                                obj.sub_sorts = [];
+                            }
+                            util.widgets.dispatch('sort',ev.target);
+                        },
+                        false
+                    );
+
+                    obj.event_listeners.add(
+                        treecol,
+                        'sort',
+                        function(ev) {
+                            if (!obj.first_sort) {
+                                return;
+                            }
+
+                            function do_it() {
+                                obj._sort_tree();
                             }
 
-                            if (obj.row_count.total != obj.row_count.fleshed && (obj.row_count.total - obj.row_count.fleshed) > 50) {
-                                var r = window.confirm(document.getElementById('offlineStrings').getFormattedString('list.row_fetch_warning',[obj.row_count.fleshed,obj.row_count.total]));
+                            if (obj.row_count.total != obj.row_count.fleshed
+                                && (obj.row_count.total - obj.row_count.fleshed) > 50
+                            ) {
+                                var r = window.confirm(
+                                    document.getElementById('offlineStrings').getFormattedString(
+                                        'list.row_fetch_warning',
+                                        [obj.row_count.fleshed,obj.row_count.total]
+                                    )
+                                );
 
                                 if (r) {
                                     setTimeout( do_it, 0 );
                                 }
+
                             } else {
                                     setTimeout( do_it, 0 );
                             }
+
                         },
                         false
                     );
@@ -153,7 +291,8 @@ util.list.prototype = {
         if (typeof params.on_checkbox_toggle == 'function') {
             this.on_checkbox_toggle = params.on_checkbox_toggle;
         }
-        this.node.addEventListener(
+        obj.event_listeners.add(
+            this.node,
             'select',
             function(ev) {
                 if (typeof params.on_select == 'function') {
@@ -168,30 +307,44 @@ util.list.prototype = {
             false
         );
         if (typeof params.on_click == 'function') {
-            this.node.addEventListener(
+            obj.event_listeners.add(
+                this.node,
                 'click',
                 params.on_click,
                 false
             );
         }
+        if (typeof params.on_dblclick == 'function') {
+            obj.event_listeners.add(
+                this.node,
+                'dblclick',
+                params.on_dblclick,
+                false
+            );
+        }
+
         /*
-        this.node.addEventListener(
+        obj.event_listeners.add(
+            this.node,
             'mousemove',
             function(ev) { obj.detect_visible(); },
             false
         );
         */
-        this.node.addEventListener(
+        obj.event_listeners.add(
+            this.node,
             'keypress',
             function(ev) { obj.auto_retrieve(); },
             false
         );
-        this.node.addEventListener(
+        obj.event_listeners.add(
+            this.node,
             'click',
             function(ev) { obj.auto_retrieve(); },
             false
         );
-        window.addEventListener(
+        obj.event_listeners.add(
+            window,
             'resize',
             function(ev) { obj.auto_retrieve(); },
             false
@@ -209,7 +362,7 @@ util.list.prototype = {
         slider.addEventListener('command',function(){alert('slider command');},false);
         slider.addEventListener('scroll',function(){alert('slider scroll');},false);
         */
-        this.node.addEventListener('scroll',function(){ obj.auto_retrieve(); },false);
+        obj.event_listeners.add(this.node, 'scroll',function(){ obj.auto_retrieve(); },false);
 
         this.restores_columns(params);
     },
@@ -236,6 +389,11 @@ util.list.prototype = {
         }
     },
 
+    'cleanup' : function () {
+        var obj = this;
+        obj.event_listeners.removeAll();
+    },
+
     'save_columns' : function (params) {
         var obj = this;
         if (obj.data.hash.aous['gui.disable_local_save_columns']) {
@@ -251,7 +409,9 @@ util.list.prototype = {
     '_save_columns_tree' : function (params) {
         var obj = this;
         try {
-            var id = obj.node.getAttribute('id'); if (!id) {
+            var id = obj.node.getAttribute('id');
+            if (obj.columns_saved_under) { id = obj.columns_saved_under; }
+            if (!id) {
                 alert("FIXME: The columns for this list cannot be saved because the list has no id.");
                 return;
             }
@@ -269,7 +429,6 @@ util.list.prototype = {
                 var col_ordinal = col.getAttribute('ordinal'); 
                 my_cols[ col_id ] = { 'hidden' : col_hidden, 'width' : col_width, 'ordinal' : col_ordinal };
             }
-            netscape.security.PrivilegeManager.enablePrivilege('UniversalXPConnect');
             JSAN.use('util.file'); var file = new util.file('tree_columns_for_'+window.escape(id));
             file.set_object(my_cols);
             file.close();
@@ -290,7 +449,9 @@ util.list.prototype = {
     '_restores_columns_tree' : function (params) {
         var obj = this;
         try {
-            var id = obj.node.getAttribute('id'); if (!id) {
+            var id = obj.node.getAttribute('id');
+            if (obj.columns_saved_under) { id = obj.columns_saved_under; }
+            if (!id) {
                 alert("FIXME: The columns for this list cannot be restored because the list has no id.");
                 return;
             }
@@ -298,7 +459,6 @@ util.list.prototype = {
             var my_cols;
             if (! obj.data.hash.aous) { obj.data.hash.aous = {}; }
             if (! obj.data.hash.aous['gui.disable_local_save_columns']) {
-                netscape.security.PrivilegeManager.enablePrivilege('UniversalXPConnect');
                 JSAN.use('util.file'); var file = new util.file('tree_columns_for_'+window.escape(id));
                 if (file._file.exists()) {
                     my_cols = file.get_object(); file.close();
@@ -392,14 +552,14 @@ util.list.prototype = {
         }
         if (rparams && params.attributes) {
             for (var i in params.attributes) {
-                rparams.my_node.setAttribute(i,params.attributes[i]);
+                rparams.treeitem_node.setAttribute(i,params.attributes[i]);
             }
         }
         this.row_count.total++;
         if (this.row_count.fleshed == this.row_count.total) {
             setTimeout( function() { obj.exec_on_all_fleshed(); }, 0 );
         }
-        rparams.my_node.setAttribute('unique_row_counter',obj.unique_row_counter);
+        rparams.treeitem_node.setAttribute('unique_row_counter',obj.unique_row_counter);
         rparams.unique_row_counter = obj.unique_row_counter++;
         if (typeof params.on_append == 'function') {
             params.on_append(rparams);
@@ -416,7 +576,7 @@ util.list.prototype = {
         }
         if (rparams && params.attributes) {
             for (var i in params.attributes) {
-                rparams.my_node.setAttribute(i,params.attributes[i]);
+                rparams.treeitem_node.setAttribute(i,params.attributes[i]);
             }
         }
         this.row_count.fleshed--;
@@ -490,7 +650,8 @@ util.list.prototype = {
         obj.put_retrieving_label(treerow);
 
         if (typeof params.retrieve_row == 'function' || typeof this.retrieve_row == 'function') {
-            treerow.addEventListener(
+            obj.event_listeners.add(
+                treerow,
                 'flesh',
                 function() {
 
@@ -509,13 +670,13 @@ util.list.prototype = {
                         }
                     }
 
-                    params.row_node = treeitem;
+                    params.treeitem_node = treeitem;
                     params.on_retrieve = function(p) {
                         try {
                             p.row = params.row;
                             obj._map_row_to_treecell(p,treerow);
                             inc_fleshed();
-                            var idx = obj.node.contentView.getIndexOfItem( params.row_node );
+                            var idx = obj.node.contentView.getIndexOfItem( params.treeitem_node );
                             dump('idx = ' + idx + '\n');
                             // if current row is selected, send another select event to re-sync data that the client code fetches on selects
                             if ( obj.node.view.selection.isSelected( idx ) ) {
@@ -540,6 +701,7 @@ util.list.prototype = {
                     
                             inc_fleshed();
                     }
+                    obj.refresh_ordinals();
                 },
                 false
             );
@@ -553,7 +715,8 @@ util.list.prototype = {
                 }
             }
         } else {
-            treerow.addEventListener(
+            obj.event_listeners.add(
+                treerow,
                 'flesh',
                 function() {
                     //dump('fleshing anon\n');
@@ -565,6 +728,7 @@ util.list.prototype = {
                     if (obj.row_count.fleshed >= obj.row_count.total) {
                         setTimeout( function() { obj.exec_on_all_fleshed(); }, 0 );
                     }
+                    obj.refresh_ordinals();
                 },
                 false
             );
@@ -596,9 +760,9 @@ util.list.prototype = {
             } catch(E) {
             }
 
-        setTimeout( function() { obj.auto_retrieve(); }, 0 );
+        setTimeout( function() { obj.auto_retrieve(); obj.refresh_ordinals(); }, 0 );
 
-        params.my_node = treeitem;
+        params.treeitem_node = treeitem;
         return params;
     },
 
@@ -607,12 +771,12 @@ util.list.prototype = {
         var obj = this;
 
         if (typeof params.row == 'undefined') throw('util.list.refresh_row: Object must contain a row');
-        if (typeof params.my_node == 'undefined') throw('util.list.refresh_row: Object must contain a my_node');
-        if (params.my_node.nodeName != 'treeitem') throw('util.list.refresh_rwo: my_node must be a treeitem');
+        if (typeof params.treeitem_node == 'undefined') throw('util.list.refresh_row: Object must contain a treeitem_node');
+        if (params.treeitem_node.nodeName != 'treeitem') throw('util.list.refresh_rwo: treeitem_node must be a treeitem');
 
         var s = ('util.list.refresh_row: params = ' + (params) + '\n');
 
-        var treeitem = params.my_node;
+        var treeitem = params.treeitem_node;
         treeitem.setAttribute('retrieve_id',params.retrieve_id);
         if (typeof params.to_bottom != 'undefined') {
             if (typeof params.no_auto_select == 'undefined') {
@@ -650,7 +814,8 @@ util.list.prototype = {
 
             s += 'found a retrieve_row function\n';
 
-            treerow.addEventListener(
+            obj.event_listeners.add(
+                treerow,
                 'flesh',
                 function() {
 
@@ -669,13 +834,13 @@ util.list.prototype = {
                         }
                     }
 
-                    params.row_node = treeitem;
+                    params.treeitem_node = treeitem;
                     params.on_retrieve = function(p) {
                         try {
                             p.row = params.row;
                             obj._map_row_to_treecell(p,treerow);
                             inc_fleshed();
-                            var idx = obj.node.contentView.getIndexOfItem( params.row_node );
+                            var idx = obj.node.contentView.getIndexOfItem( params.treeitem_node );
                             dump('idx = ' + idx + '\n');
                             // if current row is selected, send another select event to re-sync data that the client code fetches on selects
                             if ( obj.node.view.selection.isSelected( idx ) ) {
@@ -700,6 +865,7 @@ util.list.prototype = {
                     
                             inc_fleshed();
                     }
+                    obj.refresh_ordinals();
                 },
                 false
             );
@@ -717,7 +883,8 @@ util.list.prototype = {
 
             s += 'did not find a retrieve_row function\n';
 
-            treerow.addEventListener(
+            obj.event_listeners.add(
+                treerow,
                 'flesh',
                 function() {
                     //dump('fleshing anon\n');
@@ -729,6 +896,7 @@ util.list.prototype = {
                     if (obj.row_count.fleshed >= obj.row_count.total) {
                         setTimeout( function() { obj.exec_on_all_fleshed(); }, 0 );
                     }
+                    obj.refresh_ordinals();
                 },
                 false
             );
@@ -758,7 +926,7 @@ util.list.prototype = {
             } catch(E) {
             }
 
-        setTimeout( function() { obj.auto_retrieve(); }, 0 );
+        setTimeout( function() { obj.auto_retrieve(); obj.refresh_ordinals(); }, 0 );
 
         JSAN.use('util.widgets'); util.widgets.dispatch('select',obj.node);
 
@@ -767,6 +935,49 @@ util.list.prototype = {
         return params;
     },
 
+    'refresh_ordinals' : function() {
+        var obj = this;
+        try {
+            if (obj.refresh_ordinals_timeout_id) { return; }
+
+            function _refresh_ordinals(clear) {
+                var nl = obj.node.getElementsByAttribute('label','_');
+                for (var i = 0; i < nl.length; i++) {
+                    nl[i].setAttribute(
+                        'ord_col',
+                        'true'
+                    );
+                    nl[i].setAttribute( // treecell properties for css styling
+                        'properties',
+                        'ordinal'
+                    );
+                }
+                nl = obj.node.getElementsByAttribute('ord_col','true');
+                for (var i = 0; i < nl.length; i++) {
+                    nl[i].setAttribute(
+                        'label',
+                        // we could just use 'i' here if we trust the order of elements
+                        1 + obj.node.contentView.getIndexOfItem(nl[i].parentNode.parentNode) // treeitem
+                    );
+                }
+                if (clear) { obj.refresh_ordinals_timeout_id = null; }
+            }
+
+            // spamming this to cover race conditions
+            setTimeout(_refresh_ordinals, 500); // for speedy looking UI updates
+            setTimeout(_refresh_ordinals, 2000); // for most uses
+            obj.refresh_ordinals_timeout_id = setTimeout(
+                function() {
+                    _refresh_ordinals(true);
+                },
+                4000 // just in case, say with a slow rendering list
+            );
+
+        } catch(E) {
+            alert('Error in list.js, refresh_ordinals(): ' + E);
+        }
+    },
+
     'put_retrieving_label' : function(treerow) {
         var obj = this;
         try {
@@ -888,7 +1099,7 @@ util.list.prototype = {
                                 dump('exec_on_all_fleshed, processing on_all_fleshed array, length = ' + obj.on_all_fleshed.length + '\n');
                                 var f = obj.on_all_fleshed.pop();
                                 if (typeof f == 'function') { 
-                                    try { f(); } catch(E) { obj.error.standard_unexpected_error_alert('_full_retrieve_tree callback',f); } 
+                                    try { f(); } catch(E) { obj.error.standard_unexpected_error_alert('_full_retrieve_tree callback',E); }
                                 }
                                 if (obj.on_all_fleshed.length > 0) arguments.callee(); 
                             } catch(E) {
@@ -911,6 +1122,7 @@ util.list.prototype = {
             case 'tree' : obj._full_retrieve_tree(params); break;
             default: throw('NYI: Need .full_retrieve() for ' + obj.node.nodeName); break;
         }
+        obj.refresh_ordinals();
     },
 
     '_full_retrieve_tree' : function(params) {
@@ -952,7 +1164,7 @@ util.list.prototype = {
                     //FIXME//Make async and fire when row is visible in list
                     var row;
 
-                    params.row_node = listitem;
+                    params.treeitem_node = listitem;
                     params.on_retrieve = function(row) {
                         params.row = row;
                         obj._map_row_to_listcell(params,listitem);
@@ -980,7 +1192,7 @@ util.list.prototype = {
         }
 
         this.error.sdump('D_LIST',s);
-        params.my_node = listitem;
+        params.treeitem_node = listitem;
         return params;
 
     },
@@ -1002,6 +1214,7 @@ util.list.prototype = {
                 
                 if ( this.columns[i].editable == false ) { treecell.setAttribute('editable','false'); }
                 var label = '';
+                var sort_value = '';
 
                 // What skip columns is doing is rendering the treecells as blank/empty
                 if (params.skip_columns && (params.skip_columns.indexOf(i) != -1)) {
@@ -1016,13 +1229,27 @@ util.list.prototype = {
                 }
     
                 if (typeof params.map_row_to_column == 'function')  {
+                   if (this.columns[i].id == 'lineno'){
+
+                     label = this.count_for_display.toString();
+                     this.count_for_display++;
+
+                   } else {
     
-                    label = params.map_row_to_column(params.row,this.columns[i],this.scratch_data);
-    
+                     label = params.map_row_to_column(params.row,this.columns[i],this.scratch_data);
+
+                   }
                 } else if (typeof this.map_row_to_column == 'function') {
+                   if (this.columns[i].id == 'lineno'){
+
+                     label = this.count_for_display.toString();
+                     this.count_for_display++;
+
+                   } else {
+
+                     label = this.map_row_to_column(params.row,this.columns[i],this.scratch_data);
     
-                    label = this.map_row_to_column(params.row,this.columns[i],this.scratch_data);
-    
+                   }
                 }
                 if (this.columns[i].type == 'checkbox') { treecell.setAttribute('value',label); } else { treecell.setAttribute('label',label ? label : ''); }
                 s += ('treecell = ' + treecell + ' with label = ' + label + '\n');
@@ -1030,15 +1257,27 @@ util.list.prototype = {
         } else if (typeof params.map_row_to_columns == 'function' || typeof this.map_row_to_columns == 'function') {
 
             var labels = [];
+            var sort_values = [];
 
             if (typeof params.map_row_to_columns == 'function') {
 
-                labels = params.map_row_to_columns(params.row,this.columns,this.scratch_data);
+                var values = params.map_row_to_columns(params.row,this.columns,this.scratch_data);
+                if (typeof values.values == 'undefined') {
+                    labels = values;
+                } else {
+                    labels = values.values;
+                    sort_values = values.sort_values;
+                }
 
             } else if (typeof this.map_row_to_columns == 'function') {
 
-                labels = this.map_row_to_columns(params.row,this.columns,this.scratch_data);
-
+                var values = this.map_row_to_columns(params.row,this.columns,this.scratch_data);
+                if (typeof values.values == 'undefined') {
+                    labels = values;
+                } else {
+                    labels = values.values;
+                    sort_values = values.sort_values;
+                }
             }
             for (var i = 0; i < labels.length; i++) {
                 var treecell;
@@ -1054,6 +1293,9 @@ util.list.prototype = {
                 } else {
                     treecell.setAttribute('label',typeof labels[i] == 'string' || typeof labels[i] == 'number' ? labels[i] : '');
                 }
+                if (sort_values[i]) {
+                    treecell.setAttribute('sort_value',js2JSON(sort_values[i]));
+                }
                 s += ('treecell = ' + treecell + ' with label = ' + labels[i] + '\n');
             }
 
@@ -1165,22 +1407,35 @@ util.list.prototype = {
     '_dump_tree_with_keys' : function(params) {
         var obj = this;
         var dump = [];
-        for (var i = 0; i < this.treechildren.childNodes.length; i++) {
-            var row = {};
-            var treeitem = this.treechildren.childNodes[i];
-            var treerow = treeitem.firstChild;
-            for (var j = 0; j < treerow.childNodes.length; j++) {
-                if (typeof obj.columns[j] == 'undefined') {
-                    dump('=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=\n');
-                    dump('_dump_tree_with_keys @ ' + location.href + '\n');
-                    dump('\ttreerow.childNodes.length='+treerow.childNodes.length+' j='+j+' obj.columns.length='+obj.columns.length+'\n');
-                    debugger;
-                } else {
-                    row[ obj.columns[j].id ] = treerow.childNodes[j].getAttribute('label');
+
+        function process_tree(treechildren) {
+            for (var i = 0; i < treechildren.childNodes.length; i++) {
+                var row = {};
+                var treeitem = treechildren.childNodes[i];
+                var treerow = treeitem.firstChild;
+                for (var j = 0; j < treerow.childNodes.length; j++) {
+                    if (typeof obj.columns[j] == 'undefined') {
+                        dump('=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=\n');
+                        dump('_dump_tree_with_keys @ ' + location.href + '\n');
+                        dump('\ttreerow.childNodes.length='+treerow.childNodes.length+' j='+j+' obj.columns.length='+obj.columns.length+'\n');
+                        debugger;
+                    } else {
+                        row[ obj.columns[j].id ] = treerow.childNodes[j].getAttribute('label');
+                        var sort = treerow.childNodes[j].getAttribute('sort_value');
+                        if(sort) {
+                            row[ obj.columns[j].id + '_sort_value' ] = sort;
+                        }
+                    }
+                }
+                dump.push( row );
+                if (treeitem.childNodes.length > 1) {
+                    process_tree(treeitem.lastChild);
                 }
             }
-            dump.push( row );
         }
+
+        process_tree(this.treechildren);
+
         return dump;
     },
 
@@ -1214,16 +1469,25 @@ util.list.prototype = {
             _dump += '"' + obj.columns[ ord_cols[j][1] ].label.replace(/"/g, '""') + '"';
         }
         _dump += '\r\n';
-        for (var i = 0; i < this.treechildren.childNodes.length; i++) {
-            var row = '';
-            var treeitem = this.treechildren.childNodes[i];
-            var treerow = treeitem.firstChild;
-            for (var j = 0; j < ord_cols.length; j++) {
-                if (row) row += ',';
-                row += '"' + treerow.childNodes[ ord_cols[j][1] ].getAttribute('label').replace(/"/g, '""') + '"';
+
+        function process_tree(treechildren) {
+            for (var i = 0; i < treechildren.childNodes.length; i++) {
+                var row = '';
+                var treeitem = treechildren.childNodes[i];
+                var treerow = treeitem.firstChild;
+                for (var j = 0; j < ord_cols.length; j++) {
+                    if (row) row += ',';
+                    row += '"' + treerow.childNodes[ ord_cols[j][1] ].getAttribute('label').replace(/"/g, '""') + '"';
+                }
+                _dump +=  row + '\r\n';
+                if (treeitem.childNodes.length > 1) {
+                    process_tree(treeitem.lastChild);
+                }
             }
-            _dump +=  row + '\r\n';
         }
+
+        process_tree(this.treechildren);
+
         return _dump;
     },
 
@@ -1252,15 +1516,24 @@ util.list.prototype = {
             if ( Number( a[0] ) > Number( b[0] ) ) return 1; 
             return 0;
         } );
-        for (var i = 0; i < this.treechildren.childNodes.length; i++) {
-            var row = document.getElementById('offlineStrings').getString('list.dump_extended_format.record_separator') + '\r\n';
-            var treeitem = this.treechildren.childNodes[i];
-            var treerow = treeitem.firstChild;
-            for (var j = 0; j < ord_cols.length; j++) {
-                row += obj.columns[ ord_cols[j][1] ].label + ': ' + treerow.childNodes[ ord_cols[j][1] ].getAttribute('label') + '\r\n';
+
+        function process_tree(treechildren) {
+            for (var i = 0; i < treechildren.childNodes.length; i++) {
+                var row = document.getElementById('offlineStrings').getString('list.dump_extended_format.record_separator') + '\r\n';
+                var treeitem = treechildren.childNodes[i];
+                var treerow = treeitem.firstChild;
+                for (var j = 0; j < ord_cols.length; j++) {
+                    row += obj.columns[ ord_cols[j][1] ].label + ': ' + treerow.childNodes[ ord_cols[j][1] ].getAttribute('label') + '\r\n';
+                }
+                _dump +=  row + '\r\n';
+                if (treeitem.childNodes.length > 1) {
+                    process_tree(treeitem.lastChild);
+                }
             }
-            _dump +=  row + '\r\n';
         }
+
+        process_tree(this.treechildren);
+
         return _dump;
     },
 
@@ -1276,8 +1549,8 @@ util.list.prototype = {
 
     'dump_csv_to_printer' : function(params) {
         var obj = this;
-        JSAN.use('util.print'); var print = new util.print(params.printer_context || obj.printer_context);
         if (typeof params == 'undefined') params = {};
+        JSAN.use('util.print'); var print = new util.print(params.printer_context || obj.printer_context);
         if (params.no_full_retrieve) {
             print.simple( obj.dump_csv( params ), {'content_type':'text/plain'} );
         } else {
@@ -1291,8 +1564,8 @@ util.list.prototype = {
 
     'dump_extended_format_to_printer' : function(params) {
         var obj = this;
-        JSAN.use('util.print'); var print = new util.print(params.printer_context || obj.printer_context);
         if (typeof params == 'undefined') params = {};
+        JSAN.use('util.print'); var print = new util.print(params.printer_context || obj.printer_context);
         if (params.no_full_retrieve) {
             print.simple( obj.dump_extended_format( params ), {'content_type':'text/plain'} );
         } else {
@@ -1341,11 +1614,15 @@ util.list.prototype = {
                 params.staff = data.list.au[0];
             }
             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() ]) {
-                params.lib = data.hash.aou[ data.list.au[0].ws_ou() ];
-                params.lib.children(null);
+                params.lib = JSON2js( js2JSON( data.hash.aou[ data.list.au[0].ws_ou() ] ) ); // clone this sucker
+                params.lib.children(null); // since we're modifying it
             }
             if (params.template && data.print_list_templates[ params.template ]) {
                 var template = data.print_list_templates[ params.template ];
+                if (template.inherit) {
+                    template = data.print_list_templates[ template.inherit ];
+                    // if someone wants to implement recursion later, feel free
+                }
                 for (var i in template) params[i] = template[i];
             }
             obj.wrap_in_full_retrieve(
@@ -1443,15 +1720,27 @@ util.list.prototype = {
         obj.full_retrieve();
     },
 
-    '_sort_tree' : function(col,sortDir) {
+    '_sort_tree' : function() {
         var obj = this;
         try {
             if (obj.node.getAttribute('no_sort')) {
                 return;
             }
-            var col_pos;
-            for (var i = 0; i < obj.columns.length; i++) { 
-                if (obj.columns[i].id == col.id) col_pos = function(a){return a;}(i); 
+
+            var sorts = [ obj.first_sort ].concat( obj.sub_sorts );
+            var columns = util.functional.map_list(
+                sorts,
+                function(e,idx) {
+                    return e.target;
+                }
+            );
+            var column_positions = [];
+            for (var i = 0; i < columns.length; i++) {
+                for (var j = 0; j < obj.columns.length; j++) {
+                    if (obj.columns[j].id == columns[i].id) {
+                        column_positions.push( function(a){return a;}(j) );
+                    }
+                }
             }
             obj.wrap_in_full_retrieve(
                 function() {
@@ -1462,47 +1751,99 @@ util.list.prototype = {
                         for (var i = 0; i < treeitems.length; i++) {
                             var treeitem = treeitems[i];
                             var treerow = treeitem.firstChild;
-                            var treecell = treerow.childNodes[ col_pos ];
-                            value = ( { 'value' : treecell ? treecell.getAttribute('label') : '', 'node' : treeitem } );
-                            rows.push( value );
+
+                            function get_value(treecell) {
+                                value = ( {
+                                    'value' : treecell
+                                        ? treecell.getAttribute('label')
+                                        : '',
+                                    'sort_value' : treecell ? treecell.hasAttribute('sort_value')
+                                        ? JSON2js(
+                                            treecell.getAttribute('sort_value'))
+                                        : '' : ''
+                                } );
+                                return value;
+                            }
+
+                            var values = [];
+                            for (var j = 0; j < column_positions.length; j++) {
+                                var treecell = treerow.childNodes[ column_positions[j] ];
+                                values.push({
+                                    'position' : column_positions[j],
+                                    'value' : get_value(treecell)
+                                });
+                            }
+
+                            rows.push({
+                                'values' : values,
+                                'node' : treeitem
+                            });
                         }
-                        rows = rows.sort( function(a,b) { 
-                            a = a.value; b = b.value; 
-                            if (col.getAttribute('sort_type')) {
-                                switch(col.getAttribute('sort_type')) {
-                                    case 'date' :
-                                        JSAN.use('util.date'); // to pull in dojo.date.locale
-                                        a = dojo.date.locale.parse(a,{});
-                                        b = dojo.date.locale.parse(b,{});
-                                    break;
-                                    case 'number' :
-                                        a = Number(a); b = Number(b);
-                                    break;
-                                    case 'money' :
-                                        a = util.money.dollars_float_to_cents_integer(a);
-                                        b = util.money.dollars_float_to_cents_integer(b);
-                                    break;
-                                    case 'title' : /* special case for "a" and "the".  doesn't use marc 245 indicator */
-                                        a = String( a ).toUpperCase().replace( /^\s*(THE|A|AN)\s+/, '' );
-                                        b = String( b ).toUpperCase().replace( /^\s*(THE|A|AN)\s+/, '' );
-                                    break;
-                                    default:
-                                        a = String( a ).toUpperCase();
-                                        b = String( b ).toUpperCase();
-                                    break;
+                        rows = rows.sort( function(A,B) {
+                            function normalize(a,b,p) {
+                                if (a.sort_value) {
+                                    a = a.sort_value;
+                                    b = b.sort_value;
+                                } else {
+                                    a = a.value;
+                                    b = b.value;
+                                    if (obj.columns[p].sort_type) {
+                                        switch(obj.columns[p].sort_type) {
+                                            case 'date' :
+                                                JSAN.use('util.date'); // to pull in dojo.date.locale
+                                                a = dojo.date.locale.parse(a,{});
+                                                b = dojo.date.locale.parse(b,{});
+                                            break;
+                                            case 'number' :
+                                                a = Number(a); b = Number(b);
+                                            break;
+                                            case 'money' :
+                                                a = util.money.dollars_float_to_cents_integer(a);
+                                                b = util.money.dollars_float_to_cents_integer(b);
+                                            break;
+                                            case 'title' : /* special case for "a" and "the".  doesn't use marc 245 indicator */
+                                                a = String( a ).toUpperCase().replace( /^\s*(THE|A|AN)\s+/, '' );
+                                                b = String( b ).toUpperCase().replace( /^\s*(THE|A|AN)\s+/, '' );
+                                            break;
+                                            default:
+                                                a = String( a ).toUpperCase();
+                                                b = String( b ).toUpperCase();
+                                            break;
+                                        }
+                                    } else {
+                                        if (typeof a == 'string' || typeof b == 'string') {
+                                            a = String( a ).toUpperCase();
+                                            b = String( b ).toUpperCase();
+                                        }
+                                    }
                                 }
-                            } else {
-                                if (typeof a == 'string' || typeof b == 'string') {
-                                    a = String( a ).toUpperCase();
-                                    b = String( b ).toUpperCase();
+                                return [ a, b ];
+                            }
+
+                            for (var i = 0; i < sorts.length; i++) {
+                                var values;
+                                if (sorts[i].sortDir == 'asc') {
+                                    values = normalize(
+                                        A['values'][i]['value'],
+                                        B['values'][i]['value'],
+                                        A['values'][i]['position']
+                                    );
+                                } else {
+                                    values = normalize(
+                                        B['values'][i]['value'],
+                                        A['values'][i]['value'],
+                                        A['values'][i]['position']
+                                    );
+                                }
+                                if (values[0] < values[1] ) {
+                                    return -1;
+                                }
+                                if (values[0] > values[1] ) {
+                                    return 1;
                                 }
                             }
-                            //dump('sorting: type = ' + col.getAttribute('sort_type') + ' a = ' + a + ' b = ' + b + ' a<b= ' + (a<b) + ' a>b= ' + (a>b) + '\n');
-                            if (a < b) return -1; 
-                            if (a > b) return 1; 
                             return 0; 
                         } );
-                        if (sortDir == 'asc') rows = rows.reverse();
                         while(obj.treechildren.lastChild) obj.treechildren.removeChild( obj.treechildren.lastChild );
                         for (var i = 0; i < rows.length; i++) {
                             obj.treechildren.appendChild( rows[i].node );
@@ -1511,6 +1852,7 @@ util.list.prototype = {
                     } catch(E) {
                         obj.error.standard_unexpected_error_alert('sorting',E); 
                     }
+                    obj.refresh_ordinals();
                 }
             );
         } catch(E) {
@@ -1615,7 +1957,8 @@ util.list.prototype = {
         try {
             var x = document.getElementById(obj.node.id + '_clipfield');
             if (x) {
-                x.addEventListener(
+                obj.event_listeners.add(
+                    x,
                     'command',
                     function() {
                         obj.clipboard(params);
@@ -1628,7 +1971,8 @@ util.list.prototype = {
             }
             x = document.getElementById(obj.node.id + '_csv_to_clipboard');
             if (x) {
-                x.addEventListener(
+                obj.event_listeners.add(
+                    x,
                     'command',
                     function() {
                         obj.dump_csv_to_clipboard(params);
@@ -1641,7 +1985,8 @@ util.list.prototype = {
             }
             x = document.getElementById(obj.node.id + '_csv_to_printer');
             if (x) {
-                x.addEventListener(
+                obj.event_listeners.add(
+                    x,
                     'command',
                     function() {
                         obj.dump_csv_to_printer(params);
@@ -1654,7 +1999,8 @@ util.list.prototype = {
             }
             x = document.getElementById(obj.node.id + '_extended_to_printer');
             if (x) {
-                x.addEventListener(
+                obj.event_listeners.add(
+                    x,
                     'command',
                     function() {
                         obj.dump_extended_format_to_printer(params);
@@ -1668,7 +2014,8 @@ util.list.prototype = {
 
             x = document.getElementById(obj.node.id + '_csv_to_file');
             if (x) {
-                x.addEventListener(
+                obj.event_listeners.add(
+                    x,
                     'command',
                     function() {
                         obj.dump_csv_to_file(params);
@@ -1681,7 +2028,8 @@ util.list.prototype = {
             }
             x = document.getElementById(obj.node.id + '_save_columns');
             if (x) {
-                x.addEventListener(
+                obj.event_listeners.add(
+                    x,
                     'command',
                     function() {
                         obj.save_columns(params);
@@ -1716,6 +2064,7 @@ util.list.prototype = {
                 var col_id = prefix + hint + '_' + my_field.name;
                 var dataobj = hint;
                 var datafield = my_field.name;
+                var fleshed_display_field;
                 if (column_extras) {
                     if (column_extras['*']) {
                         if (column_extras['*']['dataobj']) {
@@ -1729,6 +2078,9 @@ util.list.prototype = {
                         if (column_extras[col_id]['datafield']) {
                             datafield = column_extras[col_id]['datafield'];
                         }
+                        if (column_extras[col_id]['fleshed_display_field']) {
+                            fleshed_display_field = column_extras[col_id]['fleshed_display_field'];
+                        }
                     }
                 }
                 var def = {
@@ -1743,7 +2095,24 @@ util.list.prototype = {
                 // my_field.datatype => bool float id int interval link money number org_unit text timestamp
                 if (my_field.datatype == 'link') {
                     def.render = function(my) { 
-                        return typeof my[dataobj][datafield]() == 'object' ? my[dataobj][datafield]()[my_field.key]() : my[dataobj][datafield](); 
+                        // is the object fleshed?
+                        return my[dataobj][datafield]() && typeof my[dataobj][datafield]() == 'object'
+                            // yes, show the display field
+                            ? my[dataobj][datafield]()[fleshed_display_field||my_field.key]()
+                            // no, do we have its class in data.hash?
+                            : ( typeof data.hash[ my[dataobj].Structure.field_map[datafield].class ] != 'undefined'
+                                // yes, do we have this particular object cached?
+                                ? ( data.hash[ my[dataobj].Structure.field_map[datafield].class ][ my[dataobj][datafield]() ]
+                                    // yes, show the display field
+                                    ? data.hash[ my[dataobj].Structure.field_map[datafield].class ][ my[dataobj][datafield]() ][
+                                        fleshed_display_field||my_field.key
+                                    ]()
+                                    // no, just show the raw value
+                                    : my[dataobj][datafield]()
+                                )
+                                // no, just show the raw value
+                                : my[dataobj][datafield]()
+                            ); 
                     }
                 } else {
                     def.render = function(my) { return my[dataobj][datafield](); }
@@ -1753,6 +2122,9 @@ util.list.prototype = {
                     def.render = function(my) {
                         return util.date.formatted_date( my[dataobj][datafield](), '%{localized}' );
                     }
+                    def.sort_value = function(my) {
+                        return util.date.db_date2Date( my[dataobj][datafield]() ).getTime();
+                    }
                 }
                 if (my_field.datatype == 'org_unit') {
                     def.render = function(my) {
@@ -1764,6 +2136,9 @@ util.list.prototype = {
                     def.render = function(my) {
                         return util.money.sanitize( my[dataobj][datafield]() );
                     }
+                    def.sort_value = function(my) {
+                        return util.money.dollars_float_to_cents_integer( my[dataobj][datafield]() );
+                    }
                 }
                 if (column_extras) {
                     if (column_extras['*']) {
@@ -1830,8 +2205,13 @@ util.list.prototype = {
             JSAN.use('util.network'); obj.network = new util.network();
             JSAN.use('util.money');
 
+            // FIXME: backwards compatability with server/patron code and the old patron.util.std_map_row_to_columns.
+            // Will remove in a separate commit and change all instances of obj.OpenILS.data to obj.data at the same time.
+            obj.OpenILS = { 'data' : obj.data };
+
             var my = row.my;
             var values = [];
+            var sort_values = [];
             var cmd = '';
             try {
                 for (var i = 0; i < cols.length; i++) {
@@ -1840,13 +2220,28 @@ util.list.prototype = {
                         case 'string' : cmd += 'try { ' + cols[i].render + '; values['+i+'] = v; } catch(E) { values['+i+'] = error_value; }'; break;
                         default: cmd += 'values['+i+'] = "??? '+(typeof cols[i].render)+'"; ';
                     }
+                    switch (typeof cols[i].sort_value) {
+                        case 'function':
+                            try {
+                                sort_values[i] = cols[i].sort_value(my,scratch);
+                            } catch(E) {
+                                sort_values[i] = error_value;
+                                obj.error.sdump('D_COLUMN_RENDER_ERROR',E);
+                            }
+                        break;
+                        case 'string' :
+                            sort_values[i] = JSON2js(cols[i].sort_value);
+                        break;
+                        default:
+                            cmd += 'sort_values['+i+'] = values[' + i + '];';
+                    }
                 }
                 if (cmd) eval( cmd );
             } catch(E) {
                 obj.error.sdump('D_WARN','map_row_to_column: ' + E);
                 if (error_value) { value = error_value; } else { value = '   ' };
             }
-            return values;
+            return { 'values' : values, 'sort_values' : sort_values };
         }
     }
 }