| 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 |