OLD | NEW |
| (Empty) |
1 // Copyright (c) 2011, the Dart project authors. Please see the AUTHORS file | |
2 // for details. All rights reserved. Use of this source code is governed by a | |
3 // BSD-style license that can be found in the LICENSE file. | |
4 | |
5 typedef void SelectHandler(String menuText); | |
6 | |
7 /** | |
8 * This implements a horizontal menu bar with a sliding triangle arrow | |
9 * that points at the currently selected item. | |
10 */ | |
11 class SliderMenu extends View { | |
12 static final int TRIANGLE_WIDTH = 24; | |
13 | |
14 // currently selected menu item | |
15 Element selectedItem; | |
16 | |
17 // This holds the element where a touchstart occured. (This is set | |
18 // in touchstart, and cleared in touchend.) If this is null, then a | |
19 // touch operation is not in progress. | |
20 // TODO(mattsh) - move this to a touch mixin | |
21 Element touchItem; | |
22 | |
23 /** | |
24 * Callback function that we call when the user chooses something from | |
25 * the menu. This is passed the menu item text. | |
26 */ | |
27 SelectHandler onSelect; | |
28 | |
29 List<String> _menuItems; | |
30 | |
31 SliderMenu(this._menuItems, this.onSelect) : super() {} | |
32 | |
33 Element render() { | |
34 // Create a div for each menu item. | |
35 final items = new StringBuffer(); | |
36 for (final item in _menuItems) { | |
37 items.add('<div class="sm-item">$item</div>'); | |
38 } | |
39 | |
40 // Create a root node to hold this view. | |
41 return new Element.html(''' | |
42 <div class="sm-root"> | |
43 <div class="sm-item-box"> | |
44 <div class="sm-item-filler"></div> | |
45 $items | |
46 <div class="sm-item-filler"></div> | |
47 </div> | |
48 <div class="sm-slider-box"> | |
49 <div class="sm-triangle"></div> | |
50 </div> | |
51 </div> | |
52 '''); | |
53 } | |
54 | |
55 void enterDocument() { | |
56 // select the first item | |
57 // todo(jacobr): too much actual work is performed in enterDocument. | |
58 // Ideally, enterDocument should do nothing more than redecorate a view | |
59 // and perhaps calculating the correct child sizes for edge cases that | |
60 // cannot be handled by the browser layout engine. | |
61 selectItem(node.query('.sm-item'), false); | |
62 | |
63 // TODO(mattsh), abstract this somehow into a touch click mixin | |
64 if (Device.supportsTouch) { | |
65 node.on.touchStart.add((event) { | |
66 touchItem = itemOfTouchEvent(event); | |
67 if (touchItem != null) { | |
68 selectItemText(touchItem); | |
69 } | |
70 event.preventDefault(); | |
71 }); | |
72 node.on.touchEnd.add((event) { | |
73 if (touchItem != null) { | |
74 if (itemOfTouchEvent(event) == touchItem) { | |
75 selectItem(touchItem, true); | |
76 } else { | |
77 // the Touch target is somewhere other where than the touchstart | |
78 // occured, so revert the selected menu text back to where it was | |
79 // before the touchstart, | |
80 selectItemText(selectedItem); | |
81 } | |
82 // touch operation has ended | |
83 touchItem = null; | |
84 } | |
85 event.preventDefault(); | |
86 }); | |
87 } else { | |
88 node.on.click.add((event) => selectItem(event.target, true)); | |
89 } | |
90 | |
91 window.on.resize.add((Event event) => updateIndicator(false)); | |
92 } | |
93 | |
94 /** | |
95 * Walks the parent chain of the first Touch target to find the first ancestor | |
96 * that has sm-item class. | |
97 */ | |
98 Element itemOfTouchEvent(event) { | |
99 Node node = event.changedTouches[0].target; | |
100 return itemOfNode(node); | |
101 } | |
102 | |
103 Element itemOfNode(Node node) { | |
104 // TODO(jmesserly): workaround for bug 5399957, document.parent == document | |
105 while (node != null && node != document) { | |
106 if (node is Element) { | |
107 Element element = node; | |
108 if (element.classes.contains('sm-item')) { | |
109 return element; | |
110 } | |
111 } | |
112 node = node.parent; | |
113 } | |
114 return null; | |
115 } | |
116 | |
117 void selectItemText(Element item) { | |
118 // unselect all menu items | |
119 for (final sliderItem in node.queryAll('.sm-item')) { | |
120 sliderItem.classes.remove('sel'); | |
121 } | |
122 | |
123 // select the item the user clicked on | |
124 item.classes.add('sel'); | |
125 } | |
126 | |
127 void selectItem(Element item, bool animate) { | |
128 if (!item.classes.contains('sm-item')) { | |
129 return; | |
130 } | |
131 | |
132 selectedItem = item; | |
133 selectItemText(item); | |
134 updateIndicator(animate); | |
135 onSelect(item.text); | |
136 } | |
137 | |
138 void selectNext(bool animate) { | |
139 final result = node.query('.sm-item.sel').nextElementSibling; | |
140 if (result != null) { | |
141 selectItem(result, animate); | |
142 } | |
143 } | |
144 | |
145 void selectPrevious(bool animate) { | |
146 final result = node.query('.sm-item.sel').previousElementSibling; | |
147 if (result != null) { | |
148 selectItem(result, animate); | |
149 } | |
150 } | |
151 | |
152 /** | |
153 * animate - if true, then animate the movement of the triangle slider | |
154 */ | |
155 void updateIndicator(bool animate) { | |
156 if (selectedItem != null) { | |
157 // calculate where we want to put the triangle | |
158 selectedItem.rect.then((ElementRect rect) { | |
159 num x = rect.offset.left + | |
160 rect.offset.width / 2 - TRIANGLE_WIDTH / 2; | |
161 _moveIndicator(x, animate); | |
162 }); | |
163 } else { | |
164 _moveIndicator(0, animate); | |
165 } | |
166 } | |
167 | |
168 void _moveIndicator(num x, bool animate) { | |
169 // find the slider filler (the div element to the left of the | |
170 // triangle) set its width the push the triangle to where we want it. | |
171 String duration = animate ? '.3s' : '0s'; | |
172 final triangle = node.query('.sm-triangle'); | |
173 triangle.style.transitionDuration = duration; | |
174 FxUtil.setWebkitTransform(triangle, x, 0); | |
175 } | |
176 } | |
OLD | NEW |