OLD | NEW |
| (Empty) |
1 // Copyright (c) 2011, the Dart project authors. Please see the AUTHORS file | |
2 // for details. All rights reserved. Use of this source code is governed by a | |
3 // BSD-style license that can be found in the LICENSE file. | |
4 | |
5 /** | |
6 * Implementation of a scrollbar for the custom scrolling behavior | |
7 * defined in [:Scroller:]. | |
8 */ | |
9 class Scrollbar implements ScrollListener { | |
10 /** | |
11 * The minimum size of scrollbars when not compressed. | |
12 */ | |
13 static final _MIN_SIZE = 30; | |
14 | |
15 /** | |
16 * The minimum compressed size of scrollbars. Scrollbars are compressed when | |
17 * the content is stretching past its boundaries. | |
18 */ | |
19 static final _MIN_COMPRESSED_SIZE = 8; | |
20 /** Padding in pixels to add above and bellow the scrollbar. */ | |
21 static final _PADDING_LENGTH = 10; | |
22 /** | |
23 * The amount of time to wait before hiding scrollbars after showing them. | |
24 * Measured in ms. | |
25 */ | |
26 static final _DISPLAY_TIME = 300; | |
27 static final DRAG_CLASS_NAME = 'drag'; | |
28 | |
29 Scroller _scroller; | |
30 Element _frame; | |
31 bool _scrollInProgress = false; | |
32 bool _scrollBarDragInProgressValue = false; | |
33 | |
34 /** | |
35 * Cached values of height and width. Keys will be 'height' and 'width' | |
36 * depending on if they are applied to vertical or horizontal scrollbar. | |
37 */ | |
38 Map<String, num> _cachedSize; | |
39 | |
40 /** | |
41 * This bound function will be used as the input to window.setTimeout when | |
42 * scheduling the hiding of the scrollbars. | |
43 */ | |
44 Function _boundHideFn; | |
45 | |
46 Element _verticalElement; | |
47 Element _horizontalElement; | |
48 | |
49 int _currentScrollStartMouse; | |
50 num _currentScrollStartOffset; | |
51 bool _currentScrollVertical; | |
52 num _currentScrollRatio; | |
53 num _timerId; | |
54 | |
55 bool _displayOnHover; | |
56 bool _hovering = false; | |
57 | |
58 Scrollbar(Scroller scroller, [displayOnHover = true]) : | |
59 _displayOnHover = displayOnHover, | |
60 _scroller = scroller, | |
61 _frame = scroller.getFrame(), | |
62 _cachedSize = new Map<String, num>() { | |
63 _boundHideFn = () { _showScrollbars(false); }; | |
64 } | |
65 | |
66 bool get _scrollBarDragInProgress() => _scrollBarDragInProgressValue; | |
67 | |
68 void set _scrollBarDragInProgress(bool value) { | |
69 _scrollBarDragInProgressValue = value; | |
70 _toggleClass(_verticalElement, DRAG_CLASS_NAME, | |
71 value && _currentScrollVertical); | |
72 _toggleClass(_horizontalElement, DRAG_CLASS_NAME, | |
73 value && !_currentScrollVertical); | |
74 } | |
75 | |
76 // TODO(jacobr): move this helper method into the DOM. | |
77 void _toggleClass(Element e, String className, bool enabled) { | |
78 if (enabled) { | |
79 if (!e.classes.contains(className)) { | |
80 e.classes.add(className); | |
81 } | |
82 } else { | |
83 e.classes.remove(className); | |
84 } | |
85 } | |
86 | |
87 /** | |
88 * Initializes elements and event handlers. Must be called after | |
89 * construction and before usage. | |
90 */ | |
91 void initialize() { | |
92 // Don't initialize if we have already been initialized. | |
93 // TODO(jacobr): remove this once bugs are fixed and enterDocument is only | |
94 // called once by each view. | |
95 if (_verticalElement != null) { | |
96 return; | |
97 } | |
98 _verticalElement = new Element.html( | |
99 '<div class="touch-scrollbar touch-scrollbar-vertical"></div>'); | |
100 _horizontalElement = new Element.html( | |
101 '<div class="touch-scrollbar touch-scrollbar-horizontal"></div>'); | |
102 _scroller.addScrollListener(this); | |
103 | |
104 Element scrollerEl = _scroller.getElement(); | |
105 | |
106 if (!Device.supportsTouch) { | |
107 _addEventListeners( | |
108 _verticalElement, _onStart, _onMove, _onEnd, _onEnd, true); | |
109 _addEventListeners( | |
110 _horizontalElement, _onStart, _onMove, _onEnd, _onEnd, true); | |
111 } | |
112 | |
113 _scroller.addScrollListener(this); | |
114 _showScrollbars(false); | |
115 _scroller.onScrollerStart.add(_onScrollerStart); | |
116 _scroller.onScrollerEnd.add(_onScrollerEnd); | |
117 if (_displayOnHover) { | |
118 // TODO(jacobr): rather than adding all these event listeners we could | |
119 // instead attach a single global event listener and let data in the | |
120 // DOM drive. | |
121 _frame.on.click.add((Event e) { | |
122 // Always focus on click as one of our children isn't all focused. | |
123 if (!_frame.contains(document.activeElement)) { | |
124 scrollerEl.focus(); | |
125 } | |
126 }, false); | |
127 _frame.on.mouseOver.add((Event e) { | |
128 final activeElement = document.activeElement; | |
129 // TODO(jacobr): don't steal focus from a child element or a truly | |
130 // focusable element. Only support stealing focus ffrom another | |
131 // element that was given fake focus. | |
132 if (activeElement is BodyElement || | |
133 (!_frame.contains(activeElement) && | |
134 activeElement is DivElement)) { | |
135 scrollerEl.focus(); | |
136 } | |
137 if (_hovering == false) { | |
138 _hovering = true; | |
139 _cancelTimeout(); | |
140 _showScrollbars(true); | |
141 refresh(); | |
142 } | |
143 }, false); | |
144 _frame.on.mouseOut.add((e) { | |
145 _hovering = false; | |
146 // Start hiding immediately if we aren't | |
147 // scrolling or already in the process of | |
148 // hidng the scrollbar | |
149 if (!_scrollInProgress && _timerId == null) { | |
150 _boundHideFn(); | |
151 } | |
152 }, false); | |
153 } | |
154 } | |
155 | |
156 void _onStart(UIEvent e) { | |
157 Element elementOver = e.target; | |
158 if (elementOver == _verticalElement || | |
159 elementOver == _horizontalElement) { | |
160 _currentScrollVertical = elementOver == _verticalElement; | |
161 if (_currentScrollVertical) { | |
162 _currentScrollStartMouse = e.pageY; | |
163 _currentScrollStartOffset = _scroller.getVerticalOffset(); | |
164 } else { | |
165 _currentScrollStartMouse = e.pageX; | |
166 _currentScrollStartOffset = _scroller.getHorizontalOffset(); | |
167 } | |
168 _refreshScrollRatio(); | |
169 _scrollBarDragInProgress = true; | |
170 _scroller._momentum.abort(); | |
171 e.stopPropagation(); | |
172 } | |
173 } | |
174 | |
175 void _refreshScrollRatio() { | |
176 Size contentSize = _scroller._getAdjustedContentSize(); | |
177 if (_currentScrollVertical) { | |
178 _refreshScrollRatioHelper( | |
179 _scroller._scrollSize.height, contentSize.height); | |
180 } else { | |
181 _refreshScrollRatioHelper(_scroller._scrollSize.width, | |
182 contentSize.width); | |
183 } | |
184 } | |
185 | |
186 | |
187 void _refreshScrollRatioHelper(num frameSize, num contentSize) { | |
188 num frameTravelDistance = frameSize - _defaultScrollSize( | |
189 frameSize, contentSize) -_PADDING_LENGTH * 2; | |
190 if (frameTravelDistance < 0.001) { | |
191 _currentScrollRatio = 0; | |
192 } else { | |
193 _currentScrollRatio = (contentSize - frameSize) / frameTravelDistance; | |
194 } | |
195 } | |
196 | |
197 void _onMove(UIEvent e) { | |
198 if (!_scrollBarDragInProgress) { | |
199 return; | |
200 } | |
201 _refreshScrollRatio(); | |
202 int coordinate = _currentScrollVertical ? e.pageY : e.pageX; | |
203 num delta = (coordinate - _currentScrollStartMouse) * _currentScrollRatio; | |
204 if (delta != 0) { | |
205 num x; | |
206 num y; | |
207 _currentScrollStartOffset -= delta; | |
208 if (_currentScrollVertical) { | |
209 x = _scroller.getHorizontalOffset(); | |
210 y = _currentScrollStartOffset.toInt(); | |
211 } else { | |
212 x = _currentScrollStartOffset.toInt(); | |
213 y = _scroller.getVerticalOffset(); | |
214 } | |
215 _scroller.setPosition(x, y); | |
216 } | |
217 _currentScrollStartMouse = coordinate; | |
218 } | |
219 | |
220 void _onEnd(UIEvent e) { | |
221 _scrollBarDragInProgress = false; | |
222 // TODO(jacobr): make scrollbar less tightly coupled to the scroller. | |
223 _scroller.onScrollerDragEnd.dispatch( | |
224 new Event(ScrollerEventType.DRAG_END)); | |
225 } | |
226 | |
227 | |
228 /** | |
229 * When scrolling ends, schedule a timeout to hide the scrollbars. | |
230 */ | |
231 void _onScrollerEnd(Event e) { | |
232 _cancelTimeout(); | |
233 _timerId = window.setTimeout(_boundHideFn, _DISPLAY_TIME); | |
234 _scrollInProgress = false; | |
235 } | |
236 void onScrollerMoved(num scrollX, num scrollY, bool decelerating) { | |
237 if (_scrollInProgress == false) { | |
238 // Display the scrollbar and then immediately prepare to hide it... | |
239 _onScrollerStart(null); | |
240 _onScrollerEnd(null); | |
241 } | |
242 updateScrollbars(scrollX, scrollY); | |
243 } | |
244 | |
245 void refresh() { | |
246 if (_scrollInProgress == false && _hovering == false) { | |
247 // No need to refresh if not visible. | |
248 return; | |
249 } | |
250 _scroller._resize(() { | |
251 updateScrollbars(_scroller.getHorizontalOffset(), | |
252 _scroller.getVerticalOffset()); | |
253 }); | |
254 } | |
255 | |
256 void updateScrollbars(num scrollX, num scrollY) { | |
257 Size contentSize = _scroller._getAdjustedContentSize(); | |
258 if (_scroller._shouldScrollHorizontally()) { | |
259 num scrollPercentX = _scroller.getHorizontalScrollPercent(scrollX); | |
260 _updateScrollbar(_horizontalElement, scrollX, scrollPercentX, | |
261 _scroller._scrollSize.width, | |
262 contentSize.width, 'right', 'width'); | |
263 } | |
264 if (_scroller._shouldScrollVertically()) { | |
265 num scrollPercentY = _scroller.getVerticalScrollPercent(scrollY); | |
266 _updateScrollbar(_verticalElement, scrollY, scrollPercentY, | |
267 _scroller._scrollSize.height, | |
268 contentSize.height, 'bottom', 'height'); | |
269 } | |
270 } | |
271 | |
272 /** | |
273 * When scrolling starts, show scrollbars and clear hide intervals. | |
274 */ | |
275 void _onScrollerStart(Event e) { | |
276 _scrollInProgress = true; | |
277 _cancelTimeout(); | |
278 _showScrollbars(true); | |
279 } | |
280 | |
281 void _cancelTimeout() { | |
282 if (_timerId != null) { | |
283 window.clearTimeout(_timerId); | |
284 _timerId = null; | |
285 } | |
286 } | |
287 | |
288 /** | |
289 * Show or hide the scrollbars by changing the opacity. | |
290 */ | |
291 void _showScrollbars(bool show) { | |
292 if (_hovering == true && _displayOnHover) { | |
293 show = true; | |
294 } | |
295 _toggleOpacity(_verticalElement, show); | |
296 _toggleOpacity(_horizontalElement, show); | |
297 } | |
298 | |
299 _toggleOpacity(Element element, bool show) { | |
300 if (show) { | |
301 element.style.removeProperty("opacity"); | |
302 } else { | |
303 element.style.opacity = '0'; | |
304 } | |
305 } | |
306 | |
307 num _defaultScrollSize(num frameSize, num contentSize) { | |
308 return GoogleMath.clamp( | |
309 (frameSize -_PADDING_LENGTH * 2) * frameSize / contentSize, | |
310 _MIN_SIZE, frameSize -_PADDING_LENGTH * 2); | |
311 } | |
312 | |
313 /** | |
314 * Update the vertical or horizontal scrollbar based on the new scroll | |
315 * properties. The CSS property to adjust for position (bottom|right) is | |
316 * specified by [cssPos]. The CSS property to adjust for size (height|width) | |
317 * is specified by [cssSize]. | |
318 */ | |
319 void _updateScrollbar(Element element, num offset, | |
320 num scrollPercent, num frameSize, | |
321 num contentSize, String cssPos, String cssSize) { | |
322 if (!_cachedSize.containsKey(cssSize)) { | |
323 if (offset == null || contentSize < frameSize) { | |
324 return; | |
325 } | |
326 _frame.nodes.add(element); | |
327 } | |
328 num stretchPercent; | |
329 if (scrollPercent > 1) { | |
330 stretchPercent = scrollPercent - 1; | |
331 } else { | |
332 stretchPercent = scrollPercent < 0 ? -scrollPercent : 0; | |
333 } | |
334 num scrollPx = stretchPercent * (contentSize - frameSize); | |
335 num maxSize = _defaultScrollSize(frameSize, contentSize); | |
336 num size = Math.max(_MIN_COMPRESSED_SIZE, maxSize - scrollPx); | |
337 num maxOffset = frameSize - size -_PADDING_LENGTH * 2; | |
338 num pos = GoogleMath.clamp(scrollPercent * maxOffset, | |
339 0, maxOffset) + _PADDING_LENGTH; | |
340 pos = pos.round(); | |
341 size = size.round(); | |
342 final style = element.style; | |
343 style.setProperty(cssPos, '${pos}px', ''); | |
344 if (_cachedSize[cssSize] != size) { | |
345 _cachedSize[cssSize] = size; | |
346 style.setProperty(cssSize, '${size}px', ''); | |
347 } | |
348 if (element.parent == null) { | |
349 _frame.nodes.add(element); | |
350 } | |
351 } | |
352 } | |
OLD | NEW |