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
|
window.Scroller = root = {}
#
# activatedElement is different from document.activeElement -- the latter seems to be reserved mostly for
# input elements. This mechanism allows us to decide whether to scroll a div or to scroll the whole document.
#
activatedElement = null
settings = null
root.init = (frontendSettings) ->
settings = frontendSettings
handlerStack.push DOMActivate: -> activatedElement = event.target
scrollProperties =
x: {
axisName: 'scrollLeft'
max: 'scrollWidth'
viewSize: 'clientHeight'
}
y: {
axisName: 'scrollTop'
max: 'scrollHeight'
viewSize: 'clientWidth'
}
getDimension = (el, direction, name) ->
# the clientSizes of the body are the dimensions of the entire page, but the viewport should only be the
# part visible through the window
if name is 'viewSize' and el is document.body
if direction is 'x' then window.innerWidth else window.innerHeight
else
el[scrollProperties[direction][name]]
# Chrome does not report scrollHeight accurately for nodes with pseudo-elements of height 0 (bug 110149).
# Therefore we cannot figure out if we have scrolled to the bottom of an element by testing if scrollTop +
# clientHeight == scrollHeight. So just try to increase scrollTop blindly -- if it fails we know we have
# reached the end of the content.
ensureScrollChange = (direction, changeFn) ->
axisName = scrollProperties[direction].axisName
element = activatedElement
progress = 0
loop
oldScrollValue = element[axisName]
# Elements with `overflow: hidden` should not be scrolled.
overflow = window.getComputedStyle(element).getPropertyValue("overflow-#{direction}")
changeFn(element, axisName) unless overflow == "hidden"
progress += element[axisName] - oldScrollValue
break unless element[axisName] == oldScrollValue && element != document.body
# we may have an orphaned element. if so, just scroll the body element.
element = element.parentElement || document.body
# if the activated element has been scrolled completely offscreen, subsequent changes in its scroll
# position will not provide any more visual feedback to the user. therefore we deactivate it so that
# subsequent scrolls only move the parent element.
rect = activatedElement.getBoundingClientRect()
if (rect.bottom < 0 || rect.top > window.innerHeight || rect.right < 0 || rect.left > window.innerWidth)
activatedElement = element
# Return the amount by which the scroll position has changed.
return progress
# Scroll by a relative amount in some direction, possibly smoothly.
# The constants below seem to roughly match chrome's scroll speeds for both short and long scrolls.
# TODO(smblott) For very-long scrolls, chrome implements a soft landing; we don't.
doScrollBy = do ->
interval = 10 # Update interval (in ms).
duration = 120 # This must be a multiple of interval (also in ms).
fudgeFactor = 25
timer = null
clearTimer = ->
if timer
clearInterval timer
timer = null
# Allow a bit longer for longer scrolls.
calculateExtraDuration = (amount) ->
extra = fudgeFactor * Math.log Math.abs amount
# Ensure we have a multiple of interval.
return interval * Math.round (extra / interval)
scroller = (direction,amount) ->
return ensureScrollChange direction, (element, axisName) -> element[axisName] += amount
(direction,amount,wantSmooth) ->
clearTimer()
unless wantSmooth and settings.get "smoothScroll"
scroller direction, amount
return
requiredTicks = (duration + calculateExtraDuration amount) / interval
# Round away from 0, so that we don't leave any requested scroll amount unscrolled.
rounder = (if 0 <= amount then Math.ceil else Math.floor)
delta = rounder(amount / requiredTicks)
ticks = 0
ticker = ->
# If we haven't scrolled by the expected amount, then we've hit the top, bottom or side of the activated
# element, so stop scrolling.
if scroller(direction, delta) != delta or ++ticks == requiredTicks
clearTimer()
timer = setInterval ticker, interval
ticker()
# scroll the active element in :direction by :amount * :factor.
# :factor is needed because :amount can take on string values, which scrollBy converts to element dimensions.
root.scrollBy = (direction, amount, factor = 1) ->
# if this is called before domReady, just use the window scroll function
if (!document.body and amount instanceof Number)
if (direction == "x")
window.scrollBy(amount, 0)
else
window.scrollBy(0, amount)
return
if (!activatedElement || !isRendered(activatedElement))
activatedElement = document.body
if Utils.isString amount
elementAmount = getDimension activatedElement, direction, amount
else
elementAmount = amount
elementAmount *= factor
doScrollBy direction, elementAmount, true
root.scrollTo = (direction, pos, wantSmooth=false) ->
return unless document.body
if (!activatedElement || !isRendered(activatedElement))
activatedElement = document.body
if Utils.isString pos
elementPos = getDimension activatedElement, direction, pos
else
elementPos = pos
axisName = scrollProperties[direction].axisName
elementAmount = elementPos - activatedElement[axisName]
doScrollBy direction, elementAmount, wantSmooth
# TODO refactor and put this together with the code in getVisibleClientRect
isRendered = (element) ->
computedStyle = window.getComputedStyle(element, null)
return !(computedStyle.getPropertyValue("visibility") != "visible" ||
computedStyle.getPropertyValue("display") == "none")
|