LP#1777207: teach egGrid how to prepend rows more efficiently
[evergreen-equinox.git] / Open-ILS / web / js / ui / default / staff / services / grid.js
index 4c2ff19..3998abb 100644 (file)
@@ -110,18 +110,26 @@ angular.module('egGridMod',
         templateUrl : '/eg/staff/share/t_autogrid', 
 
         link : function(scope, element, attrs) {     
-            // link() is called after page compilation, which means our
-            // eg-grid-field's have been parsed and loaded.  Now it's 
-            // safe to perform our initial page load.
-
-            // load auto fields after eg-grid-field's so they are not clobbered
-            scope.handleAutoFields();
-            scope.collect();
-
-            scope.grid_element = element;
-            $(element)
-                .find('.eg-grid-content-body')
-                .bind('contextmenu', scope.showActionContextMenu);
+
+            // Give the grid config loading steps time to fetch the 
+            // workstation setting and apply columns before loading data.
+            var loadPromise = scope.configLoadPromise || $q.when();
+            loadPromise.then(function() {
+
+                // load auto fields after eg-grid-field's so they are not clobbered
+                scope.handleAutoFields();
+                scope.collect();
+
+                scope.grid_element = element;
+
+                if(!attrs.id){
+                    $(element).attr('id', attrs.persistKey);
+                }
+
+                $(element)
+                    .find('.eg-grid-content-body')
+                    .bind('contextmenu', scope.showActionContextMenu);
+            });
         },
 
         controller : [
@@ -244,7 +252,7 @@ angular.module('egGridMod',
 
                 grid.applyControlFunctions();
 
-                grid.loadConfig().then(function() { 
+                $scope.configLoadPromise = grid.loadConfig().then(function() { 
                     // link columns to scope after loadConfig(), since it
                     // replaces the columns array.
                     $scope.columns = grid.columnsProvider.columns;
@@ -278,6 +286,10 @@ angular.module('egGridMod',
                     return grid.getSelectedItems()
                 }
 
+                controls.selectItemsByValue = function(c,v) {
+                    return grid.selectItemsByValue(c,v)
+                }
+
                 controls.allItems = function() {
                     return $scope.items;
                 }
@@ -327,6 +339,10 @@ angular.module('egGridMod',
                     grid.collect();
                 }
 
+                controls.prepend = function(limit) {
+                    grid.prepend(limit);
+                }
+
                 controls.setLimit = function(limit,forget) {
                     grid.limit = limit;
                     if (!forget && grid.persistKey) {
@@ -348,6 +364,7 @@ angular.module('egGridMod',
                 }
 
                 grid.dataProvider.refresh = controls.refresh;
+                grid.dataProvider.prepend = controls.prepend;
                 grid.controls = controls;
             }
 
@@ -681,23 +698,18 @@ angular.module('egGridMod',
                 if (!$scope.menu_dom) $scope.menu_dom = $($scope.grid_element).find('.grid-action-dropdown')[0];
                 if (!$scope.action_context_parent) $scope.action_context_parent = $($scope.menu_dom).parent();
 
-                // we need the index of the row that got right-clicked...
+                // we need the the row that got right-clicked...
                 var e = $event.target; // the DOM element
                 var s = undefined;     // the angular scope for that element
-                while(e){ // searching for the row so we can get its index from s.$index
+                while(e){ // searching for the row
                     // abort & use the browser default context menu for links (lp1669856):
                     if(e.tagName.toLowerCase() === 'a' && e.href){ return true; }
                     s = angular.element(e).scope();
                     if(s.hasOwnProperty('item')){ break; }
                     e = e.parentElement;
                 }
-                /* $scope.items and $scope.selected indexes are "backwards" to each other.
-                $scope.items counts down from the top of the list (most recent item first)
-                $scope.selected counts forward as items are scanned (most recent item last)
-                s.$index is for $scope.items. we need the index for $scope.selected: */
-                var selectable_index = ($scope.items.length-1) - s.$index;
                 // select the right-clicked row if it is not already selected (lp1776557):
-                if(!$scope.selected[selectable_index]){ $event.target.click(); }
+                if(!$scope.selected[grid.indexValue(s.item)]){ $event.target.click(); }
 
                 if (!$scope.action_context_showing) {
                     $scope.action_context_width = $($scope.menu_dom).css('width');
@@ -743,6 +755,21 @@ angular.module('egGridMod',
                 $scope.selected[index] = true;
             }
 
+            // selects items by a column value, first clearing selected list.
+            // we overwrite the object so that we can watch $scope.selected
+            grid.selectItemsByValue = function(column, value) {
+                $scope.selected = {};
+                angular.forEach($scope.items, function(item) {
+                    var col_value;
+                    if (angular.isFunction(item[column]))
+                        col_value = item[column]();
+                    else
+                        col_value = item[column];
+
+                    if (value == col_value) $scope.selected[grid.indexValue(item)] = true
+                }); 
+            }
+
             // selects or deselects an item, without affecting the others.
             // returns true if the item is selected; false if de-selected.
             // we overwrite the object so that we can watch $scope.selected
@@ -1099,6 +1126,10 @@ angular.module('egGridMod',
                 return val;
             }
 
+            $scope.getHtmlTooltip = function(col, item) {
+                return grid.getItemTextContent(item, col);
+            }
+
             /**
              * Fetches all grid data and transates each item into a simple
              * key-value pair of column name => text-value.
@@ -1296,6 +1327,72 @@ angular.module('egGridMod',
                 });
             }
 
+            grid.prepend = function(limit) {
+                var ran_into_duplicate = false;
+                var sort = grid.dataProvider.sort;
+                if (sort && sort.length) {
+                    // If sorting is in effect, we have no way
+                    // of knowing that the new item should be
+                    // visible _if the sort order is retained_.
+                    // However, since the grids that do prepending in
+                    // the first place are ones where we always
+                    // want the new row to show up on top, we'll
+                    // remove the current sort options.
+                    grid.dataProvider.sort = [];
+                }
+                if (grid.offset > 0) {
+                    // if we're prepending, we're forcing the
+                    // offset back to zero to display the top
+                    // of the list
+                    grid.offset = 0;
+                    grid.collect();
+                    return;
+                }
+                if (grid.collecting) return; // avoid parallel collect() or prepend()
+                grid.collecting = true;
+                console.debug('egGrid.prepend() starting');
+                // Note that we can count on the most-recently added
+                // item being at offset 0 in the data provider only
+                // for arrayNotifier data sources that do not have
+                // sort options currently set.
+                grid.dataProvider.get(0, 1).then(
+                null,
+                null,
+                function(item) {
+                    if (item) {
+                        var newIdx = grid.indexValue(item);
+                        angular.forEach($scope.items, function(existing) {
+                            if (grid.indexValue(existing) == newIdx) {
+                                console.debug('egGrid.prepend(): refusing to add duplicate item ' + newIdx);
+                                ran_into_duplicate = true;
+                                return;
+                            }
+                        });
+                        $scope.items.unshift(item);
+                        if (limit && $scope.items.length > limit) {
+                            // this accommodates the checkin grid that
+                            // allows the user to set a definite limit
+                            // without requiring that entire collect()
+                            $scope.items.length = limit;
+                        }
+                        if ($scope.items.length > grid.limit) {
+                            $scope.items.length = grid.limit;
+                        }
+                        if (grid.controls.itemRetrieved)
+                            grid.controls.itemRetrieved(item);
+                        if ($scope.selectAll)
+                            $scope.selected[grid.indexValue(item)] = true
+                    }
+                }).finally(function() {
+                    console.debug('egGrid.prepend() complete');
+                    grid.collecting = false;
+                    $scope.selected = angular.copy($scope.selected);
+                    if (ran_into_duplicate) {
+                        grid.collect();
+                    }
+                });
+            }
+
             grid.init();
         }]
     };
@@ -1339,7 +1436,11 @@ angular.module('egGridMod',
             // optional hash of functions that can be imported into
             // the directive's scope; meant for cases where the "compiled"
             // attribute is set
-            handlers : '='
+            handlers : '=',
+
+            // optional: CSS class name that we want to have for this field.
+            // Auto generated from path if nothing is passed in via eg-grid-field declaration
+            cssSelector : "@"
         },
         link : function(scope, element, attrs, egGridCtrl) {
 
@@ -1361,6 +1462,16 @@ angular.module('egGridMod',
                 }
             );
 
+            scope.cssSelector = attrs['cssSelector'] ? attrs['cssSelector'] : "";
+
+            // auto-generate CSS selector name for field if none declared in tt2 and there's a path
+            if (scope.path && !scope.cssSelector){
+                var cssClass = 'grid' + "." + scope.path;
+                cssClass = cssClass.replace(/\./g,'-');
+                element.addClass(cssClass);
+                scope.cssSelector = cssClass;
+            }
+
             // any HTML content within the field is its custom template
             var tmpl = element.html();
             if (tmpl && !tmpl.match(/^\s*$/))
@@ -1627,7 +1738,8 @@ angular.module('egGridMod',
                 datecontext      : colSpec.datecontext,
                 datefilter      : colSpec.datefilter,
                 dateonlyinterval : colSpec.dateonlyinterval,
-                parentIdlClass   : colSpec.parentIdlClass
+                parentIdlClass   : colSpec.parentIdlClass,
+                cssSelector      : colSpec.cssSelector
             };
         }
 
@@ -1871,6 +1983,7 @@ angular.module('egGridMod',
             // Calls the grid refresh function.  Once instantiated, the
             // grid will replace this function with it's own refresh()
             gridData.refresh = function(noReset) { }
+            gridData.prepend = function(limit) { }
 
             if (!gridData.get) {
                 // returns a promise whose notify() delivers items
@@ -2187,11 +2300,10 @@ angular.module('egGridMod',
 /**
  * Translates bare IDL object values into display values.
  * 1. Passes dates through the angular date filter
- * 2. Translates bools to Booleans so the browser can display translated 
- *    value.  (Though we could manually translate instead..)
+ * 2. Converts bools to translated Yes/No strings
  * Others likely to follow...
  */
-.filter('egGridValueFilter', ['$filter','egCore', function($filter,egCore) {
+.filter('egGridValueFilter', ['$filter','egCore', 'egStrings', function($filter,egCore,egStrings) {
     function traversePath(obj,path) {
         var list = path.split('.');
         for (var part in path) {
@@ -2209,11 +2321,11 @@ angular.module('egGridMod',
                     case 't' : 
                     case '1' :  // legacy
                     case true:
-                        return ''+true;
+                        return egStrings.YES;
                     case 'f' : 
                     case '0' :  // legacy
                     case false:
-                        return ''+false;
+                        return egStrings.NO;
                     // value may be null,  '', etc.
                     default : return '';
                 }