web staff : initial infini scroll grid support
authorBill Erickson <berick@esilibrary.com>
Mon, 31 Mar 2014 14:57:53 +0000 (10:57 -0400)
committerBill Erickson <berick@esilibrary.com>
Mon, 31 Mar 2014 14:57:53 +0000 (10:57 -0400)
Signed-off-by: Bill Erickson <berick@esilibrary.com>
Open-ILS/src/templates/staff/parts/t_autogrid.tt2
Open-ILS/src/templates/staff/test/index.tt2
Open-ILS/web/js/ui/default/staff/services/grid.js
Open-ILS/web/js/ui/default/staff/services/list.js
Open-ILS/web/js/ui/default/staff/services/ui-scroll-jqlite.js [new file with mode: 0644]
Open-ILS/web/js/ui/default/staff/services/ui-scroll.js [new file with mode: 0644]

index 6a0f57b..d3b732f 100644 (file)
@@ -79,7 +79,7 @@
     <div class="eg-grid-cell"
         eg-drag-dest column="{{column.name}}"
         ng-repeat="column in list.allColumns"
-        style="flex:{{column.flex}}"
+        style="flex:{{column.flex}};order:{{list.columnPosition(column.name)}}"
         ng-show="list.displayColumns[column.name]">
         <a eg-drag-source column="{{column.name}}" 
           href='' ng-click="sortOn(column.name)">{{column.label}}</a>
@@ -97,7 +97,7 @@
     </div>
     <div class="eg-grid-cell"
       ng-repeat="column in list.allColumns"
-      style="flex:{{column.flex}}"
+      style="flex:{{column.flex}};order:{{list.columnPosition(column.name)}}"
       ng-show="list.displayColumns[column.name]">
       <div class="eg-grid-conf-cell-entry">
         <a href="" title="[% l('Make column wider') %]"
 
   <!-- ================= -->
   <!-- grid content rows -->
-  <div class="eg-grid-content-body">
-    <div class="eg-grid-row" 
+  <!--
+  <div class="eg-grid-content-body" infinite-scroll="fetchMoreData()">
+  -->
+  <div class="eg-grid-content-body" ng-scroll-viewport style="height:600px;">
+        <!--
         ng-repeat="item in list.items"
+        -->
+    <div class="eg-grid-row" 
+        ng-scroll="item in egGridData"
         ng-class="{'eg-grid-row-selected' : itemIsSelected(item)}">
       <div class="eg-grid-cell eg-grid-cell-stock" style="flex:{{indexFlex}}"
         ng-click="handleRowClick($event, item)">
       <div class="eg-grid-cell"
           ng-click="handleRowClick($event, item)"
           ng-repeat="column in list.allColumns"
-          style="flex:{{column.flex}}"
+          style="flex:{{column.flex}};order:{{list.columnPosition(column.name)}}"
           ng-show="list.displayColumns[column.name]">
         {{fieldValue(item, column.name) | egGridvalueFilter:column}}
       </div>
index 0f360bd..3ecd3f1 100644 (file)
@@ -9,6 +9,8 @@
 <script src="[% ctx.media_prefix %]/js/ui/default/staff/services/list.js"></script>
 <script src="[% ctx.media_prefix %]/js/ui/default/staff/services/ui.js"></script>
 <script src="[% ctx.media_prefix %]/js/ui/default/staff/services/grid.js"></script>
+<script src="[% ctx.media_prefix %]/js/ui/default/staff/services/ui-scroll-jqlite.js"></script>
+<script src="[% ctx.media_prefix %]/js/ui/default/staff/services/ui-scroll.js"></script>
 <script src="[% ctx.media_prefix %]/js/ui/default/staff/test/app.js"></script>
 [% END %]
 
index 118a0a6..587532d 100644 (file)
@@ -1,7 +1,8 @@
 
-angular.module('egGridMod', ['egCoreMod', 'egListMod', 'egUiMod', 'ui.bootstrap'])
+angular.module('egGridMod', 
+    ['egCoreMod', 'egListMod', 'egUiMod', 'ui.bootstrap', 'ui.scroll.jqlite', 'ui.scroll'])
 
-.directive('egGrid', function() {
+.directive('egGrid', function($window) {
     return {
         restrict : 'AE',
         transclude : true,
@@ -48,19 +49,20 @@ angular.module('egGridMod', ['egCoreMod', 'egListMod', 'egUiMod', 'ui.bootstrap'
             // 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.
-            scope.fetchData();
+            //scope.fetchData();
         },
 
         templateUrl : '/eg/staff/parts/t_autogrid', // TODO: avoid abs url
 
         controller : // TODO: reqs list
-            function($scope, $timeout, $modal, $document, $window, egIDL, egAuth, egNet, egList) { 
+            function($scope, $timeout, $element, egIDL, egAuth, egNet, egList, egGridData) { 
             var self = this;
+            self.egGridData = egGridData;
 
             // setup function. called at the end of the controller
             this.init = function() {
-                self.limit = 25
-                self.ofset = 0;
+                self.limit = 10
+                self.offset = 0;
 
                 $scope.indexFlex = 1;
                 $scope.selectorFlex = 1;
@@ -82,6 +84,12 @@ angular.module('egGridMod', ['egCoreMod', 'egListMod', 'egUiMod', 'ui.bootstrap'
                     self.compileAutoFields();
 
                 $scope.list.indexField = $scope.idField;
+
+                self.egGridData.configure({
+                    idlClass : $scope.idlClass,
+                    query : $scope.query,
+                    list : $scope.list
+                });
             }
 
             // column-header click quick sort
@@ -94,7 +102,9 @@ angular.module('egGridMod', ['egCoreMod', 'egListMod', 'egUiMod', 'ui.bootstrap'
                 } else {
                     $scope.sort = [col_name];
                 }
-                $scope.fetchData();
+                egGridData.sort = $scope.sort;
+                egGridData.reset();
+                //$scope.fetchData();
             }
 
             // maps numeric sort priority to flattener sort blob
@@ -299,8 +309,7 @@ angular.module('egGridMod', ['egCoreMod', 'egListMod', 'egUiMod', 'ui.bootstrap'
                 // handled externally.
                 if ($scope.egList) return;
 
-                $scope.list.resetPageData();
-
+                //$scope.list.resetPageData();
 
                 var queryFields = {}
                 angular.forEach($scope.list.allColumns, function(field) {
@@ -393,6 +402,12 @@ angular.module('egGridMod', ['egCoreMod', 'egListMod', 'egUiMod', 'ui.bootstrap'
                 $scope.$apply(); // needed
             }
 
+            $scope.fetchMoreData = function() {
+                console.log('fetchMoreData');
+                self.offset += self.limit;
+                $scope.fetchData();
+            }
+
             this.init();
         }
     };
@@ -421,6 +436,63 @@ angular.module('egGridMod', ['egCoreMod', 'egListMod', 'egUiMod', 'ui.bootstrap'
     };
 })
 
+.factory('egGridData', ['egNet','egAuth',
+
+    function(egNet, egAuth) {
+        var service = {};
+
+        service.configure = function(params) {
+            console.log('configure');
+            service.idlClass = params.idlClass;
+            service.query = params.query;
+            service.list = params.list;
+        }
+
+        service.reset = function() {
+            service.list.resetPageData();
+            service._rev = Math.random();
+        }
+
+        service.revision = function() {
+            return service._rev;
+        }
+
+        service.get = function(index, count, success) {
+            var queryFields = {}
+            console.log('service.get()');
+            index -= 1; // we like zero-based
+            if (index < 0) return success([]); // hrm??
+
+            angular.forEach(service.list.allColumns, function(field) {
+                if (service.list.displayColumns[field.name])
+                    queryFields[field.name] = field.path || field.name;
+            });
+
+            egNet.request(
+                'open-ils.fielder',
+                'open-ils.fielder.flattened_search',
+                egAuth.token(), service.idlClass, queryFields,
+                service.query,
+                {   sort : service.sort,
+                    limit : count,
+                    offset : index
+                }
+            ).then( 
+                function() { // oncomplete
+                    console.log(index + ' : ' + count);
+                    success(service.list.items.slice(index, index + count)); 
+                }, 
+                null, // onerror
+                function(item) { // onmessage
+                    service.list.items.push(item);
+                }
+            );
+        }
+
+        return service;
+    }
+])
+
 /** Simplified dnd directives for grid column controls.
  *  Extract these out if the can be made generic enough
  */
@@ -445,11 +517,11 @@ angular.module('egGridMod', ['egCoreMod', 'egListMod', 'egUiMod', 'ui.bootstrap'
         require : '^egGrid',
         link : function(scope, element, attrs, egGridCtrl) {
             element.bind('dragover', function(e) {
-                console.log('dragover');
                 e.stopPropagation();
                 e.preventDefault();
                 //e.dataTransfer.dropEffect = 'copy';
                 var col = angular.element(e.target).attr('column');
+                console.log('dragover ' + col);
                 egGridCtrl.onColumnDragOver(col);
             });
         }
index b3ce709..757dc2f 100644 (file)
@@ -69,6 +69,14 @@ angular.module('egListMod', ['egCoreMod'])
             return item; 
         }
 
+        // 0-based column position
+        this.columnPosition = function(name) {
+            for (var i = 0; i < this.allColumns.length; i++) {
+                if (this.allColumns[i].name == name)
+                    return i;
+            }
+        }
+
         // returns true if item1 appears in the list before item2;
         // false otherwise.  this is slightly more efficient that
         // finding the position of each then comparing them.
@@ -86,6 +94,7 @@ angular.module('egListMod', ['egCoreMod'])
             return false;
         }
 
+        // 0-based position of item in the current data set
         this.indexOf = function(item) {
             var idx = this.indexValue(item);
             for (var i = 0; i < this.items.length; i++) {
diff --git a/Open-ILS/web/js/ui/default/staff/services/ui-scroll-jqlite.js b/Open-ILS/web/js/ui/default/staff/services/ui-scroll-jqlite.js
new file mode 100644 (file)
index 0000000..29bc512
--- /dev/null
@@ -0,0 +1,221 @@
+'use strict';
+
+angular.module('ui.scroll.jqlite', ['ui.scroll']).service('jqLiteExtras', [
+  '$log', '$window', function(console, window) {
+    return {
+      registerFor: function(element) {
+        var convertToPx, css, getMeasurements, getStyle, getWidthHeight, isWindow, scrollTo;
+        css = angular.element.prototype.css;
+        element.prototype.css = function(name, value) {
+          var elem, self;
+          self = this;
+          elem = self[0];
+          if (!(!elem || elem.nodeType === 3 || elem.nodeType === 8 || !elem.style)) {
+            return css.call(self, name, value);
+          }
+        };
+        isWindow = function(obj) {
+          return obj && obj.document && obj.location && obj.alert && obj.setInterval;
+        };
+        scrollTo = function(self, direction, value) {
+          var elem, method, preserve, prop, _ref;
+          elem = self[0];
+          _ref = {
+            top: ['scrollTop', 'pageYOffset', 'scrollLeft'],
+            left: ['scrollLeft', 'pageXOffset', 'scrollTop']
+          }[direction], method = _ref[0], prop = _ref[1], preserve = _ref[2];
+          if (isWindow(elem)) {
+            if (angular.isDefined(value)) {
+              return elem.scrollTo(self[preserve].call(self), value);
+            } else {
+              if (prop in elem) {
+                return elem[prop];
+              } else {
+                return elem.document.documentElement[method];
+              }
+            }
+          } else {
+            if (angular.isDefined(value)) {
+              return elem[method] = value;
+            } else {
+              return elem[method];
+            }
+          }
+        };
+        if (window.getComputedStyle) {
+          getStyle = function(elem) {
+            return window.getComputedStyle(elem, null);
+          };
+          convertToPx = function(elem, value) {
+            return parseFloat(value);
+          };
+        } else {
+          getStyle = function(elem) {
+            return elem.currentStyle;
+          };
+          convertToPx = function(elem, value) {
+            var core_pnum, left, result, rnumnonpx, rs, rsLeft, style;
+            core_pnum = /[+-]?(?:\d*\.|)\d+(?:[eE][+-]?\d+|)/.source;
+            rnumnonpx = new RegExp('^(' + core_pnum + ')(?!px)[a-z%]+$', 'i');
+            if (!rnumnonpx.test(value)) {
+              return parseFloat(value);
+            } else {
+              style = elem.style;
+              left = style.left;
+              rs = elem.runtimeStyle;
+              rsLeft = rs && rs.left;
+              if (rs) {
+                rs.left = style.left;
+              }
+              style.left = value;
+              result = style.pixelLeft;
+              style.left = left;
+              if (rsLeft) {
+                rs.left = rsLeft;
+              }
+              return result;
+            }
+          };
+        }
+        getMeasurements = function(elem, measure) {
+          var base, borderA, borderB, computedMarginA, computedMarginB, computedStyle, dirA, dirB, marginA, marginB, paddingA, paddingB, _ref;
+          if (isWindow(elem)) {
+            base = document.documentElement[{
+              height: 'clientHeight',
+              width: 'clientWidth'
+            }[measure]];
+            return {
+              base: base,
+              padding: 0,
+              border: 0,
+              margin: 0
+            };
+          }
+          _ref = {
+            width: [elem.offsetWidth, 'Left', 'Right'],
+            height: [elem.offsetHeight, 'Top', 'Bottom']
+          }[measure], base = _ref[0], dirA = _ref[1], dirB = _ref[2];
+          computedStyle = getStyle(elem);
+          paddingA = convertToPx(elem, computedStyle['padding' + dirA]) || 0;
+          paddingB = convertToPx(elem, computedStyle['padding' + dirB]) || 0;
+          borderA = convertToPx(elem, computedStyle['border' + dirA + 'Width']) || 0;
+          borderB = convertToPx(elem, computedStyle['border' + dirB + 'Width']) || 0;
+          computedMarginA = computedStyle['margin' + dirA];
+          computedMarginB = computedStyle['margin' + dirB];
+          marginA = convertToPx(elem, computedMarginA) || 0;
+          marginB = convertToPx(elem, computedMarginB) || 0;
+          return {
+            base: base,
+            padding: paddingA + paddingB,
+            border: borderA + borderB,
+            margin: marginA + marginB
+          };
+        };
+        getWidthHeight = function(elem, direction, measure) {
+          var computedStyle, measurements, result;
+          measurements = getMeasurements(elem, direction);
+          if (measurements.base > 0) {
+            return {
+              base: measurements.base - measurements.padding - measurements.border,
+              outer: measurements.base,
+              outerfull: measurements.base + measurements.margin
+            }[measure];
+          } else {
+            computedStyle = getStyle(elem);
+            result = computedStyle[direction];
+            if (result < 0 || result === null) {
+              result = elem.style[direction] || 0;
+            }
+            result = parseFloat(result) || 0;
+            return {
+              base: result - measurements.padding - measurements.border,
+              outer: result,
+              outerfull: result + measurements.padding + measurements.border + measurements.margin
+            }[measure];
+          }
+        };
+        return angular.forEach({
+          before: function(newElem) {
+            var children, elem, i, parent, self, _i, _ref;
+            self = this;
+            elem = self[0];
+            parent = self.parent();
+            children = parent.contents();
+            if (children[0] === elem) {
+              return parent.prepend(newElem);
+            } else {
+              for (i = _i = 1, _ref = children.length - 1; 1 <= _ref ? _i <= _ref : _i >= _ref; i = 1 <= _ref ? ++_i : --_i) {
+                if (children[i] === elem) {
+                  angular.element(children[i - 1]).after(newElem);
+                  return;
+                }
+              }
+              throw new Error('invalid DOM structure ' + elem.outerHTML);
+            }
+          },
+          height: function(value) {
+            var self;
+            self = this;
+            if (angular.isDefined(value)) {
+              if (angular.isNumber(value)) {
+                value = value + 'px';
+              }
+              return css.call(self, 'height', value);
+            } else {
+              return getWidthHeight(this[0], 'height', 'base');
+            }
+          },
+          outerHeight: function(option) {
+            return getWidthHeight(this[0], 'height', option ? 'outerfull' : 'outer');
+          },
+          offset: function(value) {
+            var box, doc, docElem, elem, self, win;
+            self = this;
+            if (arguments.length) {
+              if (value === void 0) {
+                return self;
+              } else {
+                return value;
+
+              }
+            }
+            box = {
+              top: 0,
+              left: 0
+            };
+            elem = self[0];
+            doc = elem && elem.ownerDocument;
+            if (!doc) {
+              return;
+            }
+            docElem = doc.documentElement;
+            if (elem.getBoundingClientRect) {
+              box = elem.getBoundingClientRect();
+            }
+            win = doc.defaultView || doc.parentWindow;
+            return {
+              top: box.top + (win.pageYOffset || docElem.scrollTop) - (docElem.clientTop || 0),
+              left: box.left + (win.pageXOffset || docElem.scrollLeft) - (docElem.clientLeft || 0)
+            };
+          },
+          scrollTop: function(value) {
+            return scrollTo(this, 'top', value);
+          },
+          scrollLeft: function(value) {
+            return scrollTo(this, 'left', value);
+          }
+        }, function(value, key) {
+          if (!element.prototype[key]) {
+            return element.prototype[key] = value;
+          }
+        });
+      }
+    };
+  }
+]).run([
+  '$log', '$window', 'jqLiteExtras', function(console, window, jqLiteExtras) {
+    if (!window.jQuery) {
+      return jqLiteExtras.registerFor(angular.element);
+    }
+  }
+]);
diff --git a/Open-ILS/web/js/ui/default/staff/services/ui-scroll.js b/Open-ILS/web/js/ui/default/staff/services/ui-scroll.js
new file mode 100644 (file)
index 0000000..a52430f
--- /dev/null
@@ -0,0 +1,473 @@
+'use strict';
+/*
+
+ List of used element methods available in JQuery but not in JQuery Lite
+
+ element.before(elem)
+ element.height()
+ element.outerHeight(true)
+ element.height(value) = only for Top/Bottom padding elements
+ element.scrollTop()
+ element.scrollTop(value)
+ */
+
+angular.module('ui.scroll', []).directive('ngScrollViewport', [
+        '$log', function() {
+            return {
+                controller: [
+                    '$scope', '$element', function(scope, element) {
+                        return element;
+                    }
+                ]
+            };
+        }
+    ]).directive('ngScroll', [
+        '$log', '$injector', '$rootScope', '$timeout', function(console, $injector, $rootScope, $timeout) {
+            return {
+                require: ['?^ngScrollViewport'],
+                transclude: 'element',
+                priority: 1000,
+                terminal: true,
+                compile: function(elementTemplate, attr, linker) {
+                    return function($scope, element, $attr, controllers) {
+                        var adapter, adjustBuffer, adjustRowHeight, bof, bottomVisiblePos, buffer, bufferPadding, bufferSize, clipBottom, clipTop, datasource, datasourceName, enqueueFetch, eof, eventListener, fetch, finalize, first, insert, isDatasource, isLoading, itemName, loading, match, next, pending, reload, removeFromBuffer, resizeHandler, scrollHandler, scrollHeight, shouldLoadBottom, shouldLoadTop, tempScope, topVisiblePos, viewport;
+                        match = $attr.ngScroll.match(/^\s*(\w+)\s+in\s+(\w+)\s*$/);
+                        if (!match) {
+                            throw new Error('Expected ngScroll in form of "item_ in _datasource_" but got "' + $attr.ngScroll + '"');
+                        }
+                        itemName = match[1];
+                        datasourceName = match[2];
+                        isDatasource = function(datasource) {
+                            return angular.isObject(datasource) && datasource.get && angular.isFunction(datasource.get);
+                        };
+                        datasource = $scope[datasourceName];
+                        if (!isDatasource(datasource)) {
+                            datasource = $injector.get(datasourceName);
+                            if (!isDatasource(datasource)) {
+                                throw new Error(datasourceName + ' is not a valid datasource');
+                            }
+                        }
+                        bufferSize = Math.max(3, +$attr.bufferSize || 10);
+                        bufferPadding = function() {
+                            return viewport.height() * Math.max(0.1, +$attr.padding || 0.1);
+                        };
+                        scrollHeight = function(elem) {
+                            console.log(elem[0].scrollHeight, elem[0].document);
+                            if( !elem[0].scrollHeight && !elem[0].document ) {
+                                throw new Error('Could not determine scrollHeight of your viewport; make sure it has a constrained height (not height:auto)');
+                            }
+                            return elem[0].scrollHeight || elem[0].document.documentElement.scrollHeight;
+                        };
+                        adapter = null;
+                        linker(tempScope = $scope.$new(), function(template) {
+                            var bottomPadding, createPadding, padding, repeaterType, topPadding, viewport;
+                            repeaterType = template[0].localName;
+                            if (repeaterType === 'dl') {
+                                throw new Error('ng-scroll directive does not support <' + template[0].localName + '> as a repeating tag: ' + template[0].outerHTML);
+                            }
+                            if (repeaterType !== 'li' && repeaterType !== 'tr') {
+                                repeaterType = 'div';
+                            }
+                            viewport = controllers[0] || angular.element(window);
+                            viewport.css({
+                                'overflow-y': 'auto',
+                                'display': 'block'
+                            });
+                            padding = function(repeaterType) {
+                                var div, result, table;
+                                switch (repeaterType) {
+                                    case 'tr':
+                                        table = angular.element('<table><tr><td><div></div></td></tr></table>');
+                                        div = table.find('div');
+                                        result = table.find('tr');
+                                        result.paddingHeight = function() {
+                                            return div.height.apply(div, arguments);
+                                        };
+                                        return result;
+                                    default:
+                                        result = angular.element('<' + repeaterType + '></' + repeaterType + '>');
+                                        result.paddingHeight = result.height;
+                                        return result;
+                                }
+                            };
+                            createPadding = function(padding, element, direction) {
+                                element[{
+                                    top: 'before',
+                                    bottom: 'after'
+                                }[direction]](padding);
+                                return {
+                                    paddingHeight: function() {
+                                        return padding.paddingHeight.apply(padding, arguments);
+                                    },
+                                    insert: function(element) {
+                                        return padding[{
+                                            top: 'after',
+                                            bottom: 'before'
+                                        }[direction]](element);
+                                    }
+                                };
+                            };
+                            topPadding = createPadding(padding(repeaterType), element, 'top');
+                            bottomPadding = createPadding(padding(repeaterType), element, 'bottom');
+                            tempScope.$destroy();
+                            return adapter = {
+                                viewport: viewport,
+                                topPadding: topPadding.paddingHeight,
+                                bottomPadding: bottomPadding.paddingHeight,
+                                append: bottomPadding.insert,
+                                prepend: topPadding.insert,
+                                bottomDataPos: function() {
+                                    return scrollHeight(viewport) - bottomPadding.paddingHeight();
+                                },
+                                topDataPos: function() {
+                                    return topPadding.paddingHeight();
+                                }
+                            };
+                        });
+                        viewport = adapter.viewport;
+                        first = 1;
+                        next = 1;
+                        buffer = [];
+                        pending = [];
+                        eof = false;
+                        bof = false;
+                        loading = datasource.loading || function() {};
+                        isLoading = false;
+                        removeFromBuffer = function(start, stop) {
+                            var i, _i;
+                            for (i = _i = start; start <= stop ? _i < stop : _i > stop; i = start <= stop ? ++_i : --_i) {
+                                buffer[i].scope.$destroy();
+                                buffer[i].element.remove();
+                            }
+                            return buffer.splice(start, stop - start);
+                        };
+                        reload = function() {
+                            first = 1;
+                            next = 1;
+                            removeFromBuffer(0, buffer.length);
+                            adapter.topPadding(0);
+                            adapter.bottomPadding(0);
+                            pending = [];
+                            eof = false;
+                            bof = false;
+                            return adjustBuffer(false);
+                        };
+                        bottomVisiblePos = function() {
+                            return viewport.scrollTop() + viewport.height();
+                        };
+                        topVisiblePos = function() {
+                            return viewport.scrollTop();
+                        };
+                        shouldLoadBottom = function() {
+                            return !eof && adapter.bottomDataPos() < bottomVisiblePos() + bufferPadding();
+                        };
+                        clipBottom = function() {
+                            var bottomHeight, i, itemHeight, overage, _i, _ref;
+                            bottomHeight = 0;
+                            overage = 0;
+                            for (i = _i = _ref = buffer.length - 1; _ref <= 0 ? _i <= 0 : _i >= 0; i = _ref <= 0 ? ++_i : --_i) {
+                                itemHeight = buffer[i].element.outerHeight(true);
+                                if (adapter.bottomDataPos() - bottomHeight - itemHeight > bottomVisiblePos() + bufferPadding()) {
+                                    bottomHeight += itemHeight;
+                                    overage++;
+                                    eof = false;
+                                } else {
+                                    break;
+                                }
+                            }
+                            if (overage > 0) {
+                                adapter.bottomPadding(adapter.bottomPadding() + bottomHeight);
+                                removeFromBuffer(buffer.length - overage, buffer.length);
+                                next -= overage;
+                                return console.log('clipped off bottom ' + overage + ' bottom padding ' + (adapter.bottomPadding()));
+                            }
+                        };
+                        shouldLoadTop = function() {
+                            return !bof && (adapter.topDataPos() > topVisiblePos() - bufferPadding());
+                        };
+                        clipTop = function() {
+                            var item, itemHeight, overage, topHeight, _i, _len;
+                            topHeight = 0;
+                            overage = 0;
+                            for (_i = 0, _len = buffer.length; _i < _len; _i++) {
+                                item = buffer[_i];
+                                itemHeight = item.element.outerHeight(true);
+                                if (adapter.topDataPos() + topHeight + itemHeight < topVisiblePos() - bufferPadding()) {
+                                    topHeight += itemHeight;
+                                    overage++;
+                                    bof = false;
+                                } else {
+                                    break;
+                                }
+                            }
+                            if (overage > 0) {
+                                adapter.topPadding(adapter.topPadding() + topHeight);
+                                removeFromBuffer(0, overage);
+                                first += overage;
+                                return console.log('clipped off top ' + overage + ' top padding ' + (adapter.topPadding()));
+                            }
+                        };
+                        enqueueFetch = function(direction, scrolling) {
+                            if (!isLoading) {
+                                isLoading = true;
+                                loading(true);
+                            }
+                            if (pending.push(direction) === 1) {
+                                return fetch(scrolling);
+                            }
+                        };
+                        insert = function(index, item) {
+                            var itemScope, toBeAppended, wrapper;
+                            itemScope = $scope.$new();
+                            itemScope[itemName] = item;
+                            toBeAppended = index > first;
+                            itemScope.$index = index;
+                            if (toBeAppended) {
+                                itemScope.$index--;
+                            }
+                            wrapper = {
+                                scope: itemScope
+                            };
+                            linker(itemScope, function(clone) {
+                                wrapper.element = clone;
+                                if (toBeAppended) {
+                                    if (index === next) {
+                                        adapter.append(clone);
+                                        return buffer.push(wrapper);
+                                    } else {
+                                        buffer[index - first].element.after(clone);
+                                        return buffer.splice(index - first + 1, 0, wrapper);
+                                    }
+                                } else {
+                                    adapter.prepend(clone);
+                                    return buffer.unshift(wrapper);
+                                }
+                            });
+                            return {
+                                appended: toBeAppended,
+                                wrapper: wrapper
+                            };
+                        };
+                        adjustRowHeight = function(appended, wrapper) {
+                            var newHeight;
+                            if (appended) {
+                                return adapter.bottomPadding(Math.max(0, adapter.bottomPadding() - wrapper.element.outerHeight(true)));
+                            } else {
+                                newHeight = adapter.topPadding() - wrapper.element.outerHeight(true);
+                                if (newHeight >= 0) {
+                                    return adapter.topPadding(newHeight);
+                                } else {
+                                    return viewport.scrollTop(viewport.scrollTop() + wrapper.element.outerHeight(true));
+                                }
+                            }
+                        };
+                        adjustBuffer = function(scrolling, newItems, finalize) {
+                            var doAdjustment;
+                            doAdjustment = function() {
+                                console.log('top {actual=' + (adapter.topDataPos()) + ' visible from=' + (topVisiblePos()) + ' bottom {visible through=' + (bottomVisiblePos()) + ' actual=' + (adapter.bottomDataPos()) + '}');
+                                if (shouldLoadBottom()) {
+                                    enqueueFetch(true, scrolling);
+                                } else {
+                                    if (shouldLoadTop()) {
+                                        enqueueFetch(false, scrolling);
+                                    }
+                                }
+                                if (finalize) {
+                                    return finalize();
+                                }
+                            };
+                            if (newItems) {
+                                return $timeout(function() {
+                                    var row, _i, _len;
+                                    for (_i = 0, _len = newItems.length; _i < _len; _i++) {
+                                        row = newItems[_i];
+                                        adjustRowHeight(row.appended, row.wrapper);
+                                    }
+                                    return doAdjustment();
+                                });
+                            } else {
+                                return doAdjustment();
+                            }
+                        };
+                        finalize = function(scrolling, newItems) {
+                            return adjustBuffer(scrolling, newItems, function() {
+                                pending.shift();
+                                if (pending.length === 0) {
+                                    isLoading = false;
+                                    return loading(false);
+                                } else {
+                                    return fetch(scrolling);
+                                }
+                            });
+                        };
+                        fetch = function(scrolling) {
+                            var direction;
+                            direction = pending[0];
+                            if (direction) {
+                                if (buffer.length && !shouldLoadBottom()) {
+                                    return finalize(scrolling);
+                                } else {
+                                    return datasource.get(next, bufferSize, function(result) {
+                                        var item, newItems, _i, _len;
+                                        newItems = [];
+                                        if (result.length === 0) {
+                                            eof = true;
+                                            adapter.bottomPadding(0);
+                                            console.log('appended: requested ' + bufferSize + ' records starting from ' + next + ' recieved: eof');
+                                        } else {
+                                            clipTop();
+                                            for (_i = 0, _len = result.length; _i < _len; _i++) {
+                                                item = result[_i];
+                                                newItems.push(insert(++next, item));
+                                            }
+                                            console.log('appended: requested ' + bufferSize + ' received ' + result.length + ' buffer size ' + buffer.length + ' first ' + first + ' next ' + next);
+                                        }
+                                        return finalize(scrolling, newItems);
+                                    });
+                                }
+                            } else {
+                                if (buffer.length && !shouldLoadTop()) {
+                                    return finalize(scrolling);
+                                } else {
+                                    return datasource.get(first - bufferSize, bufferSize, function(result) {
+                                        var i, newItems, _i, _ref;
+                                        newItems = [];
+                                        if (result.length === 0) {
+                                            bof = true;
+                                            adapter.topPadding(0);
+                                            console.log('prepended: requested ' + bufferSize + ' records starting from ' + (first - bufferSize) + ' recieved: bof');
+                                        } else {
+                                            clipBottom();
+                                            for (i = _i = _ref = result.length - 1; _ref <= 0 ? _i <= 0 : _i >= 0; i = _ref <= 0 ? ++_i : --_i) {
+                                                newItems.unshift(insert(--first, result[i]));
+                                            }
+                                            console.log('prepended: requested ' + bufferSize + ' received ' + result.length + ' buffer size ' + buffer.length + ' first ' + first + ' next ' + next);
+                                        }
+                                        return finalize(scrolling, newItems);
+                                    });
+                                }
+                            }
+                        };
+                        resizeHandler = function() {
+                            if (!$rootScope.$$phase && !isLoading) {
+                                adjustBuffer(false);
+                                return $scope.$apply();
+                            }
+                        };
+                        viewport.bind('resize', resizeHandler);
+                        scrollHandler = function() {
+                            if (!$rootScope.$$phase && !isLoading) {
+                                adjustBuffer(true);
+                                return $scope.$apply();
+                            }
+                        };
+                        viewport.bind('scroll', scrollHandler);
+                        $scope.$watch(datasource.revision, function() {
+                            return reload();
+                        });
+                        if (datasource.scope) {
+                            eventListener = datasource.scope.$new();
+                        } else {
+                            eventListener = $scope.$new();
+                        }
+                        $scope.$on('$destroy', function() {
+                            eventListener.$destroy();
+                            viewport.unbind('resize', resizeHandler);
+                            return viewport.unbind('scroll', scrollHandler);
+                        });
+                        eventListener.$on('update.items', function(event, locator, newItem) {
+                            var wrapper, _fn, _i, _len, _ref;
+                            if (angular.isFunction(locator)) {
+                                _fn = function(wrapper) {
+                                    return locator(wrapper.scope);
+                                };
+                                for (_i = 0, _len = buffer.length; _i < _len; _i++) {
+                                    wrapper = buffer[_i];
+                                    _fn(wrapper);
+                                }
+                            } else {
+                                if ((0 <= (_ref = locator - first - 1) && _ref < buffer.length)) {
+                                    buffer[locator - first - 1].scope[itemName] = newItem;
+                                }
+                            }
+                            return null;
+                        });
+                        eventListener.$on('delete.items', function(event, locator) {
+                            var i, item, temp, wrapper, _fn, _i, _j, _k, _len, _len1, _len2, _ref;
+                            if (angular.isFunction(locator)) {
+                                temp = [];
+                                for (_i = 0, _len = buffer.length; _i < _len; _i++) {
+                                    item = buffer[_i];
+                                    temp.unshift(item);
+                                }
+                                _fn = function(wrapper) {
+                                    if (locator(wrapper.scope)) {
+                                        removeFromBuffer(temp.length - 1 - i, temp.length - i);
+                                        return next--;
+                                    }
+                                };
+                                for (i = _j = 0, _len1 = temp.length; _j < _len1; i = ++_j) {
+                                    wrapper = temp[i];
+                                    _fn(wrapper);
+                                }
+                            } else {
+                                if ((0 <= (_ref = locator - first - 1) && _ref < buffer.length)) {
+                                    removeFromBuffer(locator - first - 1, locator - first);
+                                    next--;
+                                }
+                            }
+                            for (i = _k = 0, _len2 = buffer.length; _k < _len2; i = ++_k) {
+                                item = buffer[i];
+                                item.scope.$index = first + i;
+                            }
+                            return adjustBuffer(false);
+                        });
+                        return eventListener.$on('insert.item', function(event, locator, item) {
+                            var i, inserted, temp, wrapper, _fn, _i, _j, _k, _len, _len1, _len2, _ref;
+                            inserted = [];
+                            if (angular.isFunction(locator)) {
+                                temp = [];
+                                for (_i = 0, _len = buffer.length; _i < _len; _i++) {
+                                    item = buffer[_i];
+                                    temp.unshift(item);
+                                }
+                                _fn = function(wrapper) {
+                                    var j, newItems, _k, _len2, _results;
+                                    if (newItems = locator(wrapper.scope)) {
+                                        insert = function(index, newItem) {
+                                            insert(index, newItem);
+                                            return next++;
+                                        };
+                                        if (angular.isArray(newItems)) {
+                                            _results = [];
+                                            for (j = _k = 0, _len2 = newItems.length; _k < _len2; j = ++_k) {
+                                                item = newItems[j];
+                                                _results.push(inserted.push(insert(i + j, item)));
+                                            }
+                                            return _results;
+                                        } else {
+                                            return inserted.push(insert(i, newItems));
+                                        }
+                                    }
+                                };
+                                for (i = _j = 0, _len1 = temp.length; _j < _len1; i = ++_j) {
+                                    wrapper = temp[i];
+                                    _fn(wrapper);
+                                }
+                            } else {
+                                if ((0 <= (_ref = locator - first - 1) && _ref < buffer.length)) {
+                                    inserted.push(insert(locator, item));
+                                    next++;
+                                }
+                            }
+                            for (i = _k = 0, _len2 = buffer.length; _k < _len2; i = ++_k) {
+                                item = buffer[i];
+                                item.scope.$index = first + i;
+                            }
+                            return adjustBuffer(false, inserted);
+                        });
+                    };
+                }
+            };
+        }
+    ]);