| 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 |