| Index: client/samples/swarm/Views.dart
|
| ===================================================================
|
| --- client/samples/swarm/Views.dart (revision 3770)
|
| +++ client/samples/swarm/Views.dart (working copy)
|
| @@ -1,985 +0,0 @@
|
| -// 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.
|
| -
|
| -// This file contains View framework classes.
|
| -// As it grows, it may need to be split into multiple files.
|
| -
|
| -/** A factory that creates a view from a data model. */
|
| -interface ViewFactory<D> {
|
| - View newView(D item);
|
| -
|
| - /** The width of the created view or null if the width is not fixed. */
|
| - int get width();
|
| -
|
| - /** The height of the created view or null if the height is not fixed. */
|
| - int get height();
|
| -}
|
| -
|
| -interface VariableSizeViewFactory<D> {
|
| - View newView(D item);
|
| -
|
| - /** The width of the created view for a specific data model. */
|
| - int getWidth(D item);
|
| -
|
| - /** The height of the created view for a specific data model. */
|
| - int getHeight(D item);
|
| -}
|
| -
|
| -/** A collection of event listeners. */
|
| -class EventListeners {
|
| - var listeners;
|
| - EventListeners() {
|
| - listeners = new List();
|
| - }
|
| -
|
| - void addListener(listener) {
|
| - listeners.add(listener);
|
| - }
|
| -
|
| - void fire(var event) {
|
| - for (final listener in listeners) {
|
| - listener(event);
|
| - }
|
| - }
|
| -}
|
| -
|
| -
|
| -/**
|
| - * Private view class used to store placeholder views for detatched ListView
|
| - * elements.
|
| - */
|
| -class _PlaceholderView extends View {
|
| - _PlaceholderView() : super() {}
|
| -
|
| - Element render() => new Element.tag('div');
|
| -}
|
| -
|
| -/**
|
| - * Class providing all metrics required to layout a data driven list view.
|
| - */
|
| -interface ListViewLayout<D> {
|
| - void onDataChange();
|
| -
|
| - // TODO(jacobr): placing the newView member function on this class seems like
|
| - // the wrong design.
|
| - View newView(int index);
|
| - /** Get the height of the view. Possibly expensive to compute. */
|
| - int getHeight(int viewLength);
|
| - /** Get the width of the view. Possibly expensive to compute. */
|
| - int getWidth(int viewLength);
|
| - /** Get the length of the view. Possible expensive to compute. */
|
| - int getLength(int viewLength);
|
| - /** Estimated height of the view. Guaranteed to be fast to compute. */
|
| - int getEstimatedHeight(int viewLength);
|
| - /** Estimated with of the view. Guaranteed to be fast to compute. */
|
| - int getEstimatedWidth(int viewLength);
|
| -
|
| - /**
|
| - * Returns the offset in px that the ith item in the view should be placed
|
| - * at.
|
| - */
|
| - int getOffset(int index);
|
| -
|
| - /**
|
| - * The page the ith item in the view should be placed in.
|
| - */
|
| - int getPage(int index, int viewLength);
|
| - int getPageStartIndex(int index, int viewLength);
|
| -
|
| - int getEstimatedLength(int viewLength);
|
| - /**
|
| - * Snap a specified index to the nearest visible view given the [viewLength].
|
| - */
|
| - int getSnapIndex(num offset, num viewLength);
|
| - /**
|
| - * Returns an interval specifying what views are currently visible given a
|
| - * particular [:offset:].
|
| - */
|
| - Interval computeVisibleInterval(num offset, num viewLength,
|
| - num bufferLength);
|
| -}
|
| -
|
| -/**
|
| - * Base class used for the simple fixed size item [:ListView:] classes and more
|
| - * complex list view classes such as [:VariableSizeListView:] using a
|
| - * [:ListViewLayout:] class to drive the actual layout.
|
| - */
|
| -class GenericListView<D> extends View {
|
| - /** Minimum throw distance in pixels to trigger snapping to the next item. */
|
| - static final SNAP_TO_NEXT_THROW_THRESHOLD = 15;
|
| -
|
| - static final INDEX_DATA_ATTRIBUTE = 'data-index';
|
| -
|
| - final bool _scrollable;
|
| - final bool _showScrollbar;
|
| - final bool _snapToItems;
|
| - Scroller scroller;
|
| - Scrollbar _scrollbar;
|
| - List<D> _data;
|
| - ObservableValue<D> _selectedItem;
|
| - Map<int, View> _itemViews;
|
| - Element _containerElem;
|
| - bool _vertical;
|
| - /** Length of the scrollable dimension of the view in px. */
|
| - int _viewLength = 0;
|
| - Interval _activeInterval;
|
| - bool _paginate;
|
| - bool _removeClippedViews;
|
| - ListViewLayout<D> _layout;
|
| - D _lastSelectedItem;
|
| - PageState _pages;
|
| -
|
| - /**
|
| - * Creates a new GenericListView with the given layout and data. If [:_data:]
|
| - * is an [:ObservableList<T>:] then it will listen to changes to the list
|
| - * and update the view appropriately.
|
| - */
|
| - GenericListView(
|
| - this._layout,
|
| - this._data,
|
| - this._scrollable,
|
| - this._vertical,
|
| - this._selectedItem,
|
| - this._snapToItems,
|
| - this._paginate,
|
| - this._removeClippedViews,
|
| - this._showScrollbar,
|
| - this._pages)
|
| - : super(),
|
| - _activeInterval = new Interval(0, 0),
|
| - _itemViews = new Map<int, View>() {
|
| - // TODO(rnystrom): Move this into enterDocument once we have an exitDocument
|
| - // that we can use to unregister it.
|
| - if (_scrollable) {
|
| - window.on.resize.add((Event event) {
|
| - if (isInDocument) {
|
| - onResize();
|
| - }
|
| - });
|
| - }
|
| - }
|
| -
|
| - void onSelectedItemChange() {
|
| - // TODO(rnystrom): use Observable to track the last value of _selectedItem
|
| - // rather than tracking it ourselves.
|
| - _select(findIndex(_lastSelectedItem), false);
|
| - _select(findIndex(_selectedItem.value), true);
|
| - _lastSelectedItem = _selectedItem.value;
|
| - }
|
| -
|
| - Collection<View> get childViews() {
|
| - return _itemViews.getValues();
|
| - }
|
| -
|
| - void _onClick(MouseEvent e) {
|
| - int index = _findAssociatedIndex(e.target);
|
| - if (index != null) {
|
| - _selectedItem.value = _data[index];
|
| - }
|
| - }
|
| -
|
| - int _findAssociatedIndex(Node leafNode) {
|
| - Node node = leafNode;
|
| - while (node != null && node != _containerElem) {
|
| - if (node.parent == _containerElem) {
|
| - return _nodeToIndex(node);
|
| - }
|
| - node = node.parent;
|
| - }
|
| - return null;
|
| - }
|
| -
|
| - int _nodeToIndex(Element node) {
|
| - // TODO(jacobr): use data attributes when available.
|
| - String index = node.attributes[INDEX_DATA_ATTRIBUTE];
|
| - if (index != null && index.length > 0) {
|
| - return Math.parseInt(index);
|
| - }
|
| - return null;
|
| - }
|
| -
|
| - Element render() {
|
| - final node = new Element.tag('div');
|
| - if (_scrollable) {
|
| - _containerElem = new Element.tag('div');
|
| - _containerElem.tabIndex = -1;
|
| - node.nodes.add(_containerElem);
|
| - } else {
|
| - _containerElem = node;
|
| - }
|
| -
|
| - if (_scrollable) {
|
| - scroller = new Scroller(
|
| - _containerElem,
|
| - _vertical /* verticalScrollEnabled */,
|
| - !_vertical /* horizontalScrollEnabled */,
|
| - true /* momentumEnabled */,
|
| - () {
|
| - num width = _layout.getWidth(_viewLength);
|
| - num height = _layout.getHeight(_viewLength);
|
| - width = width != null ? width : 0;
|
| - height = height != null ? height : 0;
|
| - final completer = new Completer<Size>();
|
| - completer.complete(new Size(width, height));
|
| - return completer.future;
|
| - },
|
| - _paginate && _snapToItems ?
|
| - Scroller.FAST_SNAP_DECELERATION_FACTOR : 1);
|
| - scroller.onContentMoved.add((e) => renderVisibleItems(false));
|
| - if (_pages != null) {
|
| - watch(_pages.target, (s) => _onPageSelected());
|
| - }
|
| -
|
| - if (_snapToItems) {
|
| - scroller.onDecelStart.add((e) => _decelStart());
|
| - scroller.onScrollerDragEnd.add((e) => _decelStart());
|
| - }
|
| - if (_showScrollbar) {
|
| - _scrollbar = new Scrollbar(scroller, true);
|
| - }
|
| - } else {
|
| - _reserveArea();
|
| - renderVisibleItems(true);
|
| - }
|
| -
|
| - return node;
|
| - }
|
| -
|
| - void afterRender(Element node) {
|
| - // If our data source is observable, observe it.
|
| - if (_data is ObservableList<D>) {
|
| - ObservableList<D> observable = _data;
|
| - attachWatch(observable, (EventSummary e) {
|
| - if (e.target == observable) {
|
| - onDataChange();
|
| - }
|
| - });
|
| - }
|
| -
|
| - if (_selectedItem != null) {
|
| - addOnClick(function(Event e) { _onClick(e); });
|
| - }
|
| -
|
| - if (_selectedItem != null) {
|
| - watch(_selectedItem, (EventSummary summary) => onSelectedItemChange());
|
| - }
|
| - }
|
| -
|
| - void onDataChange() {
|
| - _layout.onDataChange();
|
| - _renderItems();
|
| - }
|
| -
|
| - void _reserveArea() {
|
| - final style = _containerElem.style;
|
| - int width = _layout.getWidth(_viewLength);
|
| - int height = _layout.getHeight(_viewLength);
|
| - if (width != null) {
|
| - style.width = '${width}px';
|
| - }
|
| - if (height != null) {
|
| - style.height = '${height}px';
|
| - }
|
| - // TODO(jacobr): this should be specified by the default CSS for a
|
| - // GenericListView.
|
| - style.overflow = 'hidden';
|
| - }
|
| -
|
| -
|
| - void onResize() {
|
| - int lastViewLength = _viewLength;
|
| - node.rect.then((ElementRect rect) {
|
| - _viewLength = _vertical ? rect.offset.height : rect.offset.width;
|
| - if (_viewLength != lastViewLength) {
|
| - if (_scrollbar != null) {
|
| - _scrollbar.refresh();
|
| - }
|
| - renderVisibleItems(true);
|
| - }
|
| - });
|
| - }
|
| -
|
| - void enterDocument() {
|
| - if (scroller != null) {
|
| - onResize();
|
| -
|
| - if (_scrollbar != null) {
|
| - _scrollbar.initialize();
|
| - }
|
| - }
|
| - }
|
| -
|
| - int getNextIndex(int index, bool forward) {
|
| - int delta = forward ? 1 : -1;
|
| - if (_paginate) {
|
| - int newPage = Math.max(0, _layout.getPage(index, _viewLength) + delta);
|
| - index = _layout.getPageStartIndex(newPage, _viewLength);
|
| - } else {
|
| - index += delta;
|
| - }
|
| - return GoogleMath.clamp(index, 0, _data.length - 1);
|
| - }
|
| -
|
| - void _decelStart() {
|
| - num currentTarget = scroller.verticalEnabled ?
|
| - scroller.currentTarget.y : scroller.currentTarget.x;
|
| - num current = scroller.verticalEnabled ?
|
| - scroller.contentOffset.y : scroller.contentOffset.x;
|
| - num targetIndex = _layout.getSnapIndex(currentTarget, _viewLength);
|
| - if (current != currentTarget) {
|
| - // The user is throwing rather than statically releasing.
|
| - // For this case, we want to move them to the next snap interval
|
| - // as long as they made at least a minimal throw gesture.
|
| - num currentIndex = _layout.getSnapIndex(current, _viewLength);
|
| - if (currentIndex == targetIndex &&
|
| - (currentTarget - current).abs() > SNAP_TO_NEXT_THROW_THRESHOLD &&
|
| - -_layout.getOffset(targetIndex) != currentTarget) {
|
| - num snappedCurrentPosition = -_layout.getOffset(targetIndex);
|
| - targetIndex = getNextIndex(targetIndex, currentTarget < current);
|
| - }
|
| - }
|
| - num targetPosition = -_layout.getOffset(targetIndex);
|
| - if (currentTarget != targetPosition) {
|
| - if (scroller.verticalEnabled) {
|
| - scroller.throwTo(scroller.contentOffset.x, targetPosition);
|
| - } else {
|
| - scroller.throwTo(targetPosition, scroller.contentOffset.y);
|
| - }
|
| - } else {
|
| - // Update the target page only after we are all done animating.
|
| - if (_pages != null) {
|
| - _pages.target.value =_layout.getPage(targetIndex, _viewLength);
|
| - }
|
| - }
|
| - }
|
| -
|
| - void _renderItems() {
|
| - for (int i = _activeInterval.start; i < _activeInterval.end; i++) {
|
| - _removeView(i);
|
| - }
|
| - _itemViews.clear();
|
| - _activeInterval = new Interval(0, 0);
|
| - if (scroller == null) {
|
| - _reserveArea();
|
| - }
|
| - renderVisibleItems(false);
|
| - }
|
| -
|
| - void _onPageSelected() {
|
| - if (_pages.target !=
|
| - _layout.getPage(_activeInterval.start, _viewLength)) {
|
| - _throwTo(_layout.getOffset(
|
| - _layout.getPageStartIndex(_pages.target.value, _viewLength)));
|
| - }
|
| - }
|
| -
|
| - num get _offset() {
|
| - return scroller.verticalEnabled ?
|
| - scroller.getVerticalOffset() : scroller.getHorizontalOffset();
|
| - }
|
| -
|
| - /**
|
| - * Calculates visible interval, based on the scroller position.
|
| - */
|
| - Interval getVisibleInterval() {
|
| - return _layout.computeVisibleInterval(_offset, _viewLength, 0);
|
| - }
|
| -
|
| - void renderVisibleItems(bool lengthChanged) {
|
| - Interval targetInterval;
|
| - if (scroller != null) {
|
| - targetInterval = getVisibleInterval();
|
| - } else {
|
| - // If the view is not scrollable, render all elements.
|
| - targetInterval = new Interval(0, _data.length);
|
| - }
|
| -
|
| - if (_pages != null) {
|
| - _pages.current.value =
|
| - _layout.getPage(targetInterval.start, _viewLength);
|
| - }
|
| - if (_pages != null) {
|
| - _pages.length.value = _data.length > 0 ?
|
| - _layout.getPage(_data.length - 1, _viewLength) + 1 : 0;
|
| - }
|
| -
|
| - if (!_removeClippedViews) {
|
| - // Avoid removing clipped views by extending the target interval to
|
| - // include the existing interval of rendered views.
|
| - targetInterval = targetInterval.union(_activeInterval);
|
| - }
|
| -
|
| - if (lengthChanged == false && targetInterval == _activeInterval) {
|
| - return;
|
| - }
|
| -
|
| - // TODO(jacobr): add unittests that this code behaves correctly.
|
| -
|
| - // Remove views that are not needed anymore
|
| - for (int i = _activeInterval.start,
|
| - end = Math.min(targetInterval.start, _activeInterval.end);
|
| - i < end; i++) {
|
| - _removeView(i);
|
| - }
|
| - for (int i = Math.max(targetInterval.end, _activeInterval.start);
|
| - i < _activeInterval.end; i++) {
|
| - _removeView(i);
|
| - }
|
| -
|
| - // Add new views
|
| - for (int i = targetInterval.start,
|
| - end = Math.min(_activeInterval.start, targetInterval.end);
|
| - i < end; i++) {
|
| - _addView(i);
|
| - }
|
| - for (int i = Math.max(_activeInterval.end, targetInterval.start);
|
| - i < targetInterval.end; i++) {
|
| - _addView(i);
|
| - }
|
| -
|
| - _activeInterval = targetInterval;
|
| - }
|
| -
|
| - void _removeView(int index) {
|
| - // Do not remove placeholder views as they need to stay present in case
|
| - // they scroll out of view and then back into view.
|
| - if (!(_itemViews[index] is _PlaceholderView)) {
|
| - // Remove from the active DOM but don't destroy.
|
| - _itemViews[index].node.remove();
|
| - childViewRemoved(_itemViews[index]);
|
| - }
|
| - }
|
| -
|
| - View _newView(int index) {
|
| - final view = _layout.newView(index);
|
| - view.node.attributes[INDEX_DATA_ATTRIBUTE] = index.toString();
|
| - return view;
|
| - }
|
| -
|
| - View _addView(int index) {
|
| - if (_itemViews.containsKey(index)) {
|
| - final view = _itemViews[index];
|
| - _addViewHelper(view, index);
|
| - childViewAdded(view);
|
| - return view;
|
| - }
|
| -
|
| - final view = _newView(index);
|
| - _itemViews[index] = view;
|
| - // TODO(jacobr): its ugly to put this here... but its needed
|
| - // as typical even-odd css queries won't work as we only display some
|
| - // children at a time.
|
| - if (index == 0) {
|
| - view.addClass('first-child');
|
| - }
|
| - _selectHelper(view, _data[index] == _lastSelectedItem);
|
| - // The order of the child elements doesn't matter as we use absolute
|
| - // positioning.
|
| - _addViewHelper(view, index);
|
| - childViewAdded(view);
|
| - return view;
|
| - }
|
| -
|
| - void _addViewHelper(View view, int index) {
|
| - _positionSubview(view.node, index);
|
| - // The view might already be attached.
|
| - if (view.node.parent != _containerElem) {
|
| - _containerElem.nodes.add(view.node);
|
| - }
|
| - }
|
| -
|
| - /**
|
| - * Detach a subview from the view replacing it with an empty placeholder view.
|
| - * The detatched subview can be safely reparented.
|
| - */
|
| - View detachSubview(D itemData) {
|
| - int index = findIndex(itemData);
|
| - View view = _itemViews[index];
|
| - if (view == null) {
|
| - // Edge case: add the view so we can detatch as the view is currently
|
| - // outside but might soon be inside the visible area.
|
| - assert(!_activeInterval.contains(index));
|
| - _addView(index);
|
| - view = _itemViews[index];
|
| - }
|
| - final placeholder = new _PlaceholderView();
|
| - view.node.replaceWith(placeholder.node);
|
| - _itemViews[index] = placeholder;
|
| - return view;
|
| - }
|
| -
|
| - /**
|
| - * Reattach a subview from the view that was detached from the view
|
| - * by calling detachSubview. [callback] is called once the subview is
|
| - * reattached and done animating into position.
|
| - */
|
| - void reattachSubview(D data, View view, bool animate) {
|
| - int index = findIndex(data);
|
| - // TODO(jacobr): perform some validation that the view is
|
| - // really detached.
|
| - var currentPosition;
|
| - if (animate) {
|
| - currentPosition =
|
| - FxUtil.computeRelativePosition(view.node, _containerElem);
|
| - }
|
| - assert (_itemViews[index] is _PlaceholderView);
|
| - view.enterDocument();
|
| - _itemViews[index].node.replaceWith(view.node);
|
| - _itemViews[index] = view;
|
| - if (animate) {
|
| - FxUtil.setTranslate(view.node, currentPosition.x, currentPosition.y, 0);
|
| - // The view's position is unchanged except now re-parented to
|
| - // the list view.
|
| - window.setTimeout(() { _positionSubview(view.node, index); }, 0);
|
| - } else {
|
| - _positionSubview(view.node, index);
|
| - }
|
| - }
|
| -
|
| - int findIndex(D targetItem) {
|
| - // TODO(jacobr): move this to a util library or modify this class so that
|
| - // the data is an List not a Collection.
|
| - int i = 0;
|
| - for (D item in _data) {
|
| - if (item == targetItem) {
|
| - return i;
|
| - }
|
| - i++;
|
| - }
|
| - return null;
|
| - }
|
| -
|
| - void _positionSubview(Element node, int index) {
|
| - if (_vertical) {
|
| - FxUtil.setTranslate(node, 0, _layout.getOffset(index), 0);
|
| - } else {
|
| - FxUtil.setTranslate(node, _layout.getOffset(index), 0, 0);
|
| - }
|
| - node.style.zIndex = index.toString();
|
| - }
|
| -
|
| - void _select(int index, bool selected) {
|
| - if (index != null) {
|
| - final subview = getSubview(index);
|
| - if (subview != null) {
|
| - _selectHelper(subview, selected);
|
| - }
|
| - }
|
| - }
|
| -
|
| - void _selectHelper(View view, bool selected) {
|
| - if (selected) {
|
| - view.addClass('sel');
|
| - } else {
|
| - view.removeClass('sel');
|
| - }
|
| - }
|
| -
|
| - View getSubview(int index) {
|
| - return _itemViews[index];
|
| - }
|
| -
|
| - void showView(D targetItem) {
|
| - int index = findIndex(targetItem);
|
| - if (index != null) {
|
| - if (_layout.getOffset(index) < -_offset) {
|
| - _throwTo(_layout.getOffset(index));
|
| - } else if (_layout.getOffset(index + 1) > (-_offset + _viewLength)) {
|
| - // TODO(jacobr): for completeness we should check whether
|
| - // the current view is longer than _viewLength in which case
|
| - // there are some nasty edge cases.
|
| - _throwTo(_layout.getOffset(index + 1) - _viewLength);
|
| - }
|
| - }
|
| - }
|
| -
|
| - void _throwTo(num offset) {
|
| - if (_vertical) {
|
| - scroller.throwTo(0, -offset);
|
| - } else {
|
| - scroller.throwTo(-offset, 0);
|
| - }
|
| - }
|
| -}
|
| -
|
| -class FixedSizeListViewLayout<D> implements ListViewLayout<D> {
|
| - final ViewFactory<D> itemViewFactory;
|
| - final bool _vertical;
|
| - List<D> _data;
|
| - bool _paginate;
|
| -
|
| - FixedSizeListViewLayout(this.itemViewFactory, this._data, this._vertical,
|
| - this._paginate);
|
| -
|
| - void onDataChange() {}
|
| -
|
| - View newView(int index) {
|
| - return itemViewFactory.newView(_data[index]);
|
| - }
|
| -
|
| - int get _itemLength() {
|
| - return _vertical ? itemViewFactory.height : itemViewFactory.width;
|
| - }
|
| -
|
| -
|
| - int getWidth(int viewLength) {
|
| - return _vertical ? itemViewFactory.width : getLength(viewLength);
|
| - }
|
| -
|
| - int getHeight(int viewLength) {
|
| - return _vertical ? getLength(viewLength) : itemViewFactory.height;
|
| - }
|
| -
|
| - int getEstimatedHeight(int viewLength) {
|
| - // Returns the exact height as it is trivial to compute for this layout.
|
| - return getHeight(viewLength);
|
| - }
|
| -
|
| - int getEstimatedWidth(int viewLength) {
|
| - // Returns the exact height as it is trivial to compute for this layout.
|
| - return getWidth(viewLength);
|
| - }
|
| -
|
| - int getEstimatedLength(int viewLength) {
|
| - // Returns the exact length as it is trivial to compute for this layout.
|
| - return getLength(viewLength);
|
| - }
|
| -
|
| - int getLength(int viewLength) {
|
| - int itemLength =
|
| - _vertical ? itemViewFactory.height : itemViewFactory.width;
|
| - if (viewLength == null || viewLength == 0) {
|
| - return itemLength * _data.length;
|
| - } else if (_paginate) {
|
| - if (_data.length > 0) {
|
| - final pageLength = getPageLength(viewLength);
|
| - return getPage(_data.length - 1, viewLength)
|
| - * pageLength + Math.max(viewLength, pageLength);
|
| - } else {
|
| - return 0;
|
| - }
|
| - } else {
|
| - return itemLength * (_data.length - 1) + Math.max(viewLength, itemLength);
|
| - }
|
| - }
|
| -
|
| - int getOffset(int index) {
|
| - return index * _itemLength;
|
| - }
|
| -
|
| - int getPageLength(int viewLength) {
|
| - final itemsPerPage = (viewLength / _itemLength).floor();
|
| - return (Math.max(1, itemsPerPage) * _itemLength).toInt();
|
| - }
|
| -
|
| - int getPage(int index, int viewLength) {
|
| - return (getOffset(index) / getPageLength(viewLength)).floor().toInt();
|
| - }
|
| -
|
| - int getPageStartIndex(int page, int viewLength) {
|
| - return (getPageLength(viewLength) / _itemLength).toInt() * page;
|
| - }
|
| -
|
| - int getSnapIndex(num offset, int viewLength) {
|
| - int index = (-offset / _itemLength).round().toInt();
|
| - if (_paginate) {
|
| - index = getPageStartIndex(getPage(index, viewLength), viewLength);
|
| - }
|
| - return GoogleMath.clamp(index, 0, _data.length - 1);
|
| - }
|
| -
|
| - Interval computeVisibleInterval(
|
| - num offset, num viewLength, num bufferLength) {
|
| - num targetIntervalStart =
|
| - Math.max(0,((-offset - bufferLength) / _itemLength).floor());
|
| - num targetIntervalEnd = GoogleMath.clamp(
|
| - ((-offset + viewLength + bufferLength) / _itemLength).ceil(),
|
| - targetIntervalStart,
|
| - _data.length);
|
| - return new Interval(targetIntervalStart.toInt(),
|
| - targetIntervalEnd.toInt());
|
| - }
|
| -}
|
| -
|
| -/**
|
| - * Simple list view class where each item has fixed width and height.
|
| - */
|
| -class ListView<D> extends GenericListView<D> {
|
| -
|
| - /**
|
| - * Creates a new ListView for the given data. If [:_data:] is an
|
| - * [:ObservableList<T>:] then it will listen to changes to the list and
|
| - * update the view appropriately.
|
| - */
|
| - ListView(List<D> data, ViewFactory<D> itemViewFactory, bool scrollable,
|
| - bool vertical, ObservableValue<D> selectedItem,
|
| - [bool snapToItems = false,
|
| - bool paginate = false,
|
| - bool removeClippedViews = false,
|
| - bool showScrollbar = false,
|
| - PageState pages = null])
|
| - : super(new FixedSizeListViewLayout<D>(itemViewFactory, data, vertical,
|
| - paginate),
|
| - data, scrollable, vertical, selectedItem, snapToItems, paginate,
|
| - removeClippedViews, showScrollbar, pages);
|
| -}
|
| -
|
| -/**
|
| - * Layout where each item may have variable size along the axis the list view
|
| - * extends.
|
| - */
|
| -class VariableSizeListViewLayout<D> implements ListViewLayout<D> {
|
| - List<D> _data;
|
| - List<int> _itemOffsets;
|
| - List<int> _lengths;
|
| - int _lastOffset = 0;
|
| - bool _vertical;
|
| - bool _paginate;
|
| - VariableSizeViewFactory<D> itemViewFactory;
|
| - Interval _lastVisibleInterval;
|
| -
|
| - VariableSizeListViewLayout(this.itemViewFactory, data, this._vertical,
|
| - this._paginate) :
|
| - _data = data,
|
| - _lastVisibleInterval = new Interval(0, 0) {
|
| - _itemOffsets = <int>[];
|
| - _lengths = <int>[];
|
| - _itemOffsets.add(0);
|
| - }
|
| -
|
| - void onDataChange() {
|
| - _itemOffsets.clear();
|
| - _itemOffsets.add(0);
|
| - _lengths.clear();
|
| - }
|
| -
|
| - View newView(int index) => itemViewFactory.newView(_data[index]);
|
| -
|
| - int getWidth(int viewLength) {
|
| - if (_vertical) {
|
| - return itemViewFactory.getWidth(null);
|
| - } else {
|
| - return getLength(viewLength);
|
| - }
|
| - }
|
| -
|
| - int getHeight(int viewLength) {
|
| - if (_vertical) {
|
| - return getLength(viewLength);
|
| - } else {
|
| - return itemViewFactory.getHeight(null);
|
| - }
|
| - }
|
| -
|
| - int getEstimatedHeight(int viewLength) {
|
| - if (_vertical) {
|
| - return getEstimatedLength(viewLength);
|
| - } else {
|
| - return itemViewFactory.getHeight(null);
|
| - }
|
| - }
|
| -
|
| - int getEstimatedWidth(int viewLength) {
|
| - if (_vertical) {
|
| - return itemViewFactory.getWidth(null);
|
| - } else {
|
| - return getEstimatedLength(viewLength);
|
| - }
|
| - }
|
| -
|
| - // TODO(jacobr): this logic is overly complicated. Replace with something
|
| - // simpler.
|
| - int getEstimatedLength(int viewLength) {
|
| - if (_lengths.length == _data.length) {
|
| - // No need to estimate... we have all the data already.
|
| - return getLength(viewLength);
|
| - }
|
| - if (_itemOffsets.length > 1 && _lengths.length > 0) {
|
| - // Estimate length by taking the average of the lengths
|
| - // of the known views.
|
| - num lengthFromAllButLastElement = 0;
|
| - if (_itemOffsets.length > 2) {
|
| - lengthFromAllButLastElement =
|
| - (getOffset(_itemOffsets.length - 2) -
|
| - getOffset(0)) *
|
| - (_data.length / (_itemOffsets.length - 2));
|
| - }
|
| - return (lengthFromAllButLastElement +
|
| - Math.max(viewLength, _lengths[_lengths.length - 1])).toInt();
|
| - } else {
|
| - if (_lengths.length == 1) {
|
| - return Math.max(viewLength, _lengths[0]);
|
| - } else {
|
| - return viewLength;
|
| - }
|
| - }
|
| - }
|
| -
|
| - int getLength(int viewLength) {
|
| - if (_data.length == 0) {
|
| - return viewLength;
|
| - } else {
|
| - // Hack so that _lengths[length - 1] is available.
|
| - getOffset(_data.length);
|
| - return (getOffset(_data.length - 1) - getOffset(0)) +
|
| - Math.max(_lengths[_lengths.length - 1], viewLength);
|
| - }
|
| - }
|
| -
|
| - int getOffset(int index) {
|
| - if (index >= _itemOffsets.length) {
|
| - int offset = _itemOffsets[_itemOffsets.length - 1];
|
| - for (int i = _itemOffsets.length; i <= index; i++) {
|
| - int length = _vertical ? itemViewFactory.getHeight(_data[i - 1])
|
| - : itemViewFactory.getWidth(_data[i - 1]);
|
| - offset += length;
|
| - _itemOffsets.add(offset);
|
| - _lengths.add(length);
|
| - }
|
| - }
|
| - return _itemOffsets[index];
|
| - }
|
| -
|
| - int getPage(int index, int viewLength) {
|
| - // TODO(jacobr): implement.
|
| - throw 'Not implemented';
|
| - }
|
| -
|
| - int getPageStartIndex(int page, int viewLength) {
|
| - // TODO(jacobr): implement.
|
| - throw 'Not implemented';
|
| - }
|
| -
|
| - int getSnapIndex(num offset, int viewLength) {
|
| - for (int i = 1; i < _data.length; i++) {
|
| - if (getOffset(i) + getOffset(i - 1) > -offset * 2) {
|
| - return i - 1;
|
| - }
|
| - }
|
| - return _data.length - 1;
|
| - }
|
| -
|
| - Interval computeVisibleInterval(
|
| - num offset, num viewLength, num bufferLength) {
|
| - offset = offset.toInt();
|
| - int start = _findFirstItemBefore(
|
| - -offset - bufferLength,
|
| - _lastVisibleInterval != null ? _lastVisibleInterval.start : 0);
|
| - int end = _findFirstItemAfter(
|
| - -offset + viewLength + bufferLength,
|
| - _lastVisibleInterval != null ? _lastVisibleInterval.end : 0);
|
| - _lastVisibleInterval = new Interval(start, Math.max(start, end));
|
| - _lastOffset = offset;
|
| - return _lastVisibleInterval;
|
| - }
|
| -
|
| - int _findFirstItemAfter(num target, int hint) {
|
| - for (int i = 0; i < _data.length; i++) {
|
| - if (getOffset(i) > target) {
|
| - return i;
|
| - }
|
| - }
|
| - return _data.length;
|
| - }
|
| -
|
| - // TODO(jacobr): use hint.
|
| - int _findFirstItemBefore(num target, int hint) {
|
| - // We go search this direction delaying computing the actual view size
|
| - // as long as possible.
|
| - for (int i = 1; i < _data.length; i++) {
|
| - if (getOffset(i) >= target) {
|
| - return i - 1;
|
| - }
|
| - }
|
| - return Math.max(_data.length - 1, 0);
|
| - }
|
| -}
|
| -
|
| -class VariableSizeListView<D> extends GenericListView<D> {
|
| -
|
| - VariableSizeListView(List<D> data,
|
| - VariableSizeViewFactory<D> itemViewFactory,
|
| - bool scrollable,
|
| - bool vertical,
|
| - ObservableValue<D> selectedItem,
|
| - [bool snapToItems = false,
|
| - bool paginate = false,
|
| - bool removeClippedViews = false,
|
| - bool showScrollbar = false,
|
| - PageState pages = null])
|
| - : super(new VariableSizeListViewLayout(itemViewFactory, data, vertical,
|
| - paginate),
|
| - data, scrollable, vertical, selectedItem, snapToItems,
|
| - paginate, removeClippedViews, showScrollbar, pages);
|
| -}
|
| -
|
| -/** A back button that is equivalent to clicking "back" in the browser. */
|
| -class BackButton extends View {
|
| - BackButton() : super();
|
| -
|
| - Element render() => new Element.html('<div class="back-arrow button"></div>');
|
| -
|
| - void afterRender(Element node) {
|
| - addOnClick((e) => window.history.back());
|
| - }
|
| -}
|
| -
|
| -
|
| -// TODO(terry): Maybe should be part of ButtonView class in appstack/view?
|
| -/** OS button. */
|
| -class PushButtonView extends View {
|
| - final String _text;
|
| - final String _cssClass;
|
| - final _clickHandler;
|
| -
|
| - PushButtonView(this._text, this._cssClass, this._clickHandler) : super();
|
| -
|
| - Element render() {
|
| - return new Element.html('<button class="${_cssClass}">${_text}</button>');
|
| - }
|
| -
|
| - void afterRender(Element node) {
|
| - addOnClick(_clickHandler);
|
| - }
|
| -}
|
| -
|
| -
|
| -// TODO(terry): Add a drop shadow around edge and corners need to be rounded.
|
| -// Need to support conveyor for contents of dialog so it's not
|
| -// larger than the parent window.
|
| -/** A generic dialog view supports title, done button and dialog content. */
|
| -class DialogView extends View {
|
| - final String _title;
|
| - final String _cssName;
|
| - final View _content;
|
| - Element container;
|
| - PushButtonView _done;
|
| -
|
| - DialogView(this._title, this._cssName, this._content) : super() {}
|
| -
|
| - Element render() {
|
| - final node = new Element.html('''
|
| - <div class="dialog-modal">
|
| - <div class="dialog $_cssName">
|
| - <div class="dialog-title-area">
|
| - <span class="dialog-title">$_title</span>
|
| - </div>
|
| - <div class="dialog-body"></div>
|
| - </div>
|
| - </div>''');
|
| -
|
| - _done = new PushButtonView('Done', 'done-button',
|
| - EventBatch.wrap((e) => onDone()));
|
| - final titleArea = node.query('.dialog-title-area');
|
| - titleArea.nodes.add(_done.node);
|
| -
|
| - container = node.query('.dialog-body');
|
| - container.nodes.add(_content.node);
|
| -
|
| - return node;
|
| - }
|
| -
|
| - /** Override to handle dialog done. */
|
| - void onDone() { }
|
| -}
|
|
|