Fixed bug in staff client offline mode.
[working/Evergreen.git] / Open-ILS / xul / staff_client / chrome / content / util / list.js
index e18e693..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' : 
@@ -20,6 +24,11 @@ util.list = function (id) {
 
     JSAN.use('util.error'); this.error = new util.error();
 
+    JSAN.use('OpenILS.data'); this.data = new OpenILS.data(); this.data.stash_retrieve();
+
+    JSAN.use('util.functional');
+    JSAN.use('util.widgets');
+
     return this;
 };
 
@@ -28,9 +37,16 @@ util.list.prototype = {
     'init' : function (params) {
 
         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');
 
+        obj.printer_context = params.printer_context;
+
         if (typeof params.map_row_to_column == 'function') obj.map_row_to_column = params.map_row_to_column;
         if (typeof params.map_row_to_columns == 'function') {
             obj.map_row_to_columns = params.map_row_to_columns;
@@ -43,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] );
@@ -72,15 +104,35 @@ 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++) {
                 var treecol = document.createElement('treecol');
                 for (var j in this.columns[i]) {
-                    treecol.setAttribute(j,this.columns[i][j]);
+                    var value = this.columns[i][j];
+                    if (j=='id') {
+                        if (typeof check_for_id_collisions[value] == 'undefined') {
+                            check_for_id_collisions[value] = true;
+                        } else {
+                            // Column id's are important for sorting and saving list configuration.  Collisions started happening because
+                            // we were using field names as id's, and then later combining column definitions for multiple objects that
+                            // shared field names.  The downside to this sort of automatic collision prevention is that these generated
+                            // id's can change as we add and remove columns, possibly breaking saved list configurations.
+                            dump('Column collision with id = ' + value + ', renaming to ');
+                            value = value + '_collision_' + i;
+                            dump(value + '\n');
+                        }
+                    }
+                    treecol.setAttribute(j,value);
                 }
                 treecols.appendChild(treecol);
+
                 if (this.columns[i].type == 'checkbox') {
-                    treecol.addEventListener(
+                    obj.event_listeners.add(
+                        treecol,
                         'click',
                         function(ev) {
                             setTimeout(
@@ -95,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
                     );
@@ -133,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') {
@@ -148,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
@@ -189,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);
     },
@@ -216,18 +389,29 @@ util.list.prototype = {
         }
     },
 
+    'cleanup' : function () {
+        var obj = this;
+        obj.event_listeners.removeAll();
+    },
+
     'save_columns' : function (params) {
         var obj = this;
-        switch (this.node.nodeName) {
-            case 'tree' : this._save_columns_tree(params); break;
-            default: throw('NYI: Need .save_columns() for ' + this.node.nodeName); break;
+        if (obj.data.hash.aous['gui.disable_local_save_columns']) {
+            alert(document.getElementById('offlineStrings').getString('list.column_save_disabled'));
+        } else {
+            switch (this.node.nodeName) {
+                case 'tree' : this._save_columns_tree(params); break;
+                default: throw('NYI: Need .save_columns() for ' + this.node.nodeName); break;
+            }
         }
     },
 
     '_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;
             }
@@ -245,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();
@@ -266,15 +449,41 @@ 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;
             }
 
-            netscape.security.PrivilegeManager.enablePrivilege('UniversalXPConnect');
-            JSAN.use('util.file'); var file = new util.file('tree_columns_for_'+window.escape(id));
-            if (file._file.exists()) {
-                var my_cols = file.get_object(); file.close();
+            var my_cols;
+            if (! obj.data.hash.aous) { obj.data.hash.aous = {}; }
+            if (! obj.data.hash.aous['gui.disable_local_save_columns']) {
+                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();
+                }
+            }
+            /* local file will trump remote file if allowed, so save ourselves an http request if this is the case */
+            if (obj.data.hash.aous['url.remote_column_settings'] && ! my_cols ) {
+                try {
+                    var x = new XMLHttpRequest();
+                    var url = obj.data.hash.aous['url.remote_column_settings'] + '/tree_columns_for_' + window.escape(id);
+                    x.open("GET", url, false);
+                    x.send(null);
+                    if (x.status == 200) {
+                        my_cols = JSON2js( x.responseText );
+                    }
+                } catch(E) {
+                    // This can happen in the offline interface if you logged in previously and url.remote_column_settings is set.
+                    // 1) You may be really "offline" now
+                    // 2) the URL may just be a path component without a hostname (ie "/xul/column_settings/"), which won't work
+                    // when appended to chrome://open_ils_staff_client/
+                    dump('Error retrieving column settings from ' + url + ': ' + E + '\n');
+                }
+            }
+
+            if (my_cols) {
                 var nl = obj.node.getElementsByTagName('treecol');
                 for (var i = 0; i < nl.length; i++) {
                     var col = nl[i];
@@ -343,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);
@@ -367,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--;
@@ -438,10 +647,11 @@ util.list.prototype = {
         s += ('tree = ' + this.node + '  treechildren = ' + treechildren_node + '\n');
         s += ('treeitem = ' + treeitem + '  treerow = ' + treerow + '\n');
 
-        if (typeof params.retrieve_row == 'function' || typeof this.retrieve_row == 'function') {
+        obj.put_retrieving_label(treerow);
 
-            obj.put_retrieving_label(treerow);
-            treerow.addEventListener(
+        if (typeof params.retrieve_row == 'function' || typeof this.retrieve_row == 'function') {
+            obj.event_listeners.add(
+                treerow,
                 'flesh',
                 function() {
 
@@ -460,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 ) ) {
@@ -491,19 +701,22 @@ util.list.prototype = {
                     
                             inc_fleshed();
                     }
+                    obj.refresh_ordinals();
                 },
                 false
             );
-            /*
-            setTimeout(
-                function() {
-                    util.widgets.dispatch('flesh',treerow);
-                }, 0
-            );
-            */
+            if (typeof params.flesh_immediately != 'undefined') {
+                if (params.flesh_immediately) {
+                    setTimeout(
+                        function() {
+                            util.widgets.dispatch('flesh',treerow);
+                        }, 0
+                    );
+                }
+            }
         } else {
-            obj.put_retrieving_label(treerow);
-            treerow.addEventListener(
+            obj.event_listeners.add(
+                treerow,
                 'flesh',
                 function() {
                     //dump('fleshing anon\n');
@@ -515,16 +728,19 @@ util.list.prototype = {
                     if (obj.row_count.fleshed >= obj.row_count.total) {
                         setTimeout( function() { obj.exec_on_all_fleshed(); }, 0 );
                     }
+                    obj.refresh_ordinals();
                 },
                 false
             );
-            /*
-            setTimeout(
-                function() {
-                    util.widgets.dispatch('flesh',treerow);
-                }, 0
-            );
-            */
+            if (typeof params.flesh_immediately != 'undefined') {
+                if (params.flesh_immediately) {
+                    setTimeout(
+                        function() {
+                            util.widgets.dispatch('flesh',treerow);
+                        }, 0
+                    );
+                }
+            }
         }
         this.error.sdump('D_LIST',s);
 
@@ -544,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;
     },
 
@@ -555,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') {
@@ -577,23 +793,29 @@ util.list.prototype = {
                 }
             }
         }
-        var delete_me = [];
-        for (var i in treeitem.childNodes) if (treeitem.childNodes[i].nodeName == 'treerow') delete_me.push(treeitem.childNodes[i]);
-        for (var i = 0; i < delete_me.length; i++) treeitem.removeChild(delete_me[i]);
+        //var delete_me = [];
+        //for (var i in treeitem.childNodes) if (treeitem.childNodes[i].nodeName == 'treerow') delete_me.push(treeitem.childNodes[i]);
+        //for (var i = 0; i < delete_me.length; i++) treeitem.removeChild(delete_me[i]);
+        var prev_treerow = treeitem.firstChild; /* FIXME: worry about hierarchal lists like copy_browser? */
         var treerow = document.createElement('treerow');
-        treeitem.appendChild( treerow );
+        while (prev_treerow.firstChild) {
+            treerow.appendChild( prev_treerow.removeChild( prev_treerow.firstChild ) );
+        }
+        treeitem.replaceChild( treerow, prev_treerow );
         treerow.setAttribute('retrieve_id',params.retrieve_id);
         if (params.row_properties) treerow.setAttribute('properties',params.row_properties);
 
         s += ('tree = ' + this.node.nodeName + '\n');
         s += ('treeitem = ' + treeitem.nodeName + '  treerow = ' + treerow.nodeName + '\n');
 
+        obj.put_retrieving_label(treerow);
+
         if (typeof params.retrieve_row == 'function' || typeof this.retrieve_row == 'function') {
 
             s += 'found a retrieve_row function\n';
 
-            obj.put_retrieving_label(treerow);
-            treerow.addEventListener(
+            obj.event_listeners.add(
+                treerow,
                 'flesh',
                 function() {
 
@@ -612,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 ) ) {
@@ -643,22 +865,26 @@ util.list.prototype = {
                     
                             inc_fleshed();
                     }
+                    obj.refresh_ordinals();
                 },
                 false
             );
-            /*
-            setTimeout(
-                function() {
-                    util.widgets.dispatch('flesh',treerow);
-                }, 0
-            );
-            */
+            if (typeof params.flesh_immediately != 'undefined') {
+                if (params.flesh_immediately) {
+                    setTimeout(
+                        function() {
+                            util.widgets.dispatch('flesh',treerow);
+                        }, 0
+                    );
+                }
+            }
+
         } else {
 
             s += 'did not find a retrieve_row function\n';
 
-            obj.put_retrieving_label(treerow);
-            treerow.addEventListener(
+            obj.event_listeners.add(
+                treerow,
                 'flesh',
                 function() {
                     //dump('fleshing anon\n');
@@ -670,16 +896,20 @@ util.list.prototype = {
                     if (obj.row_count.fleshed >= obj.row_count.total) {
                         setTimeout( function() { obj.exec_on_all_fleshed(); }, 0 );
                     }
+                    obj.refresh_ordinals();
                 },
                 false
             );
-            /*
-            setTimeout(
-                function() {
-                    util.widgets.dispatch('flesh',treerow);
-                }, 0
-            );
-            */
+            if (typeof params.flesh_immediately != 'undefined') {
+                if (params.flesh_immediately) {
+                    setTimeout(
+                        function() {
+                            util.widgets.dispatch('flesh',treerow);
+                        }, 0
+                    );
+                }
+            }
+
         }
 
             try {
@@ -696,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);
 
@@ -705,28 +935,64 @@ util.list.prototype = {
         return params;
     },
 
-    'put_retrieving_label' : function(treerow) {
+    'refresh_ordinals' : function() {
         var obj = this;
         try {
-            /*
-            var cols_idx = 0;
-            dump('put_retrieving_label.  columns = ' + js2JSON(obj.columns) + '\n');
-            while( obj.columns[cols_idx] && obj.columns[cols_idx].hidden && obj.columns[cols_idx].hidden == 'true') {
-                dump('\t' + cols_idx);
-                var treecell = document.createElement('treecell');
-                treerow.appendChild(treecell);
-                cols_idx++;
+            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 {
             for (var i = 0; i < obj.columns.length; i++) {
-            var treecell = document.createElement('treecell'); treecell.setAttribute('label',document.getElementById('offlineStrings').getString('list.row_retrieving'));
-            treerow.appendChild(treecell);
+                var treecell;
+                if (typeof treerow.childNodes[i] == 'undefined') {
+                    treecell = document.createElement('treecell');
+                    treerow.appendChild(treecell);
+                } else {
+                    treecell = treerow.childNodes[i];
+                }
+                treecell.setAttribute('label',document.getElementById('offlineStrings').getString('list.row_retrieving'));
             }
-            /*
-            dump('\t' + cols_idx + '\n');
-            */
         } catch(E) {
-            alert(E);
+            alert('Error in list.js, put_retrieving_label(): ' + E);
         }
     },
 
@@ -833,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) {
@@ -856,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) {
@@ -897,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);
@@ -925,7 +1192,7 @@ util.list.prototype = {
         }
 
         this.error.sdump('D_LIST',s);
-        params.my_node = listitem;
+        params.treeitem_node = listitem;
         return params;
 
     },
@@ -933,62 +1200,102 @@ util.list.prototype = {
     '_map_row_to_treecell' : function(params,treerow) {
         var obj = this;
         var s = '';
-        util.widgets.remove_children(treerow);
 
         if (typeof params.map_row_to_column == 'function' || typeof this.map_row_to_column == 'function') {
 
             for (var i = 0; i < this.columns.length; i++) {
-                var treecell = document.createElement('treecell');
+                var treecell;
+                if (typeof treerow.childNodes[i] == 'undefined') {
+                    treecell = document.createElement('treecell');
+                    treerow.appendChild( treecell );
+                } else {
+                    treecell = treerow.childNodes[i];
+                }
+                
                 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)) {
                     treecell.setAttribute('label',label);
-                    treerow.appendChild( treecell );
                     s += ('treecell = ' + treecell + ' with label = ' + label + '\n');
                     continue;
                 }
                 if (params.skip_all_columns_except && (params.skip_all_columns_except.indexOf(i) == -1)) {
                     treecell.setAttribute('label',label);
-                    treerow.appendChild( treecell );
                     s += ('treecell = ' + treecell + ' with label = ' + label + '\n');
                     continue;
                 }
     
                 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]);
-    
+                     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]);
-    
+                   }
                 }
                 if (this.columns[i].type == 'checkbox') { treecell.setAttribute('value',label); } else { treecell.setAttribute('label',label ? label : ''); }
-                treerow.appendChild( treecell );
                 s += ('treecell = ' + treecell + ' with label = ' + label + '\n');
             }
         } 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);
+                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);
-
+                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 = document.createElement('treecell');
+                var treecell;
+                if (typeof treerow.childNodes[i] == 'undefined') {
+                    treecell = document.createElement('treecell');
+                    treerow.appendChild(treecell);
+                } else {
+                    treecell = treerow.childNodes[i];
+                }
                 if ( this.columns[i].editable == false ) { treecell.setAttribute('editable','false'); }
                 if ( this.columns[i].type == 'checkbox') {
                     treecell.setAttribute('value', labels[i]);
                 } else {
                     treecell.setAttribute('label',typeof labels[i] == 'string' || typeof labels[i] == 'number' ? labels[i] : '');
                 }
-                treerow.appendChild( treecell );
+                if (sort_values[i]) {
+                    treecell.setAttribute('sort_value',js2JSON(sort_values[i]));
+                }
                 s += ('treecell = ' + treecell + ' with label = ' + labels[i] + '\n');
             }
 
@@ -1006,13 +1313,13 @@ util.list.prototype = {
             var value = '';
             if (typeof params.map_row_to_column == 'function')  {
 
-                value = params.map_row_to_column(params.row,this.columns[i]);
+                value = params.map_row_to_column(params.row,this.columns[i],this.scratch_data);
 
             } else {
 
                 if (typeof this.map_row_to_column == 'function') {
 
-                    value = this.map_row_to_column(params.row,this.columns[i]);
+                    value = this.map_row_to_column(params.row,this.columns[i],this.scratch_data);
                 }
             }
             if (typeof value == 'string' || typeof value == 'number') {
@@ -1100,15 +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++) {
-                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;
     },
 
@@ -1123,31 +1450,91 @@ util.list.prototype = {
 
     '_dump_tree_csv' : function(params) {
         var obj = this;
-        var dump = '';
+        var _dump = '';
+        var ord_cols = [];
         for (var j = 0; j < obj.columns.length; j++) {
             if (obj.node.treeBoxObject.columns.getColumnAt(j).element.getAttribute('hidden') == 'true') {
                 /* skip */
             } else {
-                if (dump) dump += ',';
-                dump += '"' + obj.columns[j].label.replace(/"/g, '""') + '"';
+                ord_cols.push( [ obj.node.treeBoxObject.columns.getColumnAt(j).element.getAttribute('ordinal'), j ] );
             }
         }
-        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 < treerow.childNodes.length; j++) {
-                if (obj.node.treeBoxObject.columns.getColumnAt(j).element.getAttribute('hidden') == 'true') {
-                    /* skip */
-                } else {
+        ord_cols.sort( function(a,b) { 
+            if ( Number( a[0] ) < Number( b[0] ) ) return -1; 
+            if ( Number( a[0] ) > Number( b[0] ) ) return 1; 
+            return 0;
+        } );
+        for (var j = 0; j < ord_cols.length; j++) {
+            if (_dump) _dump += ',';
+            _dump += '"' + obj.columns[ ord_cols[j][1] ].label.replace(/"/g, '""') + '"';
+        }
+        _dump += '\r\n';
+
+        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[j].getAttribute('label').replace(/"/g, '""') + '"';
+                    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';
         }
-        return dump;
+
+        process_tree(this.treechildren);
+
+        return _dump;
+    },
+
+    'dump_extended_format' : function(params) {
+        var obj = this;
+        switch(this.node.nodeName) {
+            case 'tree' : return this._dump_tree_extended_format(params); break;
+            default: throw('NYI: Need .dump_extended_format() for ' + this.node.nodeName); break;
+        }
+
+    },
+
+    '_dump_tree_extended_format' : function(params) {
+        var obj = this;
+        var _dump = '';
+        var ord_cols = [];
+        for (var j = 0; j < obj.columns.length; j++) {
+            if (obj.node.treeBoxObject.columns.getColumnAt(j).element.getAttribute('hidden') == 'true') {
+                /* skip */
+            } else {
+                ord_cols.push( [ obj.node.treeBoxObject.columns.getColumnAt(j).element.getAttribute('ordinal'), j ] );
+            }
+        }
+        ord_cols.sort( function(a,b) { 
+            if ( Number( a[0] ) < Number( b[0] ) ) return -1; 
+            if ( Number( a[0] ) > Number( b[0] ) ) return 1; 
+            return 0;
+        } );
+
+        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);
+                }
+            }
+        }
+
+        process_tree(this.treechildren);
+
+        return _dump;
     },
 
     'dump_csv_to_clipboard' : function(params) {
@@ -1162,8 +1549,8 @@ util.list.prototype = {
 
     'dump_csv_to_printer' : function(params) {
         var obj = this;
-        JSAN.use('util.print'); var print = new util.print();
         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 {
@@ -1175,6 +1562,21 @@ util.list.prototype = {
         }
     },
 
+    'dump_extended_format_to_printer' : function(params) {
+        var obj = this;
+        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 {
+            obj.wrap_in_full_retrieve( 
+                function() { 
+                    print.simple( obj.dump_extended_format( params ), {'content_type':'text/plain'} );
+                }
+            );
+        }
+    },
+
     'dump_csv_to_file' : function(params) {
         var obj = this;
         JSAN.use('util.file'); var f = new util.file();
@@ -1207,23 +1609,27 @@ util.list.prototype = {
     '_print_tree' : function(params) {
         var obj = this;
         try {
-            JSAN.use('OpenILS.data'); var data = new OpenILS.data(); data.stash_retrieve();
+            var data = obj.data; data.stash_retrieve();
             if (!params.staff && data.list.au && data.list.au[0]) {
                 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(
                 function() {
                     try {
                         if (!params.list) params.list = obj.dump_with_keys();
-                        JSAN.use('util.print'); var print = new util.print();
+                        JSAN.use('util.print'); var print = new util.print(params.printer_context || obj.printer_context);
                         print.tree_list( params );
                         if (typeof params.callback == 'function') params.callback();
                     } catch(E) {
@@ -1257,8 +1663,15 @@ util.list.prototype = {
             for (var j = 0; j < treerow.childNodes.length; j++) {
                 var value = treerow.childNodes[j].getAttribute('label');
                 if (params.skip_hidden_columns) if (obj.node.treeBoxObject.columns.getColumnAt(j).element.getAttribute('hidden') == 'true') continue;
-                var id = obj.columns[j].id; if (params.labels_instead_of_ids) id = obj.columns[j].label;
-                row[ id ] = value;
+                if (typeof obj.columns[j] == 'undefined') {
+                    dump('=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=\n');
+                    dump('_dump_tree_selection_with_keys @ ' + location.href + '\n');
+                    dump('\ttreerow.childNodes.length='+treerow.childNodes.length+' j='+j+' obj.columns.length='+obj.columns.length+'\n');
+                    debugger;
+                } else {
+                    var id = obj.columns[j].id; if (params.labels_instead_of_ids) id = obj.columns[j].label;
+                    row[ id ] = value;
+                }
             }
             dump.push( row );
         }
@@ -1269,8 +1682,8 @@ util.list.prototype = {
         try {
             var obj = this;
             var dump = obj.dump_selected_with_keys({'skip_hidden_columns':true,'labels_instead_of_ids':true});
-            JSAN.use('OpenILS.data'); var data = new OpenILS.data(); data.stash_retrieve();
-            data.list_clipboard = dump; data.stash('list_clipboard');
+            obj.data.stash_retrieve();
+            obj.data.list_clipboard = dump; obj.data.stash('list_clipboard');
             JSAN.use('util.window'); var win = new util.window();
             win.open(urls.XUL_LIST_CLIPBOARD,'list_clipboard','chrome,resizable,modal');
             window.focus(); // sometimes the main window will lower after a clipboard action
@@ -1307,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() {
@@ -1326,41 +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 '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;
                                 }
                             }
-                            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 );
@@ -1369,6 +1852,7 @@ util.list.prototype = {
                     } catch(E) {
                         obj.error.standard_unexpected_error_alert('sorting',E); 
                     }
+                    obj.refresh_ordinals();
                 }
             );
         } catch(E) {
@@ -1436,6 +1920,11 @@ util.list.prototype = {
             mi.setAttribute('accesskey',document.getElementById('offlineStrings').getString('list.actions.csv_to_printer.accesskey'));
             mp.appendChild(mi);
             mi = document.createElement('menuitem');
+            mi.setAttribute('id',obj.node.id + '_extended_to_printer');
+            mi.setAttribute('label',document.getElementById('offlineStrings').getString('list.actions.extended_to_printer.label'));
+            mi.setAttribute('accesskey',document.getElementById('offlineStrings').getString('list.actions.extended_to_printer.accesskey'));
+            mp.appendChild(mi);
+            mi = document.createElement('menuitem');
             mi.setAttribute('id',obj.node.id + '_csv_to_file');
             mi.setAttribute('label',document.getElementById('offlineStrings').getString('list.actions.csv_to_file.label'));
             mi.setAttribute('accesskey',document.getElementById('offlineStrings').getString('list.actions.csv_to_file.accesskey'));
@@ -1444,6 +1933,9 @@ util.list.prototype = {
             mi.setAttribute('id',obj.node.id + '_save_columns');
             mi.setAttribute('label',document.getElementById('offlineStrings').getString('list.actions.save_column_configuration.label'));
             mi.setAttribute('accesskey',document.getElementById('offlineStrings').getString('list.actions.save_column_configuration.accesskey'));
+            if (obj.data.hash.aous['gui.disable_local_save_columns']) {
+                mi.setAttribute('disabled','true');
+            }
             mp.appendChild(mi);
             return btn;
         } catch(E) {
@@ -1465,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);
@@ -1478,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);
@@ -1491,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);
@@ -1502,9 +1997,25 @@ util.list.prototype = {
                     false
                 );
             }
+            x = document.getElementById(obj.node.id + '_extended_to_printer');
+            if (x) {
+                obj.event_listeners.add(
+                    x,
+                    'command',
+                    function() {
+                        obj.dump_extended_format_to_printer(params);
+                        if (params && typeof params.on_complete == 'function') {
+                            params.on_complete(params);
+                        }
+                    },
+                    false
+                );
+            }
+
             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);
@@ -1517,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);
@@ -1535,9 +2047,10 @@ util.list.prototype = {
     },
 
     // Takes fieldmapper class name and attempts to spit out column definitions suitable for .init
-    'fm_columns' : function(hint,column_extras) {
+    'fm_columns' : function(hint,column_extras,prefix) {
         var obj = this;
         var columns = [];
+        if (!prefix) { prefix = ''; }
         try {
             // requires the dojo library fieldmapper.autoIDL
             if (typeof fieldmapper == 'undefined') { throw 'fieldmapper undefined'; }
@@ -1545,34 +2058,86 @@ util.list.prototype = {
             if (typeof fieldmapper.IDL.fmclasses == 'undefined') { throw 'fieldmapper.IDL.fmclasses undefined'; }
             if (typeof fieldmapper.IDL.fmclasses[hint] == 'undefined') { throw 'fieldmapper.IDL.fmclasses.' + hint + ' undefined'; }
             var my_class = fieldmapper.IDL.fmclasses[hint]; 
+            var data = obj.data; data.stash_retrieve();
 
             function col_def(my_field) {
-                var col_id = hint + '_' + my_field.name;
+                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']) {
+                            dataobj = column_extras['*']['dataobj'];
+                        }
+                    }
+                    if (column_extras[col_id]) {
+                        if (column_extras[col_id]['dataobj']) {
+                            dataobj = column_extras[col_id]['dataobj'];
+                        }
+                        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 = {
                     'id' : col_id,
                     'label' : my_field.label || my_field.name,
-                    'sort_type' : [ 'int', 'float', 'id', 'number' ].indexOf(my_field.datatype) > -1 ? 'number' : ( my_field.datatype == 'money' ? 'money' : 'default'),
-                    'hidden' : [ 'isnew', 'ischanged', 'isdeleted' ].indexOf(my_field.name) > -1,
+                    'sort_type' : [ 'int', 'float', 'id', 'number' ].indexOf(my_field.datatype) > -1 ? 'number' : 
+                        ( my_field.datatype == 'money' ? 'money' : 
+                        ( my_field.datatype == 'timestamp' ? 'date' : 'default')),
+                    'hidden' : my_field.virtual || my_field.datatype == 'link',
                     'flex' : 1
                 };                    
                 // my_field.datatype => bool float id int interval link money number org_unit text timestamp
-                def.render = function(my) { return my[hint][my_field.name](); }
+                if (my_field.datatype == 'link') {
+                    def.render = function(my) { 
+                        // 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](); }
+                }
                 if (my_field.datatype == 'timestamp') {
-                    dojo.require('dojo.date.locale');
-                    dojo.require('dojo.date.stamp');
+                    JSAN.use('util.date');
                     def.render = function(my) {
-                        return dojo.date.locale.format( dojo.date.stamp.fromISOString(my[hint][my_field.name]()) );
+                        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) {
-                        return typeof my[hint][my_field.name]() == 'object' ? my[hint][my_field.name]().shortname() : data.hash.aou[ my[hint][my_field.name]() ].shortname();
+                        return typeof my[dataobj][datafield]() == 'object' ? my[dataobj][datafield]().shortname() : data.hash.aou[ my[dataobj][datafield]() ].shortname();
                     }
                 }
                 if (my_field.datatype == 'money') {
                     JSAN.use('util.money');
                     def.render = function(my) {
-                        return util.money.sanitize( my[hint][my_field.name]() );
+                        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) {
@@ -1583,19 +2148,42 @@ util.list.prototype = {
                         if (column_extras['*']['expanded_label']) {
                             def.label = my_class.label + ': ' + def.label;
                         }
+                        if (column_extras['*']['label_prefix']) {
+                            def.label = column_extras['*']['label_prefix'] + def.label;
+                        }
+                        if (column_extras['*']['remove_virtual']) {
+                            if (my_field.virtual) {
+                                def.remove_me = true;
+                            }
+                        }
                     }
                     if (column_extras[col_id]) {
                         for (var attr in column_extras[col_id]) {
                             def[attr] = column_extras[col_id][attr];
                         }
+                        if (column_extras[col_id]['keep_me']) {
+                            def.remove_me = false;
+                        }
+                        if (column_extras[col_id]['label_prefix']) {
+                            def.label = column_extras[col_id]['label_prefix'] + def.label;
+                        }
                     }
                 }
-                return def;
+                if (def.remove_me) {
+                    dump('Skipping ' + def.label + '\n');
+                    return null;
+                } else {
+                    dump('Defining ' + def.label + '\n');
+                    return def;
+                }
             }
  
             for (var i = 0; i < my_class.fields.length; i++) {
                 var my_field = my_class.fields[i];
-                columns.push( col_def(my_field) );
+                var def = col_def(my_field);
+                if (def) {
+                    columns.push( def );
+                }
             }
 
         } catch(E) {
@@ -1605,9 +2193,11 @@ util.list.prototype = {
     },
     // Default for the map_row_to_columns function for .init
     'std_map_row_to_columns' : function(error_value) {
-        return function(row,cols) {
+        return function(row,cols,scratch) {
             // row contains { 'my' : { 'acp' : {}, 'circ' : {}, 'mvr' : {} } }
             // cols contains all of the objects listed above in columns
+            // scratch is a temporary space shared by all cells/rows (or just per row if not explicitly passed in)
+            if (!scratch) { scratch = {}; }
 
             var obj = {};
             JSAN.use('util.error'); obj.error = new util.error();
@@ -1615,23 +2205,43 @@ 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++) {
                     switch (typeof cols[i].render) {
-                        case 'function': try { values[i] = cols[i].render(my); } catch(E) { values[i] = error_value; obj.error.sdump('D_COLUMN_RENDER_ERROR',E); } break;
+                        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;
                         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 };
         }
     }
 }