| /* |
| * Copyright 2014 Google Inc. All rights reserved. |
| * |
| * Use of this source code is governed by a BSD-style |
| * license that can be found in the LICENSE file or at |
| * https://developers.google.com/open-source/licenses/bsd |
| */ |
| part of charted.selection; |
| |
| /** |
| * Implementation of [Selection]. |
| * Selections cannot be created directly - they are only created using |
| * the select or selectAll methods on [SelectionScope] and [Selection]. |
| */ |
| class _SelectionImpl implements Selection { |
| List<SelectionGroup> groups; |
| SelectionScope scope; |
| |
| /** |
| * Creates a new selection. |
| * |
| * When [source] is not specified, the new selection would have exactly |
| * one group with [SelectionScope.root] as it's parent. Otherwise, one group |
| * per for each non-null element is created with element as it's parent. |
| * |
| * When [selector] is specified, each group contains all elements matching |
| * [selector] and under the group's parent element. Otherwise, [fn] is |
| * called once per group with parent element's "data", "index" and the |
| * "element" itself passed as parameters. [fn] must return an iterable of |
| * elements to be used in each group. |
| */ |
| _SelectionImpl.all( |
| {String selector, |
| SelectionCallback<List<Element>> fn, |
| SelectionScope this.scope, |
| Selection source}) { |
| assert(selector != null || fn != null); |
| assert(source != null || scope != null); |
| |
| if (selector != null) { |
| fn = (d, i, c) => c == null |
| ? scope.root.querySelectorAll(selector) |
| : c.querySelectorAll(selector); |
| } |
| |
| var tmpGroups = new List<SelectionGroup>(); |
| if (source != null) { |
| scope = source.scope; |
| for (int gi = 0; gi < source.groups.length; ++gi) { |
| final g = source.groups.elementAt(gi); |
| for (int ei = 0; ei < g.elements.length; ++ei) { |
| final e = g.elements.elementAt(ei); |
| if (e != null) { |
| tmpGroups.add( |
| new _SelectionGroupImpl(fn(scope.datum(e), gi, e), parent: e)); |
| } |
| } |
| } |
| } else { |
| tmpGroups |
| .add(new _SelectionGroupImpl(fn(null, 0, null), parent: scope.root)); |
| } |
| groups = tmpGroups; |
| } |
| |
| /** |
| * Same as [all] but only uses the first element matching [selector] when |
| * [selector] is specified. Otherwise, call [fn] which must return the |
| * element to be selected. |
| */ |
| _SelectionImpl.single( |
| {String selector, |
| SelectionCallback<Element> fn, |
| SelectionScope this.scope, |
| Selection source}) { |
| assert(selector != null || fn != null); |
| assert(source != null || scope != null); |
| |
| if (selector != null) { |
| fn = (d, i, c) => c == null |
| ? scope.root.querySelector(selector) |
| : c.querySelector(selector); |
| } |
| |
| if (source != null) { |
| scope = source.scope; |
| groups = new List<SelectionGroup>.generate(source.groups.length, (gi) { |
| SelectionGroup g = source.groups.elementAt(gi); |
| return new _SelectionGroupImpl( |
| new List.generate(g.elements.length, (ei) { |
| var e = g.elements.elementAt(ei); |
| if (e != null) { |
| var datum = scope.datum(e); |
| var enterElement = fn(datum, ei, e); |
| if (datum != null) { |
| scope.associate(enterElement, datum); |
| } |
| return enterElement; |
| } else { |
| return null; |
| } |
| }), |
| parent: g.parent); |
| }); |
| } else { |
| groups = new List<SelectionGroup>.generate( |
| 1, |
| (_) => new _SelectionGroupImpl( |
| new List.generate(1, (_) => fn(null, 0, null), growable: false)), |
| growable: false); |
| } |
| } |
| |
| /** Creates a selection using the pre-computed list of [SelectionGroup] */ |
| _SelectionImpl.selectionGroups(this.groups, this.scope); |
| |
| /** |
| * Creates a selection using the list of elements. All elements will |
| * be part of the same group, with [SelectionScope.root] as the group's parent |
| */ |
| _SelectionImpl.elements( |
| Iterable<Element> elements, SelectionScope this.scope) { |
| groups = new List<SelectionGroup>() |
| ..add(new _SelectionGroupImpl(elements.toList())); |
| } |
| |
| /** |
| * Utility to evaluate value of parameters (uses value when given |
| * or invokes a callback to get the value) and calls [action] for |
| * each non-null element in this selection |
| */ |
| void _do(SelectionCallback f, Function action) { |
| each((d, i, e) => action(e, f == null ? null : f(scope.datum(e), i, e))); |
| } |
| |
| /** Calls a function on each non-null element in the selection */ |
| void each(SelectionCallback fn) { |
| if (fn == null) return; |
| for (int gi = 0, gLen = groups.length; gi < gLen; ++gi) { |
| final g = groups.elementAt(gi); |
| for (int ei = 0, eLen = g.elements.length; ei < eLen; ++ei) { |
| final e = g.elements.elementAt(ei); |
| if (e != null) fn(scope.datum(e), ei, e); |
| } |
| } |
| } |
| |
| void on(String type, [SelectionCallback listener, bool capture]) { |
| EventListener getEventHandler(int i, Element e) => (Event event) { |
| var previous = scope.event; |
| scope.event = event; |
| try { |
| listener(scope.datum(e), i, e); |
| } finally { |
| scope.event = previous; |
| } |
| }; |
| |
| if (!type.startsWith('.')) { |
| if (listener != null) { |
| // Add a listener to each element. |
| each((d, i, Element e) { |
| var handlers = scope._listeners[e]; |
| if (handlers == null) scope._listeners[e] = handlers = {}; |
| handlers[type] = new Pair(getEventHandler(i, e), capture); |
| e.addEventListener(type, handlers[type].first, capture); |
| }); |
| } else { |
| // Remove the listener from each element. |
| each((d, i, Element e) { |
| var handlers = scope._listeners[e]; |
| if (handlers != null && handlers[type] != null) { |
| e.removeEventListener( |
| type, handlers[type].first, handlers[type].last); |
| } |
| }); |
| } |
| } else { |
| // Remove all listeners on the event type (ignoring the namespace) |
| each((d, i, Element e) { |
| var handlers = scope._listeners[e], t = type.substring(1); |
| handlers.forEach((String s, Pair<EventListener, bool> value) { |
| if (s.split('.')[0] == t) { |
| e.removeEventListener(s, value.first, value.last); |
| } |
| }); |
| }); |
| } |
| } |
| |
| int get length { |
| int retval = 0; |
| each((d, i, e) => retval++); |
| return retval; |
| } |
| |
| bool get isEmpty => length == 0; |
| |
| /** First non-null element in this selection */ |
| Element get first { |
| for (int gi = 0; gi < groups.length; gi++) { |
| SelectionGroup g = groups.elementAt(gi); |
| for (int ei = 0; ei < g.elements.length; ei++) { |
| if (g.elements.elementAt(ei) != null) { |
| return g.elements.elementAt(ei); |
| } |
| } |
| } |
| return null; |
| } |
| |
| void attr(String name, val) { |
| assert(name != null && name.isNotEmpty); |
| attrWithCallback(name, toCallback(val)); |
| } |
| |
| void attrWithCallback(String name, SelectionCallback fn) { |
| assert(fn != null); |
| _do( |
| fn, |
| (e, v) => |
| v == null ? e.attributes.remove(name) : e.attributes[name] = "$v"); |
| } |
| |
| void classed(String name, [bool val = true]) { |
| assert(name != null && name.isNotEmpty); |
| classedWithCallback(name, toCallback(val)); |
| } |
| |
| void classedWithCallback(String name, SelectionCallback<bool> fn) { |
| assert(fn != null); |
| _do(fn, |
| (e, v) => v == false ? e.classes.remove(name) : e.classes.add(name)); |
| } |
| |
| void style(String property, val, {String priority}) { |
| assert(property != null && property.isNotEmpty); |
| styleWithCallback(property, toCallback(val as String), priority: priority); |
| } |
| |
| void styleWithCallback(String property, SelectionCallback<String> fn, |
| {String priority}) { |
| assert(fn != null); |
| _do( |
| fn, |
| (Element e, String v) => v == null || v.isEmpty |
| ? e.style.removeProperty(property) |
| : e.style.setProperty(property, v, priority)); |
| } |
| |
| void text(String val) => textWithCallback(toCallback(val)); |
| |
| void textWithCallback(SelectionCallback<String> fn) { |
| assert(fn != null); |
| _do(fn, (e, v) => e.text = v == null ? '' : v); |
| } |
| |
| void html(String val) => htmlWithCallback(toCallback(val)); |
| |
| void htmlWithCallback(SelectionCallback<String> fn) { |
| assert(fn != null); |
| _do(fn, (e, v) => e.innerHtml = v == null ? '' : v); |
| } |
| |
| void remove() => _do(null, (e, _) => e.remove()); |
| |
| Selection select(String selector) { |
| assert(selector != null && selector.isNotEmpty); |
| return new _SelectionImpl.single(selector: selector, source: this); |
| } |
| |
| Selection selectWithCallback(SelectionCallback<Element> fn) { |
| assert(fn != null); |
| return new _SelectionImpl.single(fn: fn, source: this); |
| } |
| |
| Selection append(String tag) { |
| assert(tag != null && tag.isNotEmpty); |
| return appendWithCallback( |
| (d, ei, e) => Namespace.createChildElement(tag, e)); |
| } |
| |
| Selection appendWithCallback(SelectionCallback<Element> fn) { |
| assert(fn != null); |
| return new _SelectionImpl.single( |
| fn: (datum, ei, e) { |
| Element child = fn(datum, ei, e); |
| return child == null ? null : e.append(child) as Element; |
| }, |
| source: this); |
| } |
| |
| Selection insert(String tag, |
| {String before, SelectionCallback<Element> beforeFn}) { |
| assert(tag != null && tag.isNotEmpty); |
| return insertWithCallback( |
| (d, ei, e) => Namespace.createChildElement(tag, e), |
| before: before, |
| beforeFn: beforeFn); |
| } |
| |
| Selection insertWithCallback(SelectionCallback<Element> fn, |
| {String before, SelectionCallback<Element> beforeFn}) { |
| assert(fn != null); |
| beforeFn = |
| before == null ? beforeFn : (d, ei, e) => e.querySelector(before); |
| return new _SelectionImpl.single( |
| fn: (datum, ei, e) { |
| Element child = fn(datum, ei, e); |
| Element before = beforeFn(datum, ei, e); |
| return child == null |
| ? null |
| : e.insertBefore(child, before) as Element; |
| }, |
| source: this); |
| } |
| |
| Selection selectAll(String selector) { |
| assert(selector != null && selector.isNotEmpty); |
| return new _SelectionImpl.all(selector: selector, source: this); |
| } |
| |
| Selection selectAllWithCallback(SelectionCallback<List<Element>> fn) { |
| assert(fn != null); |
| return new _SelectionImpl.all(fn: fn, source: this); |
| } |
| |
| DataSelection data(Iterable vals, [SelectionKeyFunction keyFn]) { |
| assert(vals != null); |
| return dataWithCallback(toCallback(vals), keyFn); |
| } |
| |
| DataSelection dataWithCallback(SelectionCallback<Iterable> fn, |
| [SelectionKeyFunction keyFn]) { |
| assert(fn != null); |
| |
| var enterGroups = <SelectionGroup>[], |
| updateGroups = <SelectionGroup>[], |
| exitGroups = <SelectionGroup>[]; |
| |
| // Create a dummy node to be used with enter() selection. |
| Element dummy(val) { |
| var element = new Element.div(); |
| scope.associate(element, val); |
| return element; |
| } |
| |
| ; |
| |
| // Joins data to all elements in the group. |
| void join(SelectionGroup g, Iterable vals) { |
| final int valuesLength = vals.length; |
| final int elementsLength = g.elements.length; |
| |
| // Nodes exiting, entering and updating in this group. |
| // We maintain the nodes at the same index as they currently |
| // are (for exiting) or where they should be (for entering and updating) |
| var update = new List<Element>(valuesLength); |
| var enter = new List<Element>(valuesLength); |
| var exit = new List<Element>(elementsLength); |
| |
| // Use key function to determine DOMElement to data associations. |
| if (keyFn != null) { |
| var keysOnDOM = [], elementsByKey = {}, valuesByKey = {}; |
| |
| // Create a key to DOM element map. |
| // Used later to see if an element already exists for a key. |
| for (int ei = 0, len = elementsLength; ei < len; ++ei) { |
| final e = g.elements.elementAt(ei); |
| var keyValue = keyFn(scope.datum(e)); |
| if (elementsByKey.containsKey(keyValue)) { |
| exit[ei] = e; |
| } else { |
| elementsByKey[keyValue] = e; |
| } |
| keysOnDOM.add(keyValue); |
| } |
| |
| // Iterate through the values and find values that don't have |
| // corresponding elements in the DOM, collect the entering elements. |
| for (int vi = 0, len = valuesLength; vi < len; ++vi) { |
| final v = vals.elementAt(vi); |
| var keyValue = keyFn(v); |
| Element e = elementsByKey[keyValue]; |
| if (e != null) { |
| update[vi] = e; |
| scope.associate(e, v); |
| } else if (!valuesByKey.containsKey(keyValue)) { |
| enter[vi] = dummy(v); |
| } |
| valuesByKey[keyValue] = v; |
| elementsByKey.remove(keyValue); |
| } |
| |
| // Iterate through the previously saved keys to |
| // find a list of elements that don't have data anymore. |
| // We don't use elementsByKey.keys() because that does not |
| // guarantee the order of returned keys. |
| for (int i = 0, len = elementsLength; i < len; ++i) { |
| if (elementsByKey.containsKey(keysOnDOM[i])) { |
| exit[i] = g.elements.elementAt(i); |
| } |
| } |
| } else { |
| // When we don't have the key function, just use list index as the key |
| int updateElementsCount = math.min(elementsLength, valuesLength); |
| int i = 0; |
| |
| // Collect a list of elements getting updated in this group |
| for (int len = updateElementsCount; i < len; ++i) { |
| var e = g.elements.elementAt(i); |
| if (e != null) { |
| scope.associate(e, vals.elementAt(i)); |
| update[i] = e; |
| } else { |
| enter[i] = dummy(vals.elementAt(i)); |
| } |
| } |
| |
| // List of elements newly getting added |
| for (int len = valuesLength; i < len; ++i) { |
| enter[i] = dummy(vals.elementAt(i)); |
| } |
| |
| // List of elements exiting this group |
| for (int len = elementsLength; i < len; ++i) { |
| exit[i] = g.elements.elementAt(i); |
| } |
| } |
| |
| // Create the element groups and set parents from the current group. |
| enterGroups.add(new _SelectionGroupImpl(enter, parent: g.parent)); |
| updateGroups.add(new _SelectionGroupImpl(update, parent: g.parent)); |
| exitGroups.add(new _SelectionGroupImpl(exit, parent: g.parent)); |
| } |
| |
| ; |
| |
| for (int gi = 0; gi < groups.length; ++gi) { |
| final g = groups.elementAt(gi); |
| join(g, fn(scope.datum(g.parent), gi, g.parent)); |
| } |
| |
| return new _DataSelectionImpl(updateGroups, enterGroups, exitGroups, scope); |
| } |
| |
| void datum(Iterable vals) { |
| throw new UnimplementedError(); |
| } |
| |
| void datumWithCallback(SelectionCallback<Iterable> fn) { |
| throw new UnimplementedError(); |
| } |
| |
| Transition transition() => new Transition(this); |
| } |
| |
| /* Implementation of [DataSelection] */ |
| class _DataSelectionImpl extends _SelectionImpl implements DataSelection { |
| EnterSelection enter; |
| ExitSelection exit; |
| |
| _DataSelectionImpl( |
| List<SelectionGroup> updated, |
| List<SelectionGroup> entering, |
| List<SelectionGroup> exiting, |
| SelectionScope scope) |
| : super.selectionGroups(updated, scope) { |
| enter = new _EnterSelectionImpl(entering, this); |
| exit = new _ExitSelectionImpl(exiting, this); |
| } |
| } |
| |
| /* Implementation of [EnterSelection] */ |
| class _EnterSelectionImpl implements EnterSelection { |
| final DataSelection update; |
| |
| SelectionScope scope; |
| Iterable<SelectionGroup> groups; |
| |
| _EnterSelectionImpl(this.groups, this.update) { |
| scope = update.scope; |
| } |
| |
| bool get isEmpty => false; |
| |
| Selection insert(String tag, |
| {String before, SelectionCallback<Element> beforeFn}) { |
| assert(tag != null && tag.isNotEmpty); |
| return insertWithCallback( |
| (d, ei, e) => Namespace.createChildElement(tag, e), |
| before: before, |
| beforeFn: beforeFn); |
| } |
| |
| Selection insertWithCallback(SelectionCallback<Element> fn, |
| {String before, SelectionCallback<Element> beforeFn}) { |
| assert(fn != null); |
| return selectWithCallback((d, ei, e) { |
| Element child = fn(d, ei, e); |
| e.insertBefore(child, e.querySelector(before)); |
| return child; |
| }); |
| } |
| |
| Selection append(String tag) { |
| assert(tag != null && tag.isNotEmpty); |
| return appendWithCallback( |
| (d, ei, e) => Namespace.createChildElement(tag, e)); |
| } |
| |
| Selection appendWithCallback(SelectionCallback<Element> fn) { |
| assert(fn != null); |
| return selectWithCallback((datum, ei, e) { |
| Element child = fn(datum, ei, e); |
| e.append(child); |
| return child; |
| }); |
| } |
| |
| Selection select(String selector) { |
| assert(selector == null && selector.isNotEmpty); |
| return selectWithCallback((d, ei, e) => e.querySelector(selector)); |
| } |
| |
| Selection selectWithCallback(SelectionCallback<Element> fn) { |
| var subgroups = <SelectionGroup>[]; |
| for (int gi = 0, len = groups.length; gi < len; ++gi) { |
| final g = groups.elementAt(gi); |
| final u = update.groups.elementAt(gi); |
| final subgroup = <Element>[]; |
| for (int ei = 0, eLen = g.elements.length; ei < eLen; ++ei) { |
| final e = g.elements.elementAt(ei); |
| if (e != null) { |
| var datum = scope.datum(e), selected = fn(datum, ei, g.parent); |
| scope.associate(selected, datum); |
| u.elements[ei] = selected; |
| subgroup.add(selected); |
| } else { |
| subgroup.add(null); |
| } |
| } |
| subgroups.add(new _SelectionGroupImpl(subgroup, parent: g.parent)); |
| } |
| return new _SelectionImpl.selectionGroups(subgroups, scope); |
| } |
| } |
| |
| /* Implementation of [ExitSelection] */ |
| class _ExitSelectionImpl extends _SelectionImpl implements ExitSelection { |
| final DataSelection update; |
| _ExitSelectionImpl(List<SelectionGroup> groups, DataSelection update) |
| : update = update, |
| super.selectionGroups(groups, update.scope); |
| } |
| |
| class _SelectionGroupImpl implements SelectionGroup { |
| List<Element> elements; |
| Element parent; |
| _SelectionGroupImpl(this.elements, {this.parent}); |
| } |