b33097b9b4
BUG= R=sra@google.com Review-Url: https://codereview.chromium.org/2828603002 .
180 lines
5.2 KiB
Dart
180 lines
5.2 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.
|
|
|
|
part of view;
|
|
|
|
typedef void SelectHandler(String menuText);
|
|
|
|
/**
|
|
* This implements a horizontal menu bar with a sliding triangle arrow
|
|
* that points at the currently selected item.
|
|
*/
|
|
class SliderMenu extends View {
|
|
static const int TRIANGLE_WIDTH = 24;
|
|
|
|
// currently selected menu item
|
|
Element selectedItem;
|
|
|
|
// This holds the element where a touchstart occured. (This is set
|
|
// in touchstart, and cleared in touchend.) If this is null, then a
|
|
// touch operation is not in progress.
|
|
// TODO(mattsh) - move this to a touch mixin
|
|
Element touchItem;
|
|
|
|
/**
|
|
* Callback function that we call when the user chooses something from
|
|
* the menu. This is passed the menu item text.
|
|
*/
|
|
SelectHandler onSelect;
|
|
|
|
List<String> _menuItems;
|
|
|
|
SliderMenu(this._menuItems, this.onSelect) : super() {}
|
|
|
|
Element render() {
|
|
// Create a div for each menu item.
|
|
final items = new StringBuffer();
|
|
for (final item in _menuItems) {
|
|
items.write('<div class="sm-item">$item</div>');
|
|
}
|
|
|
|
// Create a root node to hold this view.
|
|
return new Element.html('''
|
|
<div class="sm-root">
|
|
<div class="sm-item-box">
|
|
<div class="sm-item-filler"></div>
|
|
$items
|
|
<div class="sm-item-filler"></div>
|
|
</div>
|
|
<div class="sm-slider-box">
|
|
<div class="sm-triangle"></div>
|
|
</div>
|
|
</div>
|
|
''');
|
|
}
|
|
|
|
void enterDocument() {
|
|
// select the first item
|
|
// todo(jacobr): too much actual work is performed in enterDocument.
|
|
// Ideally, enterDocument should do nothing more than redecorate a view
|
|
// and perhaps calculating the correct child sizes for edge cases that
|
|
// cannot be handled by the browser layout engine.
|
|
selectItem(node.querySelector('.sm-item'), false);
|
|
|
|
// TODO(mattsh), abstract this somehow into a touch click mixin
|
|
if (Device.supportsTouch) {
|
|
node.onTouchStart.listen((event) {
|
|
touchItem = itemOfTouchEvent(event);
|
|
if (touchItem != null) {
|
|
selectItemText(touchItem);
|
|
}
|
|
event.preventDefault();
|
|
});
|
|
node.onTouchEnd.listen((event) {
|
|
if (touchItem != null) {
|
|
if (itemOfTouchEvent(event) == touchItem) {
|
|
selectItem(touchItem, true);
|
|
} else {
|
|
// the Touch target is somewhere other where than the touchstart
|
|
// occured, so revert the selected menu text back to where it was
|
|
// before the touchstart,
|
|
selectItemText(selectedItem);
|
|
}
|
|
// touch operation has ended
|
|
touchItem = null;
|
|
}
|
|
event.preventDefault();
|
|
});
|
|
} else {
|
|
node.onClick.listen((event) => selectItem(event.target, true));
|
|
}
|
|
|
|
window.onResize.listen((Event event) => updateIndicator(false));
|
|
}
|
|
|
|
/**
|
|
* Walks the parent chain of the first Touch target to find the first ancestor
|
|
* that has sm-item class.
|
|
*/
|
|
Element itemOfTouchEvent(event) {
|
|
Node node = event.changedTouches[0].target;
|
|
return itemOfNode(node);
|
|
}
|
|
|
|
Element itemOfNode(Node node) {
|
|
// TODO(jmesserly): workaround for bug 5399957, document.parent == document
|
|
while (node != null && node != document) {
|
|
if (node is Element) {
|
|
Element element = node;
|
|
if (element.classes.contains('sm-item')) {
|
|
return element;
|
|
}
|
|
}
|
|
node = node.parent;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
void selectItemText(Element item) {
|
|
// unselect all menu items
|
|
for (final sliderItem in node.querySelectorAll('.sm-item')) {
|
|
sliderItem.classes.remove('sel');
|
|
}
|
|
|
|
// select the item the user clicked on
|
|
item.classes.add('sel');
|
|
}
|
|
|
|
void selectItem(Element item, bool animate) {
|
|
if (!item.classes.contains('sm-item')) {
|
|
return;
|
|
}
|
|
|
|
selectedItem = item;
|
|
selectItemText(item);
|
|
updateIndicator(animate);
|
|
onSelect(item.text);
|
|
}
|
|
|
|
void selectNext(bool animate) {
|
|
final result = node.querySelector('.sm-item.sel').nextElementSibling;
|
|
if (result != null) {
|
|
selectItem(result, animate);
|
|
}
|
|
}
|
|
|
|
void selectPrevious(bool animate) {
|
|
final result = node.querySelector('.sm-item.sel').previousElementSibling;
|
|
if (result != null) {
|
|
selectItem(result, animate);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* animate - if true, then animate the movement of the triangle slider
|
|
*/
|
|
void updateIndicator(bool animate) {
|
|
if (selectedItem != null) {
|
|
// calculate where we want to put the triangle
|
|
scheduleMicrotask(() {
|
|
num x = selectedItem.offset.left +
|
|
selectedItem.offset.width / 2 -
|
|
TRIANGLE_WIDTH / 2;
|
|
_moveIndicator(x, animate);
|
|
});
|
|
} else {
|
|
_moveIndicator(0, animate);
|
|
}
|
|
}
|
|
|
|
void _moveIndicator(num x, bool animate) {
|
|
// find the slider filler (the div element to the left of the
|
|
// triangle) set its width the push the triangle to where we want it.
|
|
String duration = animate ? '.3s' : '0s';
|
|
final triangle = node.querySelector('.sm-triangle');
|
|
triangle.style.transitionDuration = duration;
|
|
FxUtil.setWebkitTransform(triangle, x, 0);
|
|
}
|
|
}
|