745f112ef6
Opt an initial batch of files under samples, samples-dev, utils, and runtime/tools/dartfuzz out of null safety in preparation for switching the flag on by default. Change-Id: Icdfd52a5a969e678a7205903332f73fe3841c223 Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/166960 Reviewed-by: Daco Harkes <dacoharkes@google.com> Commit-Queue: Leaf Petersen <leafp@google.com>
354 lines
11 KiB
Dart
354 lines
11 KiB
Dart
// Copyright (c) 2011, the Dart project authors. Please see the AUTHORS file
|
|
// for details. All rights reserved. Use of this source code is governed by a
|
|
// BSD-style license that can be found in the LICENSE file.
|
|
|
|
// @dart = 2.9
|
|
|
|
part of touch;
|
|
|
|
/**
|
|
* Implementation of a scrollbar for the custom scrolling behavior
|
|
* defined in [:Scroller:].
|
|
*/
|
|
class Scrollbar implements ScrollListener {
|
|
/**
|
|
* The minimum size of scrollbars when not compressed.
|
|
*/
|
|
static const _MIN_SIZE = 30;
|
|
|
|
/**
|
|
* The minimum compressed size of scrollbars. Scrollbars are compressed when
|
|
* the content is stretching past its boundaries.
|
|
*/
|
|
static const _MIN_COMPRESSED_SIZE = 8;
|
|
/** Padding in pixels to add above and bellow the scrollbar. */
|
|
static const _PADDING_LENGTH = 10;
|
|
/**
|
|
* The amount of time to wait before hiding scrollbars after showing them.
|
|
* Measured in ms.
|
|
*/
|
|
static const _DISPLAY_TIME = 300;
|
|
static const DRAG_CLASS_NAME = 'drag';
|
|
|
|
Scroller _scroller;
|
|
Element _frame;
|
|
bool _scrollInProgress = false;
|
|
bool _scrollBarDragInProgressValue = false;
|
|
|
|
/**
|
|
* Cached values of height and width. Keys will be 'height' and 'width'
|
|
* depending on if they are applied to vertical or horizontal scrollbar.
|
|
*/
|
|
Map<String, num> _cachedSize;
|
|
|
|
/**
|
|
* This bound function will be used as the input to window.setTimeout when
|
|
* scheduling the hiding of the scrollbars.
|
|
*/
|
|
Function _boundHideFn;
|
|
|
|
Element _verticalElement;
|
|
Element _horizontalElement;
|
|
|
|
int _currentScrollStartMouse;
|
|
num _currentScrollStartOffset;
|
|
bool _currentScrollVertical;
|
|
num _currentScrollRatio;
|
|
Timer _timer;
|
|
|
|
bool _displayOnHover;
|
|
bool _hovering = false;
|
|
|
|
Scrollbar(Scroller scroller, [displayOnHover = true])
|
|
: _displayOnHover = displayOnHover,
|
|
_scroller = scroller,
|
|
_frame = scroller.getFrame(),
|
|
_cachedSize = new Map<String, num>() {
|
|
_boundHideFn = () {
|
|
_showScrollbars(false);
|
|
};
|
|
}
|
|
|
|
bool get _scrollBarDragInProgress => _scrollBarDragInProgressValue;
|
|
|
|
void set _scrollBarDragInProgress(bool value) {
|
|
_scrollBarDragInProgressValue = value;
|
|
_toggleClass(
|
|
_verticalElement, DRAG_CLASS_NAME, value && _currentScrollVertical);
|
|
_toggleClass(
|
|
_horizontalElement, DRAG_CLASS_NAME, value && !_currentScrollVertical);
|
|
}
|
|
|
|
// TODO(jacobr): move this helper method into the DOM.
|
|
void _toggleClass(Element e, String className, bool enabled) {
|
|
if (enabled) {
|
|
if (!e.classes.contains(className)) {
|
|
e.classes.add(className);
|
|
}
|
|
} else {
|
|
e.classes.remove(className);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Initializes elements and event handlers. Must be called after
|
|
* construction and before usage.
|
|
*/
|
|
void initialize() {
|
|
// Don't initialize if we have already been initialized.
|
|
// TODO(jacobr): remove this once bugs are fixed and enterDocument is only
|
|
// called once by each view.
|
|
if (_verticalElement != null) {
|
|
return;
|
|
}
|
|
_verticalElement = new Element.html(
|
|
'<div class="touch-scrollbar touch-scrollbar-vertical"></div>');
|
|
_horizontalElement = new Element.html(
|
|
'<div class="touch-scrollbar touch-scrollbar-horizontal"></div>');
|
|
_scroller.addScrollListener(this);
|
|
|
|
Element scrollerEl = _scroller.getElement();
|
|
|
|
if (!Device.supportsTouch) {
|
|
_addEventListeners(
|
|
_verticalElement, _onStart, _onMove, _onEnd, _onEnd, true);
|
|
_addEventListeners(
|
|
_horizontalElement, _onStart, _onMove, _onEnd, _onEnd, true);
|
|
}
|
|
|
|
_scroller.addScrollListener(this);
|
|
_showScrollbars(false);
|
|
_scroller.onScrollerStart.listen(_onScrollerStart);
|
|
_scroller.onScrollerEnd.listen(_onScrollerEnd);
|
|
if (_displayOnHover) {
|
|
// TODO(jacobr): rather than adding all these event listeners we could
|
|
// instead attach a single global event listener and let data in the
|
|
// DOM drive.
|
|
_frame.onClick.listen((Event e) {
|
|
// Always focus on click as one of our children isn't all focused.
|
|
if (!_frame.contains(document.activeElement)) {
|
|
scrollerEl.focus();
|
|
}
|
|
});
|
|
_frame.onMouseOver.listen((Event e) {
|
|
final activeElement = document.activeElement;
|
|
// TODO(jacobr): don't steal focus from a child element or a truly
|
|
// focusable element. Only support stealing focus from another
|
|
// element that was given fake focus.
|
|
if (activeElement is BodyElement ||
|
|
(!_frame.contains(activeElement) && activeElement is DivElement)) {
|
|
scrollerEl.focus();
|
|
}
|
|
if (_hovering == false) {
|
|
_hovering = true;
|
|
_cancelTimeout();
|
|
_showScrollbars(true);
|
|
refresh();
|
|
}
|
|
});
|
|
_frame.onMouseOut.listen((e) {
|
|
_hovering = false;
|
|
// Start hiding immediately if we aren't
|
|
// scrolling or already in the process of
|
|
// hiding the scrollbar
|
|
if (!_scrollInProgress && _timer == null) {
|
|
_boundHideFn();
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
void _onStart(/*MouseEvent | Touch*/ e) {
|
|
Element elementOver = e.target;
|
|
if (elementOver == _verticalElement || elementOver == _horizontalElement) {
|
|
_currentScrollVertical = elementOver == _verticalElement;
|
|
if (_currentScrollVertical) {
|
|
_currentScrollStartMouse = e.page.y;
|
|
_currentScrollStartOffset = _scroller.getVerticalOffset();
|
|
} else {
|
|
_currentScrollStartMouse = e.page.x;
|
|
_currentScrollStartOffset = _scroller.getHorizontalOffset();
|
|
}
|
|
_refreshScrollRatio();
|
|
_scrollBarDragInProgress = true;
|
|
_scroller._momentum.abort();
|
|
e.stopPropagation();
|
|
}
|
|
}
|
|
|
|
void _refreshScrollRatio() {
|
|
Size contentSize = _scroller._getAdjustedContentSize();
|
|
if (_currentScrollVertical) {
|
|
_refreshScrollRatioHelper(
|
|
_scroller._scrollSize.height, contentSize.height);
|
|
} else {
|
|
_refreshScrollRatioHelper(_scroller._scrollSize.width, contentSize.width);
|
|
}
|
|
}
|
|
|
|
void _refreshScrollRatioHelper(num frameSize, num contentSize) {
|
|
num frameTravelDistance = frameSize -
|
|
_defaultScrollSize(frameSize, contentSize) -
|
|
_PADDING_LENGTH * 2;
|
|
if (frameTravelDistance < 0.001) {
|
|
_currentScrollRatio = 0;
|
|
} else {
|
|
_currentScrollRatio = (contentSize - frameSize) / frameTravelDistance;
|
|
}
|
|
}
|
|
|
|
void _onMove(/*MouseEvent | Touch*/ e) {
|
|
if (!_scrollBarDragInProgress) {
|
|
return;
|
|
}
|
|
_refreshScrollRatio();
|
|
int coordinate = _currentScrollVertical ? e.page.y : e.page.x;
|
|
num delta = (coordinate - _currentScrollStartMouse) * _currentScrollRatio;
|
|
if (delta != 0) {
|
|
num x;
|
|
num y;
|
|
_currentScrollStartOffset -= delta;
|
|
if (_currentScrollVertical) {
|
|
x = _scroller.getHorizontalOffset();
|
|
y = _currentScrollStartOffset.toInt();
|
|
} else {
|
|
x = _currentScrollStartOffset.toInt();
|
|
y = _scroller.getVerticalOffset();
|
|
}
|
|
_scroller.setPosition(x, y);
|
|
}
|
|
_currentScrollStartMouse = coordinate;
|
|
}
|
|
|
|
void _onEnd(Event e) {
|
|
_scrollBarDragInProgress = false;
|
|
// TODO(jacobr): make scrollbar less tightly coupled to the scroller.
|
|
_scroller._onScrollerDragEnd.add(new Event(ScrollerEventType.DRAG_END));
|
|
}
|
|
|
|
/**
|
|
* When scrolling ends, schedule a timeout to hide the scrollbars.
|
|
*/
|
|
void _onScrollerEnd(Event e) {
|
|
_cancelTimeout();
|
|
_timer =
|
|
new Timer(const Duration(milliseconds: _DISPLAY_TIME), _boundHideFn);
|
|
_scrollInProgress = false;
|
|
}
|
|
|
|
void onScrollerMoved(num scrollX, num scrollY, bool decelerating) {
|
|
if (_scrollInProgress == false) {
|
|
// Display the scrollbar and then immediately prepare to hide it...
|
|
_onScrollerStart(null);
|
|
_onScrollerEnd(null);
|
|
}
|
|
updateScrollbars(scrollX, scrollY);
|
|
}
|
|
|
|
void refresh() {
|
|
if (_scrollInProgress == false && _hovering == false) {
|
|
// No need to refresh if not visible.
|
|
return;
|
|
}
|
|
_scroller._resize(() {
|
|
updateScrollbars(
|
|
_scroller.getHorizontalOffset(), _scroller.getVerticalOffset());
|
|
});
|
|
}
|
|
|
|
void updateScrollbars(num scrollX, num scrollY) {
|
|
Size contentSize = _scroller._getAdjustedContentSize();
|
|
if (_scroller._shouldScrollHorizontally()) {
|
|
num scrollPercentX = _scroller.getHorizontalScrollPercent(scrollX);
|
|
_updateScrollbar(_horizontalElement, scrollX, scrollPercentX,
|
|
_scroller._scrollSize.width, contentSize.width, 'right', 'width');
|
|
}
|
|
if (_scroller._shouldScrollVertically()) {
|
|
num scrollPercentY = _scroller.getVerticalScrollPercent(scrollY);
|
|
_updateScrollbar(_verticalElement, scrollY, scrollPercentY,
|
|
_scroller._scrollSize.height, contentSize.height, 'bottom', 'height');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* When scrolling starts, show scrollbars and clear hide intervals.
|
|
*/
|
|
void _onScrollerStart(Event e) {
|
|
_scrollInProgress = true;
|
|
_cancelTimeout();
|
|
_showScrollbars(true);
|
|
}
|
|
|
|
void _cancelTimeout() {
|
|
if (_timer != null) {
|
|
_timer.cancel();
|
|
_timer = null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Show or hide the scrollbars by changing the opacity.
|
|
*/
|
|
void _showScrollbars(bool show) {
|
|
if (_hovering == true && _displayOnHover) {
|
|
show = true;
|
|
}
|
|
_toggleOpacity(_verticalElement, show);
|
|
_toggleOpacity(_horizontalElement, show);
|
|
}
|
|
|
|
_toggleOpacity(Element element, bool show) {
|
|
if (show) {
|
|
element.style.removeProperty("opacity");
|
|
} else {
|
|
element.style.opacity = '0';
|
|
}
|
|
}
|
|
|
|
num _defaultScrollSize(num frameSize, num contentSize) {
|
|
return GoogleMath.clamp(
|
|
(frameSize - _PADDING_LENGTH * 2) * frameSize / contentSize,
|
|
_MIN_SIZE,
|
|
frameSize - _PADDING_LENGTH * 2);
|
|
}
|
|
|
|
/**
|
|
* Update the vertical or horizontal scrollbar based on the new scroll
|
|
* properties. The CSS property to adjust for position (bottom|right) is
|
|
* specified by [cssPos]. The CSS property to adjust for size (height|width)
|
|
* is specified by [cssSize].
|
|
*/
|
|
void _updateScrollbar(Element element, num offset, num scrollPercent,
|
|
num frameSize, num contentSize, String cssPos, String cssSize) {
|
|
if (!_cachedSize.containsKey(cssSize)) {
|
|
if (offset == null || contentSize < frameSize) {
|
|
return;
|
|
}
|
|
_frame.nodes.add(element);
|
|
}
|
|
num stretchPercent;
|
|
if (scrollPercent > 1) {
|
|
stretchPercent = scrollPercent - 1;
|
|
} else {
|
|
stretchPercent = scrollPercent < 0 ? -scrollPercent : 0;
|
|
}
|
|
num scrollPx = stretchPercent * (contentSize - frameSize);
|
|
num maxSize = _defaultScrollSize(frameSize, contentSize);
|
|
num size = Math.max(_MIN_COMPRESSED_SIZE, maxSize - scrollPx);
|
|
num maxOffset = frameSize - size - _PADDING_LENGTH * 2;
|
|
num pos = GoogleMath.clamp(scrollPercent * maxOffset, 0, maxOffset) +
|
|
_PADDING_LENGTH;
|
|
pos = pos.round();
|
|
size = size.round();
|
|
final style = element.style;
|
|
style.setProperty(cssPos, '${pos}px', '');
|
|
if (_cachedSize[cssSize] != size) {
|
|
_cachedSize[cssSize] = size;
|
|
style.setProperty(cssSize, '${size}px', '');
|
|
}
|
|
if (element.parent == null) {
|
|
_frame.nodes.add(element);
|
|
}
|
|
}
|
|
}
|