| 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 |