blob: 28bc24d48bd7e5acc0882b0926b7fc8ec1c18e79 [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 'dart:math' as math;
import 'package:observatory_2/src/elements/containers/search_bar.dart';
import 'package:observatory_2/src/elements/helpers/rendering_scheduler.dart';
import 'package:observatory_2/src/elements/helpers/custom_element.dart';
typedef HtmlElement VirtualCollectionCreateCallback();
typedef List<HtmlElement> VirtualCollectionHeaderCallback();
typedef void VirtualCollectionUpdateCallback(
HtmlElement el, dynamic item, int index);
typedef bool VirtualCollectionSearchCallback(Pattern pattern, dynamic item);
class VirtualCollectionElement extends CustomElement implements Renderable {
RenderingScheduler<VirtualCollectionElement> _r;
Stream<RenderedEvent<VirtualCollectionElement>> get onRendered =>
_r.onRendered;
VirtualCollectionCreateCallback _create;
VirtualCollectionHeaderCallback _createHeader;
VirtualCollectionUpdateCallback _update;
VirtualCollectionSearchCallback _search;
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;
_searcher?.update();
_r.dirty();
}
factory VirtualCollectionElement(VirtualCollectionCreateCallback create,
VirtualCollectionUpdateCallback update,
{Iterable items: const [],
VirtualCollectionHeaderCallback createHeader,
VirtualCollectionSearchCallback search,
RenderingQueue queue}) {
assert(create != null);
assert(update != null);
assert(items != null);
VirtualCollectionElement e = new VirtualCollectionElement.created();
e._r = new RenderingScheduler<VirtualCollectionElement>(e, queue: queue);
e._create = create;
e._createHeader = createHeader;
e._update = update;
e._search = search;
e._items = new List.unmodifiable(items);
return e;
}
VirtualCollectionElement.created() : super.created('virtual-collection');
@override
attached() {
super.attached();
_r.enable();
_top = null;
_itemHeight = null;
_onScrollSubscription = _viewport.onScroll.listen(_onScroll);
_onResizeSubscription = window.onResize.listen(_onResize);
}
@override
detached() {
super.detached();
_r.disable(notify: true);
children = const [];
_onScrollSubscription.cancel();
_onResizeSubscription.cancel();
}
DivElement _header;
SearchBarElement _searcher;
final DivElement _viewport = new DivElement()
..classes = ['viewport', 'container'];
final DivElement _spacer = new DivElement()..classes = ['spacer'];
final DivElement _buffer = new DivElement()..classes = ['buffer'];
static int safeFloor(double x) {
if (x.isNaN) return 0;
return x.floor();
}
static int safeCeil(double x) {
if (x.isNaN) return 0;
return x.ceil();
}
dynamic getItemFromElement(HtmlElement element) {
final el_index = _buffer.children.indexOf(element);
if (el_index < 0) {
return null;
}
final item_index =
_top + el_index - safeFloor(_buffer.children.length * _inverse_preload);
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.
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);
var _takeIntoView;
void takeIntoView(item) {
_takeIntoView = item;
_r.dirty();
}
void render() {
if (children.isEmpty) {
children = <Element>[
_viewport
..children = <Element>[
_spacer
..children = <Element>[
_buffer..children = <Element>[_create()]
],
]
];
if (_search != null) {
_searcher =
_searcher ?? new SearchBarElement(_doSearch, queue: _r.queue)
..onSearchResultSelected.listen((e) {
takeIntoView(e.item);
});
children.insert(0, _searcher.element);
}
if (_createHeader != null) {
_header = new DivElement()
..classes = ['header', 'container']
..children = _createHeader();
children.insert(0, _header);
final rect = _header.getBoundingClientRect();
_header.classes.add('attached');
_viewport.style.top = '${rect.height}px';
final width = _header.children.fold(0.0, _foldWidth);
_buffer.style.minWidth = '${width}px';
}
_itemHeight = _buffer.children[0].getBoundingClientRect().height;
_height = getBoundingClientRect().height;
}
if (_takeIntoView != null) {
final index = items.indexOf(_takeIntoView);
if (index >= 0) {
final minScrollTop = _itemHeight * (index + 1) - _height;
final maxScrollTop = _itemHeight * index;
_viewport.scrollTop =
safeFloor((maxScrollTop - minScrollTop) / 2 + minScrollTop);
}
_takeIntoView = null;
}
final top = safeFloor(_viewport.scrollTop / _itemHeight);
_spacer.style.height = '${_itemHeight * (_items.length)}px';
final tail_length = safeCeil(_height / _itemHeight / _preload);
final length = tail_length * 2 + tail_length * _preload;
if (_buffer.children.length < length) {
while (_buffer.children.length != length) {
var e = _create();
e..style.display = 'hidden';
_buffer.children.add(e);
}
_top = null; // force update;
}
if ((_top == null) || ((top - _top).abs() >= tail_length)) {
_buffer.style.top = '${_itemHeight * (top - tail_length)}px';
int i = top - tail_length;
for (final HtmlElement e in _buffer.children) {
if (0 <= i && i < _items.length) {
e.style.display = null;
_update(e, _items[i], i);
} else {
e.style.display = 'hidden';
}
i++;
}
_top = top;
}
if (_searcher != null) {
final current = _searcher.current;
int i = _top - tail_length;
for (final HtmlElement e in _buffer.children) {
if (0 <= i && i < _items.length) {
if (_items[i] == current) {
e.classes.add('marked');
} else {
e.classes.remove('marked');
}
}
i++;
}
}
_updateHeader();
}
double _foldWidth(double value, Element child) {
return math.max(value, child.getBoundingClientRect().width);
}
void _updateHeader() {
if (_header != null) {
_header.style.left = '${-_viewport.scrollLeft}px';
final width = _buffer.getBoundingClientRect().width;
_header.children.last.style.width = '${width}px';
}
}
void _onScroll(_) {
_r.dirty();
// We anticipate the header in advance to avoid flickering
_updateHeader();
}
void _onResize(_) {
final newHeight = getBoundingClientRect().height;
if (newHeight > _height) {
_height = newHeight;
_r.dirty();
} else {
// Even if we are not updating the structure the computed size is going to
// change
_updateHeader();
}
}
Iterable<dynamic> _doSearch(Pattern search) {
return _items.where((item) => _search(search, item));
}
}