blob: b6f91b96661e63c525cdf4136b9ee12abbeaf86c [file] [log] [blame]
// Copyright (c) 2016, 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.
import 'dart:async';
import 'dart:html';
import 'package:observatory/src/elements/helpers/rendering_scheduler.dart';
import 'package:observatory/src/elements/helpers/tag.dart';
typedef HtmlElement VirtualCollectionCreateCallback();
typedef void VirtualCollectionUpdateCallback(HtmlElement el, dynamic item,
int index);
class VirtualCollectionElement extends HtmlElement implements Renderable {
static const tag =
const Tag<VirtualCollectionElement>('virtual-collection');
RenderingScheduler<VirtualCollectionElement> _r;
Stream<RenderedEvent<VirtualCollectionElement>> get onRendered =>
_r.onRendered;
VirtualCollectionCreateCallback _create;
VirtualCollectionCreateCallback _createHeader;
VirtualCollectionUpdateCallback _update;
double _itemHeight;
int _top;
double _height;
List _items;
StreamSubscription _onScrollSubscription;
StreamSubscription _onResizeSubscription;
List get items => _items;
set items(Iterable value) {
_items = new List.unmodifiable(value);
_top = null;
_r.dirty();
}
factory VirtualCollectionElement(VirtualCollectionCreateCallback create,
VirtualCollectionUpdateCallback update, {Iterable items: const [],
VirtualCollectionCreateCallback createHeader,
RenderingQueue queue}) {
assert(create != null);
assert(update != null);
assert(items != null);
VirtualCollectionElement e = document.createElement(tag.name);
e._r = new RenderingScheduler(e, queue: queue);
e._create = create;
e._createHeader = createHeader;
e._update = update;
e._items = new List.unmodifiable(items);
return e;
}
VirtualCollectionElement.created() : super.created();
@override
attached() {
super.attached();
_r.enable();
_top = null;
_itemHeight = null;
_onScrollSubscription = onScroll.listen(_onScroll);
_onResizeSubscription = window.onResize.listen(_onResize);
}
@override
detached() {
super.detached();
_r.disable(notify: true);
children = const [];
_onScrollSubscription.cancel();
_onResizeSubscription.cancel();
}
final DivElement _header = new DivElement()..classes = const ['header'];
final DivElement _scroller = new DivElement()..classes = const ['scroller'];
final DivElement _shifter = new DivElement()..classes = const ['shifter'];
dynamic getItemFromElement(HtmlElement element) {
final el_index = _shifter.children.indexOf(element);
if (el_index < 0) {
return null;
}
final item_index =
_top + el_index - (_shifter.children.length * _inverse_preload).floor();
if (0 <= item_index && item_index < items.length) {
return _items[item_index];
}
return null;
}
/// The preloaded element before and after the visible area are:
/// 1/preload_size of the number of items in the visble area.
/// See shared.css for the "top:-25%;".
static const int _preload = 2;
/// L = length of all the elements loaded
/// l = length of the visible area
///
/// L = l + 2 * l / _preload
/// l = L * _preload / (_preload + 2)
///
/// tail = l / _preload = L * 1 / (_preload + 2) = L * _inverse_preload
static const double _inverse_preload = 1 / (_preload + 2);
void render() {
if (children.isEmpty) {
children = [
_scroller
..children = [
_shifter
..children = [_create()]
],
];
if (_createHeader != null) {
_header.children = [_createHeader()];
_scroller.children.insert(0, _header);
}
_itemHeight = _shifter.children[0].getBoundingClientRect().height;
_height = getBoundingClientRect().height;
}
final top = (scrollTop / _itemHeight).floor();
_header.style.top = '${scrollTop}px';
_scroller.style.height = '${_itemHeight*(_items.length)}px';
final tail_length = (_height / _itemHeight / _preload).ceil();
final length = tail_length * 2 + tail_length * _preload;
if (_shifter.children.length < length) {
while (_shifter.children.length != length) {
var e = _create();
e..style.display = 'hidden';
_shifter.children.add(e);
}
_top = null; // force update;
}
if ((_top == null) || ((top - _top).abs() >= tail_length)) {
_shifter.style.top = '${_itemHeight*(top-tail_length)}px';
int i = top - tail_length;
for (final HtmlElement e in _shifter.children) {
if (0 <= i && i < _items.length) {
e..style.display = null;
_update(e, _items[i], i);
} else {
e.style.display = 'hidden';
}
i++;
}
_top = top;
}
}
void _onScroll(_) {
_r.dirty();
}
void _onResize(_) {
final newHeight = getBoundingClientRect().height;
if (newHeight > _height) {
_height = newHeight;
_r.dirty();
}
}
}