OLD | NEW |
1 // Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file | 1 // Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file |
2 // for details. All rights reserved. Use of this source code is governed by a | 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. | 3 // BSD-style license that can be found in the LICENSE file. |
4 | 4 |
5 part of app; | 5 part of app; |
6 | 6 |
7 abstract class TableTreeRow extends Observable { | |
8 static const arrowRight = '\u2192'; | |
9 static const arrowDownRight = '\u21b3'; | |
10 // Number of ems each subtree is indented. | |
11 static const subtreeIndent = 2; | |
12 | |
13 TableTreeRow(this.tree, TableTreeRow parent) : | |
14 parent = parent, | |
15 depth = parent != null ? parent.depth + 1 : 0 { | |
16 } | |
17 | |
18 final TableTree tree; | |
19 final TableTreeRow parent; | |
20 final int depth; | |
21 final List<TableTreeRow> children = new List<TableTreeRow>(); | |
22 final List<TableCellElement> _tableColumns = new List<TableCellElement>(); | |
23 final List<DivElement> flexColumns = new List<DivElement>(); | |
24 final List<StreamSubscription> listeners = new List<StreamSubscription>(); | |
25 | |
26 SpanElement _expander; | |
27 TableRowElement _tr; | |
28 TableRowElement get tr => _tr; | |
29 bool _expanded = false; | |
30 bool get expanded => _expanded; | |
31 set expanded(bool expanded) { | |
32 var changed = _expanded != expanded; | |
33 _expanded = expanded; | |
34 if (changed) { | |
35 // If the state has changed, fire callbacks. | |
36 if (_expanded) { | |
37 _onExpand(); | |
38 } else { | |
39 _onCollapse(); | |
40 } | |
41 } | |
42 } | |
43 | |
44 /// Fired when the tree row is being expanded. | |
45 void _onExpand() { | |
46 _updateExpanderView(); | |
47 } | |
48 | |
49 /// Fired when the tree row is being collapsed. | |
50 void _onCollapse() { | |
51 for (var child in children) { | |
52 child.onHide(); | |
53 } | |
54 _updateExpanderView(); | |
55 } | |
56 | |
57 bool toggle() { | |
58 expanded = !expanded; | |
59 return expanded; | |
60 } | |
61 | |
62 HtmlElement _makeColorBlock(String backgroundColor) { | |
63 var colorBlock = new DivElement(); | |
64 colorBlock.style.minWidth = '2px'; | |
65 colorBlock.style.backgroundColor = backgroundColor; | |
66 return colorBlock; | |
67 } | |
68 | |
69 HtmlElement _makeExpander() { | |
70 var expander = new SpanElement(); | |
71 expander.style.minWidth = '24px'; | |
72 expander.style.minHeight = '24px'; | |
73 listeners.add(expander.onClick.listen(onClick)); | |
74 return expander; | |
75 } | |
76 | |
77 void _cleanUpListeners() { | |
78 for (var i = 0; i < listeners.length; i++) { | |
79 listeners[i].cancel(); | |
80 } | |
81 listeners.clear(); | |
82 } | |
83 | |
84 void onClick(Event e) { | |
85 e.stopPropagation(); | |
86 tree.toggle(this); | |
87 } | |
88 | |
89 static const redColor = '#F44336'; | |
90 static const blueColor = '#3F51B5'; | |
91 static const purpleColor = '#673AB7'; | |
92 static const greenColor = '#4CAF50'; | |
93 static const orangeColor = '#FF9800'; | |
94 static const lightGrayColor = '#FAFAFA'; | |
95 | |
96 void _buildRow() { | |
97 const List backgroundColors = const [ | |
98 purpleColor, | |
99 redColor, | |
100 greenColor, | |
101 blueColor, | |
102 orangeColor, | |
103 ]; | |
104 _tr = new TableRowElement(); | |
105 for (var i = 0; i < tree.columnCount; i++) { | |
106 var cell = _tr.insertCell(-1); | |
107 _tableColumns.add(cell); | |
108 var flex = new DivElement(); | |
109 flex.classes.add('flex-row'); | |
110 cell.children.add(flex); | |
111 flexColumns.add(flex); | |
112 } | |
113 var firstColumn = flexColumns[0]; | |
114 _tableColumns[0].style.paddingLeft = '${(depth - 1) * subtreeIndent}em'; | |
115 var backgroundColor = lightGrayColor; | |
116 if (depth > 1) { | |
117 var colorIndex = (depth - 1) % backgroundColors.length; | |
118 backgroundColor = backgroundColors[colorIndex]; | |
119 } | |
120 var colorBlock = _makeColorBlock(backgroundColor); | |
121 firstColumn.children.add(colorBlock); | |
122 _expander = _makeExpander(); | |
123 firstColumn.children.add(_expander); | |
124 // Enable expansion by clicking anywhere on the first column. | |
125 listeners.add(firstColumn.onClick.listen(onClick)); | |
126 _updateExpanderView(); | |
127 } | |
128 | |
129 void _updateExpanderView() { | |
130 if (_expander == null) { | |
131 return; | |
132 } | |
133 if (!hasChildren()) { | |
134 _expander.style.visibility = 'hidden'; | |
135 _expander.classes.remove('pointer'); | |
136 return; | |
137 } else { | |
138 _expander.style.visibility = 'visible'; | |
139 _expander.classes.add('pointer'); | |
140 } | |
141 _expander.children.clear(); | |
142 _expander.children.add(expanded ? | |
143 new Element.tag('icon-expand-more') : | |
144 new Element.tag('icon-chevron-right')); | |
145 } | |
146 | |
147 bool hasChildren(); | |
148 | |
149 /// Fired when the tree row is being shown. | |
150 /// Populate tr and add logical children here. | |
151 void onShow() { | |
152 assert(_tr == null); | |
153 _buildRow(); | |
154 } | |
155 | |
156 /// Fired when the tree row is being hidden. | |
157 void onHide() { | |
158 _tr = null; | |
159 _expander = null; | |
160 if (_tableColumns != null) { | |
161 _tableColumns.clear(); | |
162 } | |
163 if (flexColumns != null) { | |
164 flexColumns.clear(); | |
165 } | |
166 _cleanUpListeners(); | |
167 } | |
168 } | |
169 | |
170 class TableTree extends Observable { | |
171 final TableSectionElement tableBody; | |
172 final List<TableTreeRow> rows = []; | |
173 final int columnCount; | |
174 Future _pendingOperation; | |
175 /// Create a table tree with column [headers]. | |
176 TableTree(this.tableBody, this.columnCount); | |
177 | |
178 void clear() { | |
179 tableBody.children.clear(); | |
180 for (var i = 0; i < rows.length; i++) { | |
181 rows[i]._cleanUpListeners(); | |
182 } | |
183 rows.clear(); | |
184 } | |
185 | |
186 /// Initialize the table tree with the list of root children. | |
187 void initialize(TableTreeRow root) { | |
188 clear(); | |
189 root.onShow(); | |
190 toggle(root); | |
191 } | |
192 | |
193 /// Toggle expansion of row in tree. | |
194 toggle(TableTreeRow row) async { | |
195 if (_pendingOperation != null) { | |
196 return; | |
197 } | |
198 if (row.toggle()) { | |
199 document.body.classes.add('busy'); | |
200 _pendingOperation = _expand(row); | |
201 await _pendingOperation; | |
202 _pendingOperation = null; | |
203 document.body.classes.remove('busy'); | |
204 if (row.children.length == 1) { | |
205 // Auto expand single child. | |
206 await toggle(row.children[0]); | |
207 } | |
208 } else { | |
209 document.body.classes.add('busy'); | |
210 _pendingOperation = _collapse(row); | |
211 await _pendingOperation; | |
212 _pendingOperation = null; | |
213 document.body.classes.remove('busy'); | |
214 } | |
215 } | |
216 | |
217 int _index(TableTreeRow row) => rows.indexOf(row); | |
218 | |
219 _insertRow(index, child) { | |
220 rows.insert(index, child); | |
221 tableBody.children.insert(index, child.tr); | |
222 } | |
223 | |
224 _expand(TableTreeRow row) async { | |
225 int index = _index(row); | |
226 if ((index == -1) && (rows.length != 0)) { | |
227 return; | |
228 } | |
229 assert((index != -1) || (rows.length == 0)); | |
230 var i = 0; | |
231 var addPerIteration = 10; | |
232 while (i < row.children.length) { | |
233 await window.animationFrame; | |
234 for (var j = 0; j < addPerIteration; j++) { | |
235 if (i == row.children.length) { | |
236 break; | |
237 } | |
238 var child = row.children[i]; | |
239 child.onShow(); | |
240 child._updateExpanderView(); | |
241 _insertRow(index + i + 1, child); | |
242 i++; | |
243 } | |
244 } | |
245 } | |
246 | |
247 _collapseSync(TableTreeRow row) { | |
248 var childCount = row.children.length; | |
249 if (childCount == 0) { | |
250 return; | |
251 } | |
252 for (var i = 0; i < childCount; i++) { | |
253 // Close all inner rows. | |
254 if (row.children[i].expanded) { | |
255 _collapseSync(row.children[i]); | |
256 } | |
257 } | |
258 // Collapse this row. | |
259 row.expanded = false; | |
260 // Remove all children. | |
261 int index = _index(row); | |
262 for (var i = 0; i < childCount; i++) { | |
263 rows.removeAt(index + 1); | |
264 tableBody.children.removeAt(index + 1); | |
265 } | |
266 } | |
267 | |
268 _collapse(TableTreeRow row) async { | |
269 _collapseSync(row); | |
270 } | |
271 } | |
272 | |
273 typedef String ValueFormatter(dynamic value); | 7 typedef String ValueFormatter(dynamic value); |
274 | 8 |
275 class SortedTableColumn { | 9 class SortedTableColumn { |
276 static String toStringFormatter(dynamic v) { | 10 static String toStringFormatter(dynamic v) { |
277 return v != null ? v.toString() : '<null>'; | 11 return v != null ? v.toString() : '<null>'; |
278 } | 12 } |
279 final String label; | 13 final String label; |
280 final ValueFormatter formatter; | 14 final ValueFormatter formatter; |
281 SortedTableColumn.withFormatter(this.label, this.formatter); | 15 SortedTableColumn.withFormatter(this.label, this.formatter); |
282 SortedTableColumn(this.label) | 16 SortedTableColumn(this.label) |
283 : formatter = toStringFormatter; | 17 : formatter = toStringFormatter; |
284 } | 18 } |
285 | 19 |
286 class SortedTableRow { | 20 class SortedTableRow { |
287 final List values; | 21 final List values; |
288 SortedTableRow(this.values); | 22 SortedTableRow(this.values); |
289 } | 23 } |
290 | 24 |
291 class SortedTable extends Observable { | 25 class SortedTable { |
292 final List<SortedTableColumn> columns; | 26 final List<SortedTableColumn> columns; |
293 final List<SortedTableRow> rows = new List<SortedTableRow>(); | 27 final List<SortedTableRow> rows = new List<SortedTableRow>(); |
294 final List<int> sortedRows = []; | 28 final List<int> sortedRows = []; |
295 | 29 |
296 SortedTable(this.columns); | 30 SortedTable(this.columns); |
297 | 31 |
298 int _sortColumnIndex = 0; | 32 int _sortColumnIndex = 0; |
299 set sortColumnIndex(var index) { | 33 set sortColumnIndex(var index) { |
300 assert(index >= 0); | 34 assert(index >= 0); |
301 assert(index < columns.length); | 35 assert(index < columns.length); |
302 _sortColumnIndex = index; | 36 _sortColumnIndex = index; |
303 notifyPropertyChange(#getColumnLabel, 0, 1); | |
304 } | 37 } |
305 int get sortColumnIndex => _sortColumnIndex; | 38 int get sortColumnIndex => _sortColumnIndex; |
306 bool _sortDescending = true; | 39 bool _sortDescending = true; |
307 bool get sortDescending => _sortDescending; | 40 bool get sortDescending => _sortDescending; |
308 set sortDescending(var descending) { | 41 set sortDescending(var descending) { |
309 _sortDescending = descending; | 42 _sortDescending = descending; |
310 notifyPropertyChange(#getColumnLabel, 0, 1); | |
311 } | 43 } |
312 | 44 |
313 | 45 |
314 dynamic getSortKeyFor(int row, int col) { | 46 dynamic getSortKeyFor(int row, int col) { |
315 return rows[row].values[col]; | 47 return rows[row].values[col]; |
316 } | 48 } |
317 | 49 |
318 int _sortFuncDescending(int i, int j) { | 50 int _sortFuncDescending(int i, int j) { |
319 var a = getSortKeyFor(i, _sortColumnIndex); | 51 var a = getSortKeyFor(i, _sortColumnIndex); |
320 var b = getSortKeyFor(j, _sortColumnIndex); | 52 var b = getSortKeyFor(j, _sortColumnIndex); |
(...skipping 25 matching lines...) Expand all Loading... |
346 sortedRows.add(rows.length); | 78 sortedRows.add(rows.length); |
347 rows.add(row); | 79 rows.add(row); |
348 } | 80 } |
349 | 81 |
350 String getFormattedValue(int row, int column) { | 82 String getFormattedValue(int row, int column) { |
351 var value = getValue(row, column); | 83 var value = getValue(row, column); |
352 var formatter = columns[column].formatter; | 84 var formatter = columns[column].formatter; |
353 return formatter(value); | 85 return formatter(value); |
354 } | 86 } |
355 | 87 |
356 @observable String getColumnLabel(int column) { | 88 String getColumnLabel(int column) { |
357 assert(column >= 0); | 89 assert(column >= 0); |
358 assert(column < columns.length); | 90 assert(column < columns.length); |
359 // TODO(johnmccutchan): Move expander display decisions into html once | 91 // TODO(johnmccutchan): Move expander display decisions into html once |
360 // tables and templates are better supported. | 92 // tables and templates are better supported. |
361 const arrowUp = '\u25BC'; | 93 const arrowUp = '\u25BC'; |
362 const arrowDown = '\u25B2'; | 94 const arrowDown = '\u25B2'; |
363 if (column != _sortColumnIndex) { | 95 if (column != _sortColumnIndex) { |
364 return columns[column].label + '\u2003'; | 96 return columns[column].label + '\u2003'; |
365 } | 97 } |
366 return columns[column].label + (_sortDescending ? arrowUp : arrowDown); | 98 return columns[column].label + (_sortDescending ? arrowUp : arrowDown); |
367 } | 99 } |
368 | 100 |
369 dynamic getValue(int row, int column) { | 101 dynamic getValue(int row, int column) { |
370 return rows[row].values[column]; | 102 return rows[row].values[column]; |
371 } | 103 } |
372 } | 104 } |
OLD | NEW |