blob: 7cb60e590d8de65d6ff170e66c8cf89075291061 [file] [log] [blame]
// Copyright 2018 The Chromium Authors. 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 '../ui/icons.dart';
import 'elements.dart';
import 'html_icon_renderer.dart';
import 'trees.dart';
import 'trees_html.dart';
class ProgressElement extends CoreElement {
ProgressElement() : super('div') {
clazz('progress-element');
add(completeElement = div(c: 'complete'));
}
int _value = 0;
int _max = 100;
CoreElement completeElement;
int get value => _value;
set value(int val) {
_value = val;
_update();
}
int get max => _max;
set max(int val) {
_max = val;
_update();
}
void _update() {
// TODO(devoncarew): don't hard-code the width
completeElement.element.style.width = '${(200 * _value / _max).round()}px';
}
}
class Spinner extends CoreElement {
Spinner({List<String> classes = const []}) : super('div') {
clazz('spinner');
classes.forEach(clazz);
}
static Spinner centered({List<String> classes = const []}) =>
Spinner(classes: ['centered']..addAll(classes));
void remove() => element.remove();
}
typedef ListRenderer<T> = CoreElement Function(T item);
class SelectableList<T> extends CoreElement {
SelectableList() : super('div');
List<T> items = <T>[];
ListRenderer<T> renderer;
CoreElement _selectedElement;
bool _hadClicked = false;
bool get hadClicked => _hadClicked;
final StreamController<T> _selectionController = StreamController.broadcast();
final StreamController<T> _doubleClickController =
StreamController.broadcast();
final StreamController<void> _itemsChangedController =
StreamController.broadcast();
bool canDeselect = false;
Stream<T> get onSelectionChanged => _selectionController.stream;
Stream<T> get onDoubleClick => _doubleClickController.stream;
Stream<void> get onItemsChanged => _itemsChangedController.stream;
void setRenderer(ListRenderer<T> renderer) {
this.renderer = renderer;
}
T selectedItem() {
if (_selectedElement != null) {
final childrenElements = element.children;
for (var i = 0; i < childrenElements.length; i++) {
final elem = childrenElements[i];
if (elem.classes.contains('selected')) return items[i];
}
}
return null;
}
void setItems(List<T> items,
{T selection, bool scrollSelectionIntoView = false}) {
this.items = items;
final bool hadSelection = _selectedElement != null;
_selectedElement = null;
if (selection == null) {
// Reset the clicked state.
_hadClicked = false;
}
final ListRenderer<T> renderer = this.renderer ?? _defaultRenderer;
clear();
add(items.map((T item) {
final CoreElement element = _hookup(renderer, item, selection);
if (scrollSelectionIntoView && selection == item) {
_selectedElement = element;
}
return element;
}).toList());
if (hadSelection) {
if (_selectedElement == null) {
_selectionController.add(null);
} else if (_selectedElement != null) {
_select(_selectedElement, selection);
}
}
_itemsChangedController.add(null);
}
CoreElement setReplace(int index, T item) {
_selectedElement?.toggleClass('selected', false);
_selectedElement = null;
final ListRenderer<T> renderer = this.renderer ?? _defaultRenderer;
final CoreElement element = _hookup(renderer, item, item);
replace(index, element);
_select(element, item);
return element;
}
CoreElement _hookup(ListRenderer<T> renderer, T item, T selection) {
final CoreElement element = renderer(item);
element.click(() {
_select(element, item,
clear: canDeselect && element.hasClass('selected'), clicked: true);
});
element.dblclick(() {
_doubleClickController.add(item);
});
if (selection == item) {
_select(element, item);
}
return element;
}
void clearItems() {
setItems(<T>[]);
}
void _select(
CoreElement element,
T item, {
bool clear = false,
bool clicked = false,
}) {
_selectedElement?.toggleClass('selected', false);
if (clear) {
element = null;
item = null;
}
_selectedElement = element;
element?.toggleClass('selected', true);
element?.scrollIntoView();
_selectionController.add(item);
_hadClicked = clicked;
}
}
abstract class ChildProvider<T> {
bool hasChildren(T item);
Future<List<T>> getChildren(T item);
}
class SelectableTreeNodeItem<T> {
SelectableTreeNodeItem(this.element, this.item);
final CoreElement element;
final T item;
}
class SelectableTree<T> extends CoreElement
with
Tree<SelectableTreeNodeItem<T>>,
TreeNavigator<SelectableTreeNodeItem<T>>,
HtmlTreeNavigator<SelectableTreeNodeItem<T>> {
SelectableTree() : super('ul') {
// Ensure the tree can be tabbed into.
element.tabIndex = 0;
element.onKeyDown.listen(handleKeyPress);
}
List<T> items = <T>[];
@override
List<TreeNode<SelectableTreeNodeItem<T>>> treeNodes = [];
ListRenderer<T> renderer;
ChildProvider<T> childProvider;
TreeNode<SelectableTreeNodeItem<T>> _selectedItem;
@override
TreeNode<SelectableTreeNodeItem<T>> get selectedItem => _selectedItem;
final StreamController<T> _selectionController = StreamController.broadcast();
Stream<T> get onSelectionChanged => _selectionController.stream;
void setRenderer(ListRenderer<T> renderer) {
this.renderer = renderer;
}
void setChildProvider(ChildProvider<T> childProvider) {
this.childProvider = childProvider;
}
void setItems(List<T> items) {
this.items = items;
final bool hadSelection = _selectedItem != null;
_selectedItem = null;
clear();
treeNodes = _buildTree(items, this, null);
if (hadSelection && _selectedItem == null) {
_selectionController.add(null);
}
}
TreeNode<SelectableTreeNodeItem<T>> _addItemToTree(
CoreElement container, T item) {
final ListRenderer<T> renderer = this.renderer ?? _defaultRenderer;
final obj = TreeNode(new SelectableTreeNodeItem(renderer(item), item));
obj.data.element.click(() {
select(obj, clear: obj.data.element.hasClass('selected'));
});
final CoreElement element = div();
element.add(obj.data.element);
if (childProvider.hasChildren(item)) {
final TreeToggle toggle = new TreeToggle();
obj.data.element.element.children.insert(0, toggle.element);
bool hasPopulated = false;
final CoreElement childContainer = ul(c: 'tree-list');
element.add(childContainer);
childContainer.hidden(true);
// Attach helpers that allow TreeItem to expand/collapse for use in
// keyboard navigation.
obj.expand = () => toggle.toggle(onlyExpand: true);
obj.collapse = () => toggle.toggle(onlyCollapse: true);
toggle.onOpen.listen((bool open) {
obj.isExpanded = open;
childContainer.hidden(!open);
if (!hasPopulated) {
hasPopulated = true;
childProvider.getChildren(item).then((List<T> results) {
_buildTree(results, childContainer, obj);
}).catchError((e) {
// ignore
});
}
});
} else {
obj.data.element.element.children
.insert(0, new TreeToggle(empty: true).element);
}
container.add(element);
return obj;
}
/// Builds a tree for [results] into [container].
List<TreeNode<SelectableTreeNodeItem<T>>> _buildTree(
List<T> results,
CoreElement container,
TreeNode<SelectableTreeNodeItem<T>> parent,
) {
final List<TreeNode<SelectableTreeNodeItem<T>>> children =
results.map((result) => _addItemToTree(container, result)).toList();
connectNodes(
parent,
children,
(node) => childProvider.hasChildren(node.item),
);
return children;
}
void clearItems() {
setItems(<T>[]);
}
@override
void select(TreeNode<SelectableTreeNodeItem<T>> node, {bool clear = false}) {
_selectedItem?.data?.element?.toggleClass('selected', false);
if (clear) {
node = null;
}
_selectedItem = node;
_selectedItem?.data?.element?.toggleClass('selected', true);
_selectedItem?.data?.element?.scrollIntoView();
_selectionController.add(node?.data?.item);
}
}
// TODO(kenzie): wrap this element in a larger div to increase tap target.
class TreeToggle extends CoreElement {
TreeToggle({bool empty = false, bool forceOpen = false})
: super('div', classes: 'tree-toggle octicon') {
if (!empty) {
click(toggle);
if (forceOpen) {
_isOpen = true;
clazz('octicon-triangle-down');
} else {
clazz('octicon-triangle-right');
}
}
}
void toggle({bool onlyExpand = false, bool onlyCollapse = false}) {
if ((onlyExpand && _isOpen) || (onlyCollapse && !_isOpen)) {
return;
}
_isOpen = !_isOpen;
_openController.add(_isOpen);
toggleClass('octicon-triangle-right', !_isOpen);
toggleClass('octicon-triangle-down', _isOpen);
}
bool _isOpen = false;
final StreamController<bool> _openController =
new StreamController.broadcast();
Stream<bool> get onOpen => _openController.stream;
}
CoreElement _defaultRenderer<T>(T item) {
return li(text: item.toString(), c: 'list-item');
}
class ActionButton implements CoreElementView {
ActionButton(this.id, this.icon, this.tooltip) {
_element = div(c: 'masthead-item action-button')
..tooltip = tooltip
..add(createIconElement(icon));
}
final String id;
final Icon icon;
final String tooltip;
CoreElement _element;
StreamSubscription click(void handle()) => _element.click(handle);
set disabled(bool value) => _element.disabled = value;
@override
CoreElement get element => _element;
}