OLD | NEW |
1 // Copyright (c) 2016, the Dart project authors. Please see the AUTHORS file | 1 // Copyright (c) 2016, 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 import 'dart:async'; | 5 import 'dart:async'; |
6 import 'dart:html'; | 6 import 'dart:html'; |
| 7 import 'dart:math' as math; |
7 import 'package:observatory/src/elements/helpers/rendering_scheduler.dart'; | 8 import 'package:observatory/src/elements/helpers/rendering_scheduler.dart'; |
8 import 'package:observatory/src/elements/helpers/tag.dart'; | 9 import 'package:observatory/src/elements/helpers/tag.dart'; |
9 | 10 |
10 typedef HtmlElement VirtualCollectionCreateCallback(); | 11 typedef HtmlElement VirtualCollectionCreateCallback(); |
11 typedef List<HtmlElement> VirtualCollectionHeaderCallback(); | 12 typedef List<HtmlElement> VirtualCollectionHeaderCallback(); |
12 typedef void VirtualCollectionUpdateCallback( | 13 typedef void VirtualCollectionUpdateCallback( |
13 HtmlElement el, dynamic item, int index); | 14 HtmlElement el, dynamic item, int index); |
14 | 15 |
15 class VirtualCollectionElement extends HtmlElement implements Renderable { | 16 class VirtualCollectionElement extends HtmlElement implements Renderable { |
16 static const tag = const Tag<VirtualCollectionElement>('virtual-collection'); | 17 static const tag = const Tag<VirtualCollectionElement>('virtual-collection'); |
17 | 18 |
18 RenderingScheduler<VirtualCollectionElement> _r; | 19 RenderingScheduler<VirtualCollectionElement> _r; |
19 | 20 |
20 Stream<RenderedEvent<VirtualCollectionElement>> get onRendered => | 21 Stream<RenderedEvent<VirtualCollectionElement>> get onRendered => |
21 _r.onRendered; | 22 _r.onRendered; |
22 | 23 |
23 VirtualCollectionCreateCallback _create; | 24 VirtualCollectionCreateCallback _create; |
24 VirtualCollectionHeaderCallback _createHeader; | 25 VirtualCollectionHeaderCallback _createHeader; |
25 VirtualCollectionUpdateCallback _update; | 26 VirtualCollectionUpdateCallback _update; |
26 double _itemHeight; | 27 double _itemHeight; |
27 double _headerHeight = 0.0; | |
28 int _top; | 28 int _top; |
29 double _height; | 29 double _height; |
30 List _items; | 30 List _items; |
31 StreamSubscription _onScrollSubscription; | 31 StreamSubscription _onScrollSubscription; |
32 StreamSubscription _onResizeSubscription; | 32 StreamSubscription _onResizeSubscription; |
33 | 33 |
34 List get items => _items; | 34 List get items => _items; |
35 | 35 |
36 set items(Iterable value) { | 36 set items(Iterable value) { |
37 _items = new List.unmodifiable(value); | 37 _items = new List.unmodifiable(value); |
(...skipping 19 matching lines...) Expand all Loading... |
57 } | 57 } |
58 | 58 |
59 VirtualCollectionElement.created() : super.created(); | 59 VirtualCollectionElement.created() : super.created(); |
60 | 60 |
61 @override | 61 @override |
62 attached() { | 62 attached() { |
63 super.attached(); | 63 super.attached(); |
64 _r.enable(); | 64 _r.enable(); |
65 _top = null; | 65 _top = null; |
66 _itemHeight = null; | 66 _itemHeight = null; |
67 _onScrollSubscription = onScroll.listen(_onScroll); | 67 _onScrollSubscription = _viewport.onScroll.listen(_onScroll); |
68 _onResizeSubscription = window.onResize.listen(_onResize); | 68 _onResizeSubscription = window.onResize.listen(_onResize); |
69 } | 69 } |
70 | 70 |
71 @override | 71 @override |
72 detached() { | 72 detached() { |
73 super.detached(); | 73 super.detached(); |
74 _r.disable(notify: true); | 74 _r.disable(notify: true); |
75 children = const []; | 75 children = const []; |
76 _onScrollSubscription.cancel(); | 76 _onScrollSubscription.cancel(); |
77 _onResizeSubscription.cancel(); | 77 _onResizeSubscription.cancel(); |
78 } | 78 } |
79 | 79 |
80 final DivElement _header = new DivElement()..classes = ['header']; | 80 DivElement _header; |
81 final DivElement _scroller = new DivElement()..classes = ['scroller']; | 81 final DivElement _viewport = new DivElement() |
82 final DivElement _shifter = new DivElement()..classes = ['shifter']; | 82 ..classes = ['viewport', 'container']; |
83 final DivElement _container = new DivElement()..classes = ['container']; | 83 final DivElement _spacer = new DivElement()..classes = ['spacer']; |
| 84 final DivElement _buffer = new DivElement()..classes = ['buffer']; |
84 | 85 |
85 dynamic getItemFromElement(HtmlElement element) { | 86 dynamic getItemFromElement(HtmlElement element) { |
86 final el_index = _container.children.indexOf(element); | 87 final el_index = _buffer.children.indexOf(element); |
87 if (el_index < 0) { | 88 if (el_index < 0) { |
88 return null; | 89 return null; |
89 } | 90 } |
90 final item_index = _top + | 91 final item_index = |
91 el_index - | 92 _top + el_index - (_buffer.children.length * _inverse_preload).floor(); |
92 (_container.children.length * _inverse_preload).floor(); | |
93 if (0 <= item_index && item_index < items.length) { | 93 if (0 <= item_index && item_index < items.length) { |
94 return _items[item_index]; | 94 return _items[item_index]; |
95 } | 95 } |
96 return null; | 96 return null; |
97 } | 97 } |
98 | 98 |
99 /// The preloaded element before and after the visible area are: | 99 /// The preloaded element before and after the visible area are: |
100 /// 1/preload_size of the number of items in the visble area. | 100 /// 1/preload_size of the number of items in the visble area. |
101 /// See shared.css for the "top:-25%;". | |
102 static const int _preload = 2; | 101 static const int _preload = 2; |
103 | 102 |
104 /// L = length of all the elements loaded | 103 /// L = length of all the elements loaded |
105 /// l = length of the visible area | 104 /// l = length of the visible area |
106 /// | 105 /// |
107 /// L = l + 2 * l / _preload | 106 /// L = l + 2 * l / _preload |
108 /// l = L * _preload / (_preload + 2) | 107 /// l = L * _preload / (_preload + 2) |
109 /// | 108 /// |
110 /// tail = l / _preload = L * 1 / (_preload + 2) = L * _inverse_preload | 109 /// tail = l / _preload = L * 1 / (_preload + 2) = L * _inverse_preload |
111 static const double _inverse_preload = 1 / (_preload + 2); | 110 static const double _inverse_preload = 1 / (_preload + 2); |
112 | 111 |
113 var _takeIntoView; | 112 var _takeIntoView; |
114 | 113 |
115 void takeIntoView(item) { | 114 void takeIntoView(item) { |
116 _takeIntoView = item; | 115 _takeIntoView = item; |
117 _r.dirty(); | 116 _r.dirty(); |
118 } | 117 } |
119 | 118 |
120 void render() { | 119 void render() { |
121 if (children.isEmpty) { | 120 if (children.isEmpty) { |
122 children = [ | 121 children = [ |
123 _scroller | 122 _viewport |
124 ..children = [ | 123 ..children = [ |
125 _shifter | 124 _spacer |
126 ..children = [ | 125 ..children = [ |
127 _container..children = [_create()] | 126 _buffer..children = [_create()] |
128 ] | 127 ], |
129 ], | 128 ] |
130 ]; | 129 ]; |
131 if (_createHeader != null) { | 130 if (_createHeader != null) { |
132 _header.children = [ | 131 _header = new DivElement() |
133 new DivElement() | 132 ..classes = ['header', 'container'] |
134 ..classes = ['container'] | 133 ..children = _createHeader(); |
135 ..children = _createHeader() | 134 children.insert(0, _header); |
136 ]; | 135 final rect = _header.getBoundingClientRect(); |
137 _scroller.children.insert(0, _header); | 136 _header.classes.add('attached'); |
138 _headerHeight = _header.children[0].getBoundingClientRect().height; | 137 _viewport.style.top = '${rect.height}px'; |
| 138 final width = _header.children.fold(0, _foldWidth); |
| 139 _buffer.style.minWidth = '${width}px'; |
139 } | 140 } |
140 _itemHeight = _container.children[0].getBoundingClientRect().height; | 141 _itemHeight = _buffer.children[0].getBoundingClientRect().height; |
141 _height = getBoundingClientRect().height; | 142 _height = getBoundingClientRect().height; |
142 } | 143 } |
143 | 144 |
144 if (_takeIntoView != null) { | 145 if (_takeIntoView != null) { |
145 final index = items.indexOf(_takeIntoView); | 146 final index = items.indexOf(_takeIntoView); |
146 if (index >= 0) { | 147 if (index >= 0) { |
147 final minScrollTop = _itemHeight * (index + 1) - _height; | 148 final minScrollTop = _itemHeight * (index + 1) - _height; |
148 final maxScrollTop = _itemHeight * index; | 149 final maxScrollTop = _itemHeight * index; |
149 scrollTop = ((maxScrollTop - minScrollTop) / 2 + minScrollTop).floor(); | 150 _viewport.scrollTop = |
| 151 ((maxScrollTop - minScrollTop) / 2 + minScrollTop).floor(); |
150 } | 152 } |
151 _takeIntoView = null; | 153 _takeIntoView = null; |
152 } | 154 } |
153 | 155 |
154 final top = (scrollTop / _itemHeight).floor(); | 156 final top = (_viewport.scrollTop / _itemHeight).floor(); |
155 | 157 |
156 _updateHeader(); | 158 _spacer.style.height = '${_itemHeight*(_items.length)}px'; |
157 _scroller.style.height = '${_itemHeight*(_items.length)+_headerHeight}px'; | |
158 final tail_length = (_height / _itemHeight / _preload).ceil(); | 159 final tail_length = (_height / _itemHeight / _preload).ceil(); |
159 final length = tail_length * 2 + tail_length * _preload; | 160 final length = tail_length * 2 + tail_length * _preload; |
160 | 161 |
161 if (_container.children.length < length) { | 162 if (_buffer.children.length < length) { |
162 while (_container.children.length != length) { | 163 while (_buffer.children.length != length) { |
163 var e = _create(); | 164 var e = _create(); |
164 e..style.display = 'hidden'; | 165 e..style.display = 'hidden'; |
165 _container.children.add(e); | 166 _buffer.children.add(e); |
166 } | 167 } |
167 _top = null; // force update; | 168 _top = null; // force update; |
168 } | 169 } |
169 | 170 |
170 if ((_top == null) || ((top - _top).abs() >= tail_length)) { | 171 if ((_top == null) || ((top - _top).abs() >= tail_length)) { |
171 _shifter.style.top = '${_itemHeight*(top-tail_length)}px'; | 172 _buffer.style.top = '${_itemHeight*(top-tail_length)}px'; |
172 int i = top - tail_length; | 173 int i = top - tail_length; |
173 for (final HtmlElement e in _container.children) { | 174 for (final HtmlElement e in _buffer.children) { |
174 if (0 <= i && i < _items.length) { | 175 if (0 <= i && i < _items.length) { |
175 e..style.display = null; | 176 e..style.display = null; |
176 _update(e, _items[i], i); | 177 _update(e, _items[i], i); |
177 } else { | 178 } else { |
178 e.style.display = 'hidden'; | 179 e.style.display = 'hidden'; |
179 } | 180 } |
180 i++; | 181 i++; |
181 } | 182 } |
182 _top = top; | 183 _top = top; |
183 } | 184 } |
| 185 _updateHeader(); |
| 186 } |
| 187 |
| 188 double _foldWidth(double value, HtmlElement child) { |
| 189 return math.max(value, child.getBoundingClientRect().width); |
184 } | 190 } |
185 | 191 |
186 void _updateHeader() { | 192 void _updateHeader() { |
187 _header.style.top = '${scrollTop}px'; | 193 if (_header != null) { |
| 194 _header.style.left = '${-_viewport.scrollLeft}px'; |
| 195 final width = _buffer.getBoundingClientRect().width; |
| 196 _header.children.last.style.width = '${width}px'; |
| 197 } |
188 } | 198 } |
189 | 199 |
190 void _onScroll(_) { | 200 void _onScroll(_) { |
191 // needed to avoid flickering | 201 _r.dirty(); |
| 202 // We anticipate the header in advance to avoid flickering |
192 _updateHeader(); | 203 _updateHeader(); |
193 _r.dirty(); | |
194 } | 204 } |
195 | 205 |
196 void _onResize(_) { | 206 void _onResize(_) { |
197 final newHeight = getBoundingClientRect().height; | 207 final newHeight = getBoundingClientRect().height; |
198 if (newHeight > _height) { | 208 if (newHeight > _height) { |
199 _height = newHeight; | 209 _height = newHeight; |
200 _r.dirty(); | 210 _r.dirty(); |
| 211 } else { |
| 212 // Even if we are not updating the structure the computed size is going to |
| 213 // change |
| 214 _updateHeader(); |
201 } | 215 } |
202 } | 216 } |
203 } | 217 } |
OLD | NEW |