aboutsummaryrefslogtreecommitdiffstats
path: root/catalog-ui/app/third-party/ng-infinite-scroll/src/infinite-scroll.coffee
blob: cf2f90fc19ad3024e4bb4842e1f5047a1e4a9769 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
angular.module('infinite-scroll', [])
  .value('THROTTLE_MILLISECONDS', null)
  .directive 'infiniteScroll', [
    '$rootScope', '$window', '$interval', 'THROTTLE_MILLISECONDS',
($rootScope, $window, $interval, THROTTLE_MILLISECONDS) ->
  scope:
    infiniteScroll: '&'
    infiniteScrollContainer: '='
    infiniteScrollDistance: '='
    infiniteScrollDisabled: '='
    infiniteScrollUseDocumentBottom: '=',
    infiniteScrollListenForEvent: '@'

  link: (scope, elem, attrs) ->
    windowElement = angular.element($window)

    scrollDistance = null
    scrollEnabled = null
    checkWhenEnabled = null
    container = null
    immediateCheck = true
    useDocumentBottom = false
    unregisterEventListener = null
    checkInterval = false

    height = (elem) ->
      elem = elem[0] or elem

      if isNaN(elem.offsetHeight) then elem.document.documentElement.clientHeight else elem.offsetHeight

    offsetTop = (elem) ->
      if not elem[0].getBoundingClientRect or elem.css('none')
        return

      elem[0].getBoundingClientRect().top + pageYOffset(elem)

    pageYOffset = (elem) ->
      elem = elem[0] or elem

      if isNaN(window.pageYOffset) then elem.document.documentElement.scrollTop else elem.ownerDocument.defaultView.pageYOffset

    # infinite-scroll specifies a function to call when the window,
    # or some other container specified by infinite-scroll-container,
    # is scrolled within a certain range from the bottom of the
    # document. It is recommended to use infinite-scroll-disabled
    # with a boolean that is set to true when the function is
    # called in order to throttle the function call.
    handler = ->
      if container == windowElement
        containerBottom = height(container) + pageYOffset(container[0].document.documentElement)
        elementBottom = offsetTop(elem) + height(elem)
      else
        containerBottom = height(container)
        containerTopOffset = 0
        if offsetTop(container) != undefined
          containerTopOffset = offsetTop(container)
        elementBottom = offsetTop(elem) - containerTopOffset + height(elem)

      if(useDocumentBottom)
        elementBottom = height((elem[0].ownerDocument || elem[0].document).documentElement)

      remaining = elementBottom - containerBottom
      shouldScroll = remaining <= height(container) * scrollDistance + 1

      if shouldScroll
        checkWhenEnabled = true

        if scrollEnabled
          if scope.$$phase || $rootScope.$$phase
            scope.infiniteScroll()
          else
            scope.$apply(scope.infiniteScroll)
      else
        if checkInterval then $interval.cancel checkInterval
        checkWhenEnabled = false

    # The optional THROTTLE_MILLISECONDS configuration value specifies
    # a minimum time that should elapse between each call to the
    # handler. N.b. the first call the handler will be run
    # immediately, and the final call will always result in the
    # handler being called after the `wait` period elapses.
    # A slimmed down version of underscore's implementation.
    throttle = (func, wait) ->
      timeout = null
      previous = 0
      later = ->
        previous = new Date().getTime()
        $interval.cancel(timeout)
        timeout = null
        func.call()

      return ->
        now = new Date().getTime()
        remaining = wait - (now - previous)
        if remaining <= 0
          $interval.cancel(timeout)
          timeout = null
          previous = now
          func.call()
        else timeout = $interval(later, remaining, 1) unless timeout

    if THROTTLE_MILLISECONDS?
      handler = throttle(handler, THROTTLE_MILLISECONDS)

    scope.$on '$destroy', ->
      container.unbind 'scroll', handler
      if unregisterEventListener?
        unregisterEventListener()
        unregisterEventListener = null
      if checkInterval
        $interval.cancel checkInterval

    # infinite-scroll-distance specifies how close to the bottom of the page
    # the window is allowed to be before we trigger a new scroll. The value
    # provided is multiplied by the container height; for example, to load
    # more when the bottom of the page is less than 3 container heights away,
    # specify a value of 3. Defaults to 0.
    handleInfiniteScrollDistance = (v) ->
      scrollDistance = parseFloat(v) or 0

    scope.$watch 'infiniteScrollDistance', handleInfiniteScrollDistance
    # If I don't explicitly call the handler here, tests fail. Don't know why yet.
    handleInfiniteScrollDistance scope.infiniteScrollDistance

    # infinite-scroll-disabled specifies a boolean that will keep the
    # infnite scroll function from being called; this is useful for
    # debouncing or throttling the function call. If an infinite
    # scroll is triggered but this value evaluates to true, then
    # once it switches back to false the infinite scroll function
    # will be triggered again.
    handleInfiniteScrollDisabled = (v) ->
      scrollEnabled = !v
      if scrollEnabled && checkWhenEnabled
        checkWhenEnabled = false
        handler()

    scope.$watch 'infiniteScrollDisabled', handleInfiniteScrollDisabled
    # If I don't explicitly call the handler here, tests fail. Don't know why yet.
    handleInfiniteScrollDisabled scope.infiniteScrollDisabled

    # use the bottom of the document instead of the element's bottom.
    # This useful when the element does not have a height due to its
    # children being absolute positioned.
    handleInfiniteScrollUseDocumentBottom = (v) ->
      useDocumentBottom = v

    scope.$watch 'infiniteScrollUseDocumentBottom', handleInfiniteScrollUseDocumentBottom
    handleInfiniteScrollUseDocumentBottom scope.infiniteScrollUseDocumentBottom

    # infinite-scroll-container sets the container which we want to be
    # infinte scrolled, instead of the whole window. Must be an
    # Angular or jQuery element, or, if jQuery is loaded,
    # a jQuery selector as a string.
    changeContainer = (newContainer) ->
      if container?
        container.unbind 'scroll', handler

      container = newContainer
      if newContainer?
        container.bind 'scroll', handler

    changeContainer windowElement

    if scope.infiniteScrollListenForEvent
      unregisterEventListener = $rootScope.$on scope.infiniteScrollListenForEvent, handler

    handleInfiniteScrollContainer = (newContainer) ->
      # TODO: For some reason newContainer is sometimes null instead
      # of the empty array, which Angular is supposed to pass when the
      # element is not defined
      # (https://github.com/sroze/ngInfiniteScroll/pull/7#commitcomment-5748431).
      # So I leave both checks.
      if (not newContainer?) or newContainer.length == 0
        return

      if newContainer.nodeType && newContainer.nodeType == 1
        newContainer = angular.element newContainer
      else if typeof newContainer.append == 'function'
        newContainer = angular.element newContainer[newContainer.length - 1]
      else if typeof newContainer == 'string'
        newContainer = angular.element document.querySelector newContainer

      if newContainer?
        changeContainer newContainer
      else
        throw new Error("invalid infinite-scroll-container attribute.")

    scope.$watch 'infiniteScrollContainer', handleInfiniteScrollContainer
    handleInfiniteScrollContainer(scope.infiniteScrollContainer or [])

    # infinite-scroll-parent establishes this element's parent as the
    # container infinitely scrolled instead of the whole window.
    if attrs.infiniteScrollParent?
      changeContainer angular.element elem.parent()

    # infinte-scoll-immediate-check sets whether or not run the
    # expression passed on infinite-scroll for the first time when the
    # directive first loads, before any actual scroll.
    if attrs.infiniteScrollImmediateCheck?
      immediateCheck = scope.$eval(attrs.infiniteScrollImmediateCheck)

    checkInterval = $interval (->
      if immediateCheck
        handler()
      $interval.cancel checkInterval
    )
]
if typeof module != "undefined" && typeof exports != "undefined" && module.exports == exports
  module.exports = 'infinite-scroll'