blob: 0360b06ff2f27ef880e9b31fb78ba9f3b827e1c2 [file] [log] [blame]
// 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.
part of swarmlib;
// 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. */
abstract class 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;
}
abstract class 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 detached ListView
* elements.
*/
class _PlaceholderView extends View {
_PlaceholderView();
Element render() => new Element.tag('div');
}
/**
* Class providing all metrics required to layout a data driven list view.
*/
abstract class 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 const SNAP_TO_NEXT_THROW_THRESHOLD = 15;
static const 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)
: _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.onResize.listen((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;
}
Iterable<View> get childViews {
return _itemViews.values.toList();
}
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 int.parse(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;
return new Size(width, height);
},
_paginate && _snapToItems
? Scroller.FAST_SNAP_DECELERATION_FACTOR
: 1);
scroller.onContentMoved.listen((e) => renderVisibleItems(false));
if (_pages != null) {
watch(_pages.target, (s) => _onPageSelected());
}
if (_snapToItems) {
scroller.onDecelStart.listen((e) => _decelStart());
scroller.onScrollerDragEnd.listen((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((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;
scheduleMicrotask(() {
_viewLength = _vertical ? node.offset.height : node.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 detached 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 detach 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.
Timer.run(() {
_positionSubview(view.node, index);
});
} 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;
return Math.max(1, itemsPerPage) * _itemLength;
}
int getPage(int index, int viewLength) {
return getOffset(index) ~/ getPageLength(viewLength);
}
int getPageStartIndex(int page, int viewLength) {
return getPageLength(viewLength) ~/ _itemLength * page;
}
int getSnapIndex(num offset, num viewLength) {
int index = (-offset / _itemLength).round();
if (_paginate) {
index = getPageStartIndex(getPage(index, viewLength), viewLength);
}
return GoogleMath.clamp(index, 0, _data.length - 1);
}
Interval computeVisibleInterval(
num offset, num viewLength, num bufferLength) {
int targetIntervalStart =
Math.max(0, (-offset - bufferLength) ~/ _itemLength);
num targetIntervalEnd = GoogleMath.clamp(
((-offset + viewLength + bufferLength) / _itemLength).ceil(),
targetIntervalStart,
_data.length);
return new Interval(targetIntervalStart, 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, num 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();
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);
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);
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.querySelector('.dialog-title-area');
titleArea.nodes.add(_done.node);
container = node.querySelector('.dialog-body');
container.nodes.add(_content.node);
return node;
}
/** Override to handle dialog done. */
void onDone() {}
}