blob: bd2d57f2fe3527514c94da7f00357c7074cdbbf7 [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.
// @dart = 2.9
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);
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);
}
}