app = angular.module('plunker', ['ngAnimate', 'infiniteScroll'])

app.factory 'Item', [
  '$q', '$timeout',
  ($q, $timeout) ->
    new class Item
      id = 0
      load: (n, timeout=2000) =>
        deferred = $q.defer()

        fn = () =>
          items = []
          for i in [0...n]
            items.push
              id: id
              url: "http://lorempixel.com/100/100?#{id}"
            id += 1
          deferred.resolve items

        $timeout fn, timeout
        deferred.promise
]

app.controller 'ItemsCtrl', [
  '$scope', '$q', 'Item',
  ($scope, $q, Item) ->
    $scope.items = []
    $scope.loading = false

    $scope.loadMore = () =>
      $scope.loading = true
      Item.load(20)
        .then (items) =>
          Array.prototype.push.apply $scope.items, items
        .finally () =>
          $scope.loading = false
    
    $scope.options =
      disabled: false
      threshold: 0.1
      
    return
]
<!DOCTYPE html>
<html ng-app="plunker">

  <head>
    <meta charset="utf-8" />
    <title>AngularJS Plunker</title>
    <link rel="stylesheet" href="style.css" />
    <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.3.1/angular.js"></script>
    <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.3.1/angular-animate.js"></script>
    <script src="app.js"></script>
    <script src="infinite-scroll.js"></script>
  </head>

  <body>
    <div ng-controller="ItemsCtrl">
      <h1>Element</h1>
      disabled: <input type="checkbox" ng-model="options.disabled">
      <div id="box" infinite-scroll-container>
        <ul class="items" infinite-scroll="loadMore()" infinite-scroll-options="options">
          <li class="item fade" ng-repeat="item in items track by item.id">
            <img ng-src="{{item.url}}">
          </li>
          <li class="item fade loading" ng-show="loading">Loading...</li>
        </ul>
      </div>
    </div>
    <div ng-controller="ItemsCtrl">
      <h1>Window</h1>
      disabled: <input type="checkbox" ng-model="options.disabled">
      <ul class="items" infinite-scroll="loadMore()" infinite-scroll-options="options">
        <li class="item fade" ng-repeat="item in items track by item.id">
          <img ng-src="{{item.url}}">
        </li>
        <li class="item fade loading" ng-show="loading">Loading...</li>
      </ul>
    </div>
  </body>
  
</html>
#box {
  width: 100%;
  height: 400px;
  background-color: #888;
  overflow-y: scroll;
}

.items {
  list-style-type: none;
  margin: 0;
  padding: 5px;
  overflow: hidden;
}

.item {
  float: left;
  margin: 5px;
  width: 100px;
  height: 100px;
  background-color: #fdd;
}

.item img {
  display: block;
}

.item.loading {
  text-align: center;
  line-height: 100px;
}

.fade.ng-enter,
.fade.ng-leave {
  transition: all 0.3s ease-out;
}
.fade.ng-enter,
.fade.ng-leave-active {
  opacity: 0;
  position: relative;
  top: 10px;
}
.fade.ng-enter.ng-enter-active,
.fade.ng-leave {
  opacity: 1;
  top: 0;
}


mod = angular.module('infiniteScroll', [])


mod.controller 'InfiniteScrollWindowController', [
  '$window', '$document',
  ($window, $document) ->
    window = angular.element $window
    document = $document[0]
  
    @getElement = () =>
      window
      
    @getHeight= () =>
      $window.innerHeight

    @getBottom = () =>
      $window.pageYOffset + document.documentElement.clientHeight
  
    return
]

mod.controller 'InfiniteScrollContainerController', [
  '$window', '$element',
  ($window, $element) ->
    window = angular.element $window
    element = $element[0]

    @getElement = () =>
      $element
      
    @getHeight = () =>
      element.clientHeight
    
    @getBottom = () =>
      element.getBoundingClientRect().bottom + $window.pageYOffset

    return
]

mod.directive 'infiniteScrollContainer', [
  '$parse',
  ($parse) ->
    restrict: 'A'
    controller: 'InfiniteScrollContainerController'
]

mod.directive 'infiniteScroll', [
  '$window', '$parse', '$timeout', '$controller',
  ($window, $parse, $timeout, $controller) ->
    restrict: 'A'
    require: ['infiniteScroll', '?^infiniteScrollContainer']
    controller: ($scope, $element, $attrs) ->
      window = angular.element $window
      element = $element[0]
      fn = $parse $attrs.infiniteScroll
    
      options =
        disabled: false
        threshold: 0.1

      @options = (opts) =>
        if angular.isDefined(opts)
          angular.extend options, opts
          @check()
        options
      
      containerCtrl = null
      @setContainerCtrl = (ctrl) =>
        containerCtrl = ctrl if angular.isDefined(ctrl)
        containerCtrl
        
      @getBottom = () =>
        element.getBoundingClientRect().bottom + $window.pageYOffset

      @needMore = () =>
        return false if options.disabled
        elementBottom = @getBottom()
        containerBottom = containerCtrl.getBottom()
        remaining = elementBottom - containerBottom
        
        console.log "#{elementBottom} - #{containerBottom}"
        console.log "#{remaining} <= #{containerCtrl.getHeight() * options.threshold}"
        
        remaining <= (containerCtrl.getHeight() * options.threshold)

      lock = false
      
      @check = () =>
        if !lock and @needMore()
          lock = true
          $timeout =>
            promise = fn($scope)
            promise.then () =>
              lock = false
              $timeout @check
            promise.catch () =>
              lock = false

      return

    link: (scope, element, attrs, ctrls) ->
      thisCtrl = ctrls[0]
      containerCtrl = ctrls[1] or $controller('InfiniteScrollWindowController')

      containerElement = containerCtrl.getElement()
      thisCtrl.setContainerCtrl containerCtrl

      checker = () =>
        thisCtrl.check()
    
      containerElement.on 'scroll', checker
      scope.$on '$destroy', () =>
        containerElement.off 'scroll', checker
        
      if angular.isDefined(attrs.infiniteScrollOptions)
        options = $parse attrs.infiniteScrollOptions
        optionsWatch = () => options(scope)
        optionsChange = (value) =>
          if angular.isDefined(value)
            thisCtrl.options(value or {})
        scope.$watch optionsWatch, optionsChange, true  
      else
        checker()
        
      return
]