OLD | NEW |
| (Empty) |
1 /* | |
2 * Copyright 2014 Google Inc. All rights reserved. | |
3 * | |
4 * Use of this source code is governed by a BSD-style | |
5 * license that can be found in the LICENSE file or at | |
6 * https://developers.google.com/open-source/licenses/bsd | |
7 */ | |
8 part of charted.selection; | |
9 | |
10 /** | |
11 * Implementation of [Selection]. | |
12 * Selections cannot be created directly - they are only created using | |
13 * the select or selectAll methods on [SelectionScope] and [Selection]. | |
14 */ | |
15 class _SelectionImpl implements Selection { | |
16 | |
17 Iterable<SelectionGroup> groups; | |
18 SelectionScope scope; | |
19 | |
20 /** | |
21 * Creates a new selection. | |
22 * | |
23 * When [source] is not specified, the new selection would have exactly | |
24 * one group with [SelectionScope.root] as it's parent. Otherwise, one group | |
25 * per for each non-null element is created with element as it's parent. | |
26 * | |
27 * When [selector] is specified, each group contains all elements matching | |
28 * [selector] and under the group's parent element. Otherwise, [fn] is | |
29 * called once per group with parent element's "data", "index" and the | |
30 * "element" itself passed as parameters. [fn] must return an iterable of | |
31 * elements to be used in each group. | |
32 */ | |
33 _SelectionImpl.all({String selector, SelectionCallback<Iterable<Element>> fn, | |
34 SelectionScope this.scope, Selection source}) { | |
35 assert(selector != null || fn != null); | |
36 assert(source != null || scope != null); | |
37 | |
38 if (selector != null) { | |
39 fn = (d, i, c) => c == null ? | |
40 scope.root.querySelectorAll(selector) : | |
41 c.querySelectorAll(selector); | |
42 } | |
43 | |
44 var tmpGroups = new List<SelectionGroup>(); | |
45 if (source != null) { | |
46 scope = source.scope; | |
47 for (int gi = 0; gi < source.groups.length; ++gi) { | |
48 final g = source.groups.elementAt(gi); | |
49 for (int ei = 0; ei < g.elements.length; ++ei) { | |
50 final e = g.elements.elementAt(ei); | |
51 if (e != null) { | |
52 tmpGroups.add( | |
53 new _SelectionGroupImpl( | |
54 fn(scope.datum(e), gi, e), parent: e)); | |
55 } | |
56 } | |
57 } | |
58 } else { | |
59 tmpGroups.add( | |
60 new _SelectionGroupImpl(fn(null, 0, null), parent: scope.root)); | |
61 } | |
62 groups = tmpGroups; | |
63 } | |
64 | |
65 /** | |
66 * Same as [all] but only uses the first element matching [selector] when | |
67 * [selector] is specified. Otherwise, call [fn] which must return the | |
68 * element to be selected. | |
69 */ | |
70 _SelectionImpl.single({String selector, SelectionCallback<Element> fn, | |
71 SelectionScope this.scope, Selection source}) { | |
72 assert(selector != null || fn != null); | |
73 assert(source != null || scope != null); | |
74 | |
75 if (selector != null) { | |
76 fn = (d, i, c) => c == null ? | |
77 scope.root.querySelector(selector) : | |
78 c.querySelector(selector); | |
79 } | |
80 | |
81 if (source != null) { | |
82 scope = source.scope; | |
83 groups = new List<SelectionGroup>.generate(source.groups.length, (gi) { | |
84 SelectionGroup g = source.groups.elementAt(gi); | |
85 return new _SelectionGroupImpl( | |
86 new List.generate(g.elements.length, (ei) { | |
87 var e = g.elements.elementAt(ei); | |
88 if (e != null) { | |
89 var datum = scope.datum(e); | |
90 var enterElement = fn(datum, ei, e); | |
91 if (datum != null) { | |
92 scope.associate(enterElement, datum); | |
93 } | |
94 return enterElement; | |
95 } else { | |
96 return null; | |
97 } | |
98 }), parent: g.parent); | |
99 }); | |
100 } else { | |
101 groups = new List<SelectionGroup>.generate(1, | |
102 (_) => new _SelectionGroupImpl(new List.generate(1, | |
103 (_) => fn(null, 0, null), growable: false)), growable: false); | |
104 } | |
105 } | |
106 | |
107 /** Creates a selection using the pre-computed list of [SelectionGroup] */ | |
108 _SelectionImpl.selectionGroups( | |
109 Iterable<SelectionGroup> this.groups, SelectionScope this.scope); | |
110 | |
111 /** | |
112 * Creates a selection using the list of elements. All elements will | |
113 * be part of the same group, with [SelectionScope.root] as the group's parent | |
114 */ | |
115 _SelectionImpl.elements(Iterable elements, SelectionScope this.scope) { | |
116 groups = new List<SelectionGroup>() | |
117 ..add(new _SelectionGroupImpl(elements)); | |
118 } | |
119 | |
120 /** | |
121 * Utility to evaluate value of parameters (uses value when given | |
122 * or invokes a callback to get the value) and calls [action] for | |
123 * each non-null element in this selection | |
124 */ | |
125 void _do(SelectionCallback f, Function action) { | |
126 each((d, i, e) => action(e, f == null ? null : f(scope.datum(e), i, e))); | |
127 } | |
128 | |
129 /** Calls a function on each non-null element in the selection */ | |
130 void each(SelectionCallback fn) { | |
131 if (fn == null) return; | |
132 for (int gi = 0, gLen = groups.length; gi < gLen; ++gi) { | |
133 final g = groups.elementAt(gi); | |
134 for (int ei = 0, eLen = g.elements.length; ei < eLen; ++ei) { | |
135 final e = g.elements.elementAt(ei); | |
136 if (e != null) fn(scope.datum(e), ei, e); | |
137 } | |
138 } | |
139 } | |
140 | |
141 void on(String type, [SelectionCallback listener, bool capture]) { | |
142 Function getEventHandler(i, e) => (Event event) { | |
143 var previous = scope.event; | |
144 scope.event = event; | |
145 try { | |
146 listener(scope.datum(e), i, e); | |
147 } finally { | |
148 scope.event = previous; | |
149 } | |
150 }; | |
151 | |
152 if (!type.startsWith('.')) { | |
153 if (listener != null) { | |
154 // Add a listener to each element. | |
155 each((d, i, Element e){ | |
156 var handlers = scope._listeners[e]; | |
157 if (handlers == null) scope._listeners[e] = handlers = {}; | |
158 handlers[type] = new Pair(getEventHandler(i, e), capture); | |
159 e.addEventListener(type, handlers[type].first, capture); | |
160 }); | |
161 } else { | |
162 // Remove the listener from each element. | |
163 each((d, i, Element e) { | |
164 var handlers = scope._listeners[e]; | |
165 if (handlers != null && handlers[type] != null) { | |
166 e.removeEventListener( | |
167 type, handlers[type].first, handlers[type].last); | |
168 } | |
169 }); | |
170 } | |
171 } else { | |
172 // Remove all listeners on the event type (ignoring the namespace) | |
173 each((d, i, Element e) { | |
174 var handlers = scope._listeners[e], | |
175 t = type.substring(1); | |
176 handlers.forEach((String s, Pair<Function, bool> value) { | |
177 if (s.split('.')[0] == t) { | |
178 e.removeEventListener(s, value.first, value.last); | |
179 } | |
180 }); | |
181 }); | |
182 } | |
183 } | |
184 | |
185 int get length { | |
186 int retval = 0; | |
187 each((d, i, e) => retval++); | |
188 return retval; | |
189 } | |
190 | |
191 bool get isEmpty => length == 0; | |
192 | |
193 /** First non-null element in this selection */ | |
194 Element get first { | |
195 for (int gi = 0; gi < groups.length; gi++) { | |
196 SelectionGroup g = groups.elementAt(gi); | |
197 for (int ei = 0; ei < g.elements.length; ei++) { | |
198 if (g.elements.elementAt(ei) != null) { | |
199 return g.elements.elementAt(ei); | |
200 } | |
201 } | |
202 } | |
203 return null; | |
204 } | |
205 | |
206 void attr(String name, val) { | |
207 assert(name != null && name.isNotEmpty); | |
208 attrWithCallback(name, toCallback(val)); | |
209 } | |
210 | |
211 void attrWithCallback(String name, SelectionCallback fn) { | |
212 assert(fn != null); | |
213 _do(fn, (e, v) => v == null ? | |
214 e.attributes.remove(name) : e.attributes[name] = "$v"); | |
215 } | |
216 | |
217 void classed(String name, [bool val = true]) { | |
218 assert(name != null && name.isNotEmpty); | |
219 classedWithCallback(name, toCallback(val)); | |
220 } | |
221 | |
222 void classedWithCallback(String name, SelectionCallback<bool> fn) { | |
223 assert(fn != null); | |
224 _do(fn, (e, v) => | |
225 v == false ? e.classes.remove(name) : e.classes.add(name)); | |
226 } | |
227 | |
228 void style(String property, val, {String priority}) { | |
229 assert(property != null && property.isNotEmpty); | |
230 styleWithCallback(property, | |
231 toCallback(val as String), priority: priority); | |
232 } | |
233 | |
234 void styleWithCallback(String property, | |
235 SelectionCallback<String> fn, {String priority}) { | |
236 assert(fn != null); | |
237 _do(fn, (Element e, String v) => | |
238 v == null || v.isEmpty ? | |
239 e.style.removeProperty(property) : | |
240 e.style.setProperty(property, v, priority)); | |
241 } | |
242 | |
243 void text(String val) => textWithCallback(toCallback(val)); | |
244 | |
245 void textWithCallback(SelectionCallback<String> fn) { | |
246 assert(fn != null); | |
247 _do(fn, (e, v) => e.text = v == null ? '' : v); | |
248 } | |
249 | |
250 void html(String val) => htmlWithCallback(toCallback(val)); | |
251 | |
252 void htmlWithCallback(SelectionCallback<String> fn) { | |
253 assert(fn != null); | |
254 _do(fn, (e, v) => e.innerHtml = v == null ? '' : v); | |
255 } | |
256 | |
257 void remove() => _do(null, (e, _) => e.remove()); | |
258 | |
259 Selection select(String selector) { | |
260 assert(selector != null && selector.isNotEmpty); | |
261 return new _SelectionImpl.single(selector: selector, source: this); | |
262 } | |
263 | |
264 Selection selectWithCallback(SelectionCallback<Element> fn) { | |
265 assert(fn != null); | |
266 return new _SelectionImpl.single(fn: fn, source:this); | |
267 } | |
268 | |
269 Selection append(String tag) { | |
270 assert(tag != null && tag.isNotEmpty); | |
271 return appendWithCallback( | |
272 (d, ei, e) => Namespace.createChildElement(tag, e)); | |
273 } | |
274 | |
275 Selection appendWithCallback(SelectionCallback<Element> fn) { | |
276 assert(fn != null); | |
277 return new _SelectionImpl.single(fn: (datum, ei, e) { | |
278 Element child = fn(datum, ei, e); | |
279 return child == null ? null : e.append(child); | |
280 }, source: this); | |
281 } | |
282 | |
283 Selection insert(String tag, | |
284 {String before, SelectionCallback<Element> beforeFn}) { | |
285 assert(tag != null && tag.isNotEmpty); | |
286 return insertWithCallback( | |
287 (d, ei, e) => Namespace.createChildElement(tag, e), | |
288 before: before, beforeFn: beforeFn); | |
289 } | |
290 | |
291 Selection insertWithCallback(SelectionCallback<Element> fn, | |
292 {String before, SelectionCallback<Element> beforeFn}) { | |
293 assert(fn != null); | |
294 beforeFn = | |
295 before == null ? beforeFn : (d, ei, e) => e.querySelector(before); | |
296 return new _SelectionImpl.single( | |
297 fn: (datum, ei, e) { | |
298 Element child = fn(datum, ei, e); | |
299 Element before = beforeFn(datum, ei, e); | |
300 return child == null ? null : e.insertBefore(child, before); | |
301 }, | |
302 source: this); | |
303 } | |
304 | |
305 Selection selectAll(String selector) { | |
306 assert(selector != null && selector.isNotEmpty); | |
307 return new _SelectionImpl.all(selector: selector, source: this); | |
308 } | |
309 | |
310 Selection selectAllWithCallback(SelectionCallback<Iterable<Element>> fn) { | |
311 assert(fn != null); | |
312 return new _SelectionImpl.all(fn: fn, source:this); | |
313 } | |
314 | |
315 DataSelection data(Iterable vals, [SelectionKeyFunction keyFn]) { | |
316 assert(vals != null); | |
317 return dataWithCallback(toCallback(vals), keyFn); | |
318 } | |
319 | |
320 DataSelection dataWithCallback( | |
321 SelectionCallback<Iterable> fn, [SelectionKeyFunction keyFn]) { | |
322 assert(fn != null); | |
323 | |
324 var enterGroups = [], | |
325 updateGroups = [], | |
326 exitGroups = []; | |
327 | |
328 // Create a dummy node to be used with enter() selection. | |
329 Object dummy(val) { | |
330 var element = new Object(); | |
331 scope.associate(element, val); | |
332 return element; | |
333 }; | |
334 | |
335 // Joins data to all elements in the group. | |
336 void join(SelectionGroup g, Iterable vals) { | |
337 final int valuesLength = vals.length; | |
338 final int elementsLength = g.elements.length; | |
339 | |
340 // Nodes exiting, entering and updating in this group. | |
341 // We maintain the nodes at the same index as they currently | |
342 // are (for exiting) or where they should be (for entering and updating) | |
343 var update = new List(valuesLength), | |
344 enter = new List(valuesLength), | |
345 exit = new List(elementsLength); | |
346 | |
347 // Use key function to determine DOMElement to data associations. | |
348 if (keyFn != null) { | |
349 var keysOnDOM = [], | |
350 elementsByKey = {}, | |
351 valuesByKey = {}; | |
352 | |
353 // Create a key to DOM element map. | |
354 // Used later to see if an element already exists for a key. | |
355 for (int ei = 0, len = elementsLength; ei < len; ++ei) { | |
356 final e = g.elements.elementAt(ei); | |
357 var keyValue = keyFn(scope.datum(e)); | |
358 if (elementsByKey.containsKey(keyValue)) { | |
359 exit[ei] = e; | |
360 } else { | |
361 elementsByKey[keyValue] = e; | |
362 } | |
363 keysOnDOM.add(keyValue); | |
364 } | |
365 | |
366 // Iterate through the values and find values that don't have | |
367 // corresponding elements in the DOM, collect the entering elements. | |
368 for (int vi = 0, len = valuesLength; vi < len; ++vi) { | |
369 final v = vals.elementAt(vi); | |
370 var keyValue = keyFn(v); | |
371 Element e = elementsByKey[keyValue]; | |
372 if (e != null) { | |
373 update[vi] = e; | |
374 scope.associate(e, v); | |
375 } else if (!valuesByKey.containsKey(keyValue)) { | |
376 enter[vi] = dummy(v); | |
377 } | |
378 valuesByKey[keyValue] = v; | |
379 elementsByKey.remove(keyValue); | |
380 } | |
381 | |
382 // Iterate through the previously saved keys to | |
383 // find a list of elements that don't have data anymore. | |
384 // We don't use elementsByKey.keys() because that does not | |
385 // guarantee the order of returned keys. | |
386 for (int i = 0, len = elementsLength; i < len; ++i) { | |
387 if (elementsByKey.containsKey(keysOnDOM[i])) { | |
388 exit[i] = g.elements.elementAt(i); | |
389 } | |
390 } | |
391 } else { | |
392 // When we don't have the key function, just use list index as the key | |
393 int updateElementsCount = math.min(elementsLength, valuesLength); | |
394 int i = 0; | |
395 | |
396 // Collect a list of elements getting updated in this group | |
397 for (int len = updateElementsCount; i < len; ++i) { | |
398 var e = g.elements.elementAt(i); | |
399 if (e != null) { | |
400 scope.associate(e, vals.elementAt(i)); | |
401 update[i] = e; | |
402 } else { | |
403 enter[i] = dummy(vals.elementAt(i)); | |
404 } | |
405 } | |
406 | |
407 // List of elements newly getting added | |
408 for (int len = valuesLength; i < len; ++i) { | |
409 enter[i] = dummy(vals.elementAt(i)); | |
410 } | |
411 | |
412 // List of elements exiting this group | |
413 for (int len = elementsLength; i < len; ++i) { | |
414 exit[i] = g.elements.elementAt(i); | |
415 } | |
416 } | |
417 | |
418 // Create the element groups and set parents from the current group. | |
419 enterGroups.add(new _SelectionGroupImpl(enter, parent: g.parent)); | |
420 updateGroups.add(new _SelectionGroupImpl(update, parent: g.parent)); | |
421 exitGroups.add(new _SelectionGroupImpl(exit, parent: g.parent)); | |
422 }; | |
423 | |
424 for (int gi = 0; gi < groups.length; ++gi) { | |
425 final g = groups.elementAt(gi); | |
426 join(g, fn(scope.datum(g.parent), gi, g.parent)); | |
427 } | |
428 | |
429 return new _DataSelectionImpl( | |
430 updateGroups, enterGroups, exitGroups, scope); | |
431 } | |
432 | |
433 void datum(Iterable vals) { | |
434 throw new UnimplementedError(); | |
435 } | |
436 | |
437 void datumWithCallback(SelectionCallback<Iterable> fn) { | |
438 throw new UnimplementedError(); | |
439 } | |
440 | |
441 Transition transition() => new Transition(this); | |
442 } | |
443 | |
444 /* Implementation of [DataSelection] */ | |
445 class _DataSelectionImpl extends _SelectionImpl implements DataSelection { | |
446 EnterSelection enter; | |
447 ExitSelection exit; | |
448 | |
449 _DataSelectionImpl(Iterable updated, Iterable entering, Iterable exiting, | |
450 SelectionScope scope) : super.selectionGroups(updated, scope) { | |
451 enter = new _EnterSelectionImpl(entering, this); | |
452 exit = new _ExitSelectionImpl(exiting, this); | |
453 } | |
454 } | |
455 | |
456 /* Implementation of [EnterSelection] */ | |
457 class _EnterSelectionImpl implements EnterSelection { | |
458 final DataSelection update; | |
459 | |
460 SelectionScope scope; | |
461 Iterable<SelectionGroup> groups; | |
462 | |
463 _EnterSelectionImpl(Iterable this.groups, DataSelection this.update) { | |
464 scope = update.scope; | |
465 } | |
466 | |
467 bool get isEmpty => false; | |
468 | |
469 Selection insert(String tag, | |
470 {String before, SelectionCallback<Element> beforeFn}) { | |
471 assert(tag != null && tag.isNotEmpty); | |
472 return insertWithCallback( | |
473 (d, ei, e) => Namespace.createChildElement(tag, e), | |
474 before: before, beforeFn: beforeFn); | |
475 } | |
476 | |
477 Selection insertWithCallback(SelectionCallback<Element> fn, | |
478 {String before, SelectionCallback<Element> beforeFn}) { | |
479 assert(fn != null); | |
480 return selectWithCallback((d, ei, e) { | |
481 Element child = fn(d, ei, e); | |
482 e.insertBefore(child, e.querySelector(before)); | |
483 return child; | |
484 }); | |
485 } | |
486 | |
487 Selection append(String tag) { | |
488 assert(tag != null && tag.isNotEmpty); | |
489 return appendWithCallback( | |
490 (d, ei, e) => Namespace.createChildElement(tag, e)); | |
491 } | |
492 | |
493 Selection appendWithCallback(SelectionCallback<Element> fn) { | |
494 assert(fn != null); | |
495 return selectWithCallback((datum, ei, e) { | |
496 Element child = fn(datum, ei, e); | |
497 e.append(child); | |
498 return child; | |
499 }); | |
500 } | |
501 | |
502 Selection select(String selector) { | |
503 assert(selector == null && selector.isNotEmpty); | |
504 return selectWithCallback((d, ei, e) => e.querySelector(selector)); | |
505 } | |
506 | |
507 Selection selectWithCallback(SelectionCallback<Element> fn) { | |
508 var subgroups = []; | |
509 for (int gi = 0, len = groups.length; gi < len; ++gi) { | |
510 final g = groups.elementAt(gi); | |
511 final u = update.groups.elementAt(gi); | |
512 final subgroup = []; | |
513 for (int ei = 0, eLen = g.elements.length; ei < eLen; ++ei) { | |
514 final e = g.elements.elementAt(ei); | |
515 if (e != null) { | |
516 var datum = scope.datum(e), | |
517 selected = fn(datum, ei, g.parent); | |
518 scope.associate(selected, datum); | |
519 u.elements[ei] = selected; | |
520 subgroup.add(selected); | |
521 } else { | |
522 subgroup.add(null); | |
523 } | |
524 } | |
525 subgroups.add(new _SelectionGroupImpl(subgroup, parent: g.parent)); | |
526 } | |
527 return new _SelectionImpl.selectionGroups(subgroups, scope); | |
528 } | |
529 } | |
530 | |
531 /* Implementation of [ExitSelection] */ | |
532 class _ExitSelectionImpl extends _SelectionImpl implements ExitSelection { | |
533 final DataSelection update; | |
534 _ExitSelectionImpl(Iterable groups, DataSelection update) | |
535 : update = update, super.selectionGroups(groups, update.scope); | |
536 } | |
537 | |
538 class _SelectionGroupImpl implements SelectionGroup { | |
539 Iterable<Element> elements; | |
540 Element parent; | |
541 _SelectionGroupImpl(this.elements, {this.parent}); | |
542 } | |
OLD | NEW |