| // 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:collection'; |
| import 'dart:math' as Math; |
| import 'package:observatory/src/elements/containers/virtual_collection.dart'; |
| import 'package:observatory/src/elements/helpers/rendering_scheduler.dart'; |
| import 'package:observatory/src/elements/helpers/tag.dart'; |
| |
| typedef HtmlElement VirtualTreeCreateCallback( |
| toggle({bool autoToggleSingleChildNodes, bool autoToggleWholeTree})); |
| typedef void VirtualTreeUpdateCallback(HtmlElement el, dynamic item, int depth); |
| typedef Iterable<dynamic> VritualTreeGetChildrenCallback(dynamic value); |
| typedef bool VirtualTreeSearchCallback(Pattern pattern, dynamic item); |
| |
| void virtualTreeUpdateLines(SpanElement element, int n) { |
| n = Math.max(0, n); |
| while (element.children.length > n) { |
| element.children.removeLast(); |
| } |
| while (element.children.length < n) { |
| element.children.add(new SpanElement()); |
| } |
| } |
| |
| class VirtualTreeElement extends HtmlElement implements Renderable { |
| static const tag = const Tag<VirtualTreeElement>('virtual-tree', |
| dependencies: const [VirtualCollectionElement.tag]); |
| |
| RenderingScheduler<VirtualTreeElement> _r; |
| |
| Stream<RenderedEvent<VirtualTreeElement>> get onRendered => _r.onRendered; |
| |
| VritualTreeGetChildrenCallback _children; |
| List _items; |
| List _depths; |
| final Set _expanded = new Set(); |
| |
| List get items => _items; |
| |
| set items(Iterable value) { |
| _items = new List.unmodifiable(value); |
| _expanded.clear(); |
| _r.dirty(); |
| } |
| |
| factory VirtualTreeElement(VirtualTreeCreateCallback create, |
| VirtualTreeUpdateCallback update, VritualTreeGetChildrenCallback children, |
| {Iterable items: const [], |
| VirtualTreeSearchCallback search, |
| RenderingQueue queue}) { |
| assert(create != null); |
| assert(update != null); |
| assert(children != null); |
| assert(items != null); |
| VirtualTreeElement e = document.createElement(tag.name); |
| e._r = new RenderingScheduler<VirtualTreeElement>(e, queue: queue); |
| e._children = children; |
| e._collection = new VirtualCollectionElement(() { |
| var element; |
| return element = create(( |
| {bool autoToggleSingleChildNodes: false, |
| bool autoToggleWholeTree: false}) { |
| var item = e._collection.getItemFromElement(element); |
| if (e.isExpanded(item)) { |
| e.collapse(item, |
| autoCollapseWholeTree: autoToggleWholeTree, |
| autoCollapseSingleChildNodes: autoToggleSingleChildNodes); |
| } else { |
| e.expand(item, |
| autoExpandWholeTree: autoToggleWholeTree, |
| autoExpandSingleChildNodes: autoToggleSingleChildNodes); |
| } |
| }); |
| }, (HtmlElement el, dynamic item, int index) { |
| update(el, item, e._depths[index]); |
| }, search: search, queue: queue); |
| e._items = new List.unmodifiable(items); |
| return e; |
| } |
| |
| VirtualTreeElement.created() : super.created(); |
| |
| bool isExpanded(item) { |
| return _expanded.contains(item); |
| } |
| |
| void expand(item, |
| {bool autoExpandSingleChildNodes: false, |
| bool autoExpandWholeTree: false}) { |
| if (_expanded.add(item)) _r.dirty(); |
| if (autoExpandWholeTree) { |
| // The tree is potentially very deep, simple recursion can produce a |
| // Stack Overflow |
| Queue pendingNodes = new Queue(); |
| pendingNodes.addAll(_children(item)); |
| while (pendingNodes.isNotEmpty) { |
| final item = pendingNodes.removeFirst(); |
| if (_expanded.add(item)) _r.dirty(); |
| pendingNodes.addAll(_children(item)); |
| } |
| } else if (autoExpandSingleChildNodes) { |
| var children = _children(item); |
| while (children.length == 1) { |
| _expanded.add(children.first); |
| children = _children(children.first); |
| } |
| } |
| } |
| |
| void collapse(item, |
| {bool autoCollapseSingleChildNodes: false, |
| bool autoCollapseWholeTree: false}) { |
| if (_expanded.remove(item)) _r.dirty(); |
| if (autoCollapseWholeTree) { |
| // The tree is potentially very deep, simple recursion can produce a |
| // Stack Overflow |
| Queue pendingNodes = new Queue(); |
| pendingNodes.addAll(_children(item)); |
| while (pendingNodes.isNotEmpty) { |
| final item = pendingNodes.removeFirst(); |
| if (_expanded.remove(item)) _r.dirty(); |
| pendingNodes.addAll(_children(item)); |
| } |
| } else if (autoCollapseSingleChildNodes) { |
| var children = _children(item); |
| while (children.length == 1) { |
| _expanded.remove(children.first); |
| children = _children(children.first); |
| } |
| } |
| } |
| |
| @override |
| attached() { |
| super.attached(); |
| _r.enable(); |
| } |
| |
| @override |
| detached() { |
| super.detached(); |
| _r.disable(notify: true); |
| children = const []; |
| } |
| |
| VirtualCollectionElement _collection; |
| |
| void render() { |
| if (children.length == 0) { |
| children = <Element>[_collection]; |
| } |
| |
| final items = []; |
| final depths = new List.filled(_items.length, 0, growable: true); |
| |
| { |
| final toDo = new Queue(); |
| |
| toDo.addAll(_items); |
| while (toDo.isNotEmpty) { |
| final item = toDo.removeFirst(); |
| |
| items.add(item); |
| if (isExpanded(item)) { |
| final children = _children(item); |
| children |
| .toList(growable: false) |
| .reversed |
| .forEach((c) => toDo.addFirst(c)); |
| final depth = depths[items.length - 1]; |
| depths.insertAll( |
| items.length, new List.filled(children.length, depth + 1)); |
| } |
| } |
| } |
| |
| _depths = depths; |
| _collection.items = items; |
| |
| _r.waitFor([_collection.onRendered.first]); |
| } |
| } |