| // 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); |
| } |
| } |