OLD | NEW |
(Empty) | |
| 1 part of fn; |
| 2 |
| 3 void parentInsertBefore(sky.ParentNode parent, |
| 4 sky.Node node, |
| 5 sky.Node ref) { |
| 6 if (ref != null) { |
| 7 ref.insertBefore([node]); |
| 8 } else { |
| 9 parent.appendChild(node); |
| 10 } |
| 11 } |
| 12 |
| 13 abstract class Node { |
| 14 String _key = null; |
| 15 sky.Node _root = null; |
| 16 |
| 17 Node({ Object key }) { |
| 18 _key = key == null ? "$runtimeType" : "$runtimeType-$key"; |
| 19 } |
| 20 |
| 21 // Return true IFF the old node has *become* the new node (should be |
| 22 // retained because it is stateful) |
| 23 bool _sync(Node old, sky.ParentNode host, sky.Node insertBefore); |
| 24 |
| 25 void _remove() { |
| 26 assert(_root != null); |
| 27 _root.remove(); |
| 28 _root = null; |
| 29 } |
| 30 } |
| 31 |
| 32 class Text extends Node { |
| 33 String data; |
| 34 |
| 35 // Text nodes are special cases of having non-unique keys (which don't need |
| 36 // to be assigned as part of the API). Since they are unique in not having |
| 37 // children, there's little point to reordering, so we always just re-assign |
| 38 // the data. |
| 39 Text(this.data) : super(key:'*text*'); |
| 40 |
| 41 bool _sync(Node old, sky.ParentNode host, sky.Node insertBefore) { |
| 42 if (old == null) { |
| 43 _root = new sky.Text(data); |
| 44 parentInsertBefore(host, _root, insertBefore); |
| 45 return false; |
| 46 } |
| 47 |
| 48 _root = old._root; |
| 49 (_root as sky.Text).data = data; |
| 50 return false; |
| 51 } |
| 52 } |
| 53 |
| 54 var _emptyList = new List<Node>(); |
| 55 |
| 56 abstract class Element extends Node { |
| 57 |
| 58 String get _tagName; |
| 59 |
| 60 Element get _emptyElement; |
| 61 |
| 62 String inlineStyle; |
| 63 |
| 64 sky.EventListener onClick; |
| 65 sky.EventListener onFlingCancel; |
| 66 sky.EventListener onFlingStart; |
| 67 sky.EventListener onGestureTap; |
| 68 sky.EventListener onPointerCancel; |
| 69 sky.EventListener onPointerDown; |
| 70 sky.EventListener onPointerMove; |
| 71 sky.EventListener onPointerUp; |
| 72 sky.EventListener onScrollEnd; |
| 73 sky.EventListener onScrollStart; |
| 74 sky.EventListener onScrollUpdate; |
| 75 sky.EventListener onWheel; |
| 76 |
| 77 List<Node> _children = null; |
| 78 String _className = ''; |
| 79 |
| 80 Element({ |
| 81 Object key, |
| 82 List<Node> children, |
| 83 Style style, |
| 84 |
| 85 this.inlineStyle, |
| 86 |
| 87 // Events |
| 88 this.onClick, |
| 89 this.onFlingCancel, |
| 90 this.onFlingStart, |
| 91 this.onGestureTap, |
| 92 this.onPointerCancel, |
| 93 this.onPointerDown, |
| 94 this.onPointerMove, |
| 95 this.onPointerUp, |
| 96 this.onScrollEnd, |
| 97 this.onScrollStart, |
| 98 this.onScrollUpdate, |
| 99 this.onWheel |
| 100 }) : super(key:key) { |
| 101 |
| 102 _className = style == null ? '': style._className; |
| 103 _children = children == null ? _emptyList : children; |
| 104 |
| 105 if (debugWarnings()) { |
| 106 _debugReportDuplicateIds(); |
| 107 } |
| 108 } |
| 109 |
| 110 void _remove() { |
| 111 super._remove(); |
| 112 if (_children != null) { |
| 113 for (var child in _children) { |
| 114 child._remove(); |
| 115 } |
| 116 } |
| 117 _children = null; |
| 118 } |
| 119 |
| 120 void _debugReportDuplicateIds() { |
| 121 var idSet = new HashSet<String>(); |
| 122 for (var child in _children) { |
| 123 if (child is Text) { |
| 124 continue; // Text nodes all have the same key and are never reordered. |
| 125 } |
| 126 |
| 127 if (!idSet.add(child._key)) { |
| 128 throw '''If multiple (non-Text) nodes of the same type exist as children |
| 129 of another node, they must have unique keys.'''; |
| 130 } |
| 131 } |
| 132 } |
| 133 |
| 134 void _syncEvent(String eventName, sky.EventListener listener, |
| 135 sky.EventListener oldListener) { |
| 136 sky.Element root = _root as sky.Element; |
| 137 if (listener == oldListener) |
| 138 return; |
| 139 |
| 140 if (oldListener != null) { |
| 141 root.removeEventListener(eventName, oldListener); |
| 142 } |
| 143 |
| 144 if (listener != null) { |
| 145 root.addEventListener(eventName, listener); |
| 146 } |
| 147 } |
| 148 |
| 149 void _syncEvents([Element old]) { |
| 150 _syncEvent('click', onClick, old.onClick); |
| 151 _syncEvent('gestureflingcancel', onFlingCancel, old.onFlingCancel); |
| 152 _syncEvent('gestureflingstart', onFlingStart, old.onFlingStart); |
| 153 _syncEvent('gesturescrollend', onScrollEnd, old.onScrollEnd); |
| 154 _syncEvent('gesturescrollstart', onScrollStart, old.onScrollStart); |
| 155 _syncEvent('gesturescrollupdate', onScrollUpdate, old.onScrollUpdate); |
| 156 _syncEvent('gesturetap', onGestureTap, old.onGestureTap); |
| 157 _syncEvent('pointercancel', onPointerCancel, old.onPointerCancel); |
| 158 _syncEvent('pointerdown', onPointerDown, old.onPointerDown); |
| 159 _syncEvent('pointermove', onPointerMove, old.onPointerMove); |
| 160 _syncEvent('pointerup', onPointerUp, old.onPointerUp); |
| 161 _syncEvent('wheel', onWheel, old.onWheel); |
| 162 } |
| 163 |
| 164 void _syncNode([Element old]) { |
| 165 if (old == null) { |
| 166 old = _emptyElement; |
| 167 } |
| 168 |
| 169 _syncEvents(old); |
| 170 |
| 171 sky.Element root = _root as sky.Element; |
| 172 if (_className != old._className) { |
| 173 root.setAttribute('class', _className); |
| 174 } |
| 175 |
| 176 if (inlineStyle != old.inlineStyle) { |
| 177 root.setAttribute('style', inlineStyle); |
| 178 } |
| 179 } |
| 180 |
| 181 bool _sync(Node old, sky.ParentNode host, sky.Node insertBefore) { |
| 182 // print("---Syncing children of $_key"); |
| 183 |
| 184 Element oldElement = old as Element; |
| 185 |
| 186 if (oldElement == null) { |
| 187 // print("...no oldElement, initial render"); |
| 188 |
| 189 _root = sky.document.createElement(_tagName); |
| 190 _syncNode(); |
| 191 |
| 192 for (var child in _children) { |
| 193 child._sync(null, _root, null); |
| 194 assert(child._root is sky.Node); |
| 195 } |
| 196 |
| 197 parentInsertBefore(host, _root, insertBefore); |
| 198 return false; |
| 199 } |
| 200 |
| 201 _root = oldElement._root; |
| 202 oldElement._root = null; |
| 203 sky.Element root = (_root as sky.Element); |
| 204 |
| 205 _syncNode(oldElement); |
| 206 |
| 207 var startIndex = 0; |
| 208 var endIndex = _children.length; |
| 209 |
| 210 var oldChildren = oldElement._children; |
| 211 var oldStartIndex = 0; |
| 212 var oldEndIndex = oldChildren.length; |
| 213 |
| 214 sky.Node nextSibling = null; |
| 215 Node currentNode = null; |
| 216 Node oldNode = null; |
| 217 |
| 218 void sync(int atIndex) { |
| 219 if (currentNode._sync(oldNode, root, nextSibling)) { |
| 220 // oldNode was stateful and must be retained. |
| 221 assert(oldNode != null); |
| 222 currentNode = oldNode; |
| 223 _children[atIndex] = currentNode; |
| 224 } |
| 225 assert(currentNode._root is sky.Node); |
| 226 } |
| 227 |
| 228 // Scan backwards from end of list while nodes can be directly synced |
| 229 // without reordering. |
| 230 // print("...scanning backwards"); |
| 231 while (endIndex > startIndex && oldEndIndex > oldStartIndex) { |
| 232 currentNode = _children[endIndex - 1]; |
| 233 oldNode = oldChildren[oldEndIndex - 1]; |
| 234 |
| 235 if (currentNode._key != oldNode._key) { |
| 236 break; |
| 237 } |
| 238 |
| 239 // print('> syncing matched at: $endIndex : $oldEndIndex'); |
| 240 endIndex--; |
| 241 oldEndIndex--; |
| 242 sync(endIndex); |
| 243 nextSibling = currentNode._root; |
| 244 } |
| 245 |
| 246 HashMap<String, Node> oldNodeIdMap = null; |
| 247 |
| 248 bool oldNodeReordered(String key) { |
| 249 return oldNodeIdMap != null && |
| 250 oldNodeIdMap.containsKey(key) && |
| 251 oldNodeIdMap[key] == null; |
| 252 } |
| 253 |
| 254 void advanceOldStartIndex() { |
| 255 oldStartIndex++; |
| 256 while (oldStartIndex < oldEndIndex && |
| 257 oldNodeReordered(oldChildren[oldStartIndex]._key)) { |
| 258 oldStartIndex++; |
| 259 } |
| 260 } |
| 261 |
| 262 void ensureOldIdMap() { |
| 263 if (oldNodeIdMap != null) |
| 264 return; |
| 265 |
| 266 oldNodeIdMap = new HashMap<String, Node>(); |
| 267 for (int i = oldStartIndex; i < oldEndIndex; i++) { |
| 268 var node = oldChildren[i]; |
| 269 if (node is! Text) { |
| 270 oldNodeIdMap.putIfAbsent(node._key, () => node); |
| 271 } |
| 272 } |
| 273 } |
| 274 |
| 275 bool searchForOldNode() { |
| 276 if (currentNode is Text) |
| 277 return false; // Never re-order Text nodes. |
| 278 |
| 279 ensureOldIdMap(); |
| 280 oldNode = oldNodeIdMap[currentNode._key]; |
| 281 if (oldNode == null) |
| 282 return false; |
| 283 |
| 284 oldNodeIdMap[currentNode._key] = null; // mark it reordered. |
| 285 // print("Reparenting ${currentNode._key}"); |
| 286 parentInsertBefore(root, oldNode._root, nextSibling); |
| 287 return true; |
| 288 } |
| 289 |
| 290 // Scan forwards, this time we may re-order; |
| 291 // print("...scanning forward"); |
| 292 nextSibling = root.firstChild; |
| 293 while (startIndex < endIndex && oldStartIndex < oldEndIndex) { |
| 294 currentNode = _children[startIndex]; |
| 295 oldNode = oldChildren[oldStartIndex]; |
| 296 |
| 297 if (currentNode._key == oldNode._key) { |
| 298 // print('> syncing matched at: $startIndex : $oldStartIndex'); |
| 299 assert(currentNode.runtimeType == oldNode.runtimeType); |
| 300 nextSibling = nextSibling.nextSibling; |
| 301 sync(startIndex); |
| 302 startIndex++; |
| 303 advanceOldStartIndex(); |
| 304 continue; |
| 305 } |
| 306 |
| 307 oldNode = null; |
| 308 if (searchForOldNode()) { |
| 309 // print('> reordered to $startIndex'); |
| 310 } else { |
| 311 // print('> inserting at $startIndex'); |
| 312 } |
| 313 |
| 314 sync(startIndex); |
| 315 startIndex++; |
| 316 } |
| 317 |
| 318 // New insertions |
| 319 oldNode = null; |
| 320 // print('...processing remaining insertions'); |
| 321 while (startIndex < endIndex) { |
| 322 // print('> inserting at $startIndex'); |
| 323 currentNode = _children[startIndex]; |
| 324 sync(startIndex); |
| 325 startIndex++; |
| 326 } |
| 327 |
| 328 // Removals |
| 329 // print('...processing remaining removals'); |
| 330 currentNode = null; |
| 331 while (oldStartIndex < oldEndIndex) { |
| 332 oldNode = oldChildren[oldStartIndex]; |
| 333 // print('> ${oldNode._key} removing from $oldEndIndex'); |
| 334 oldNode._remove(); |
| 335 advanceOldStartIndex(); |
| 336 } |
| 337 |
| 338 oldElement._children = null; |
| 339 return false; |
| 340 } |
| 341 } |
| 342 |
| 343 class Container extends Element { |
| 344 |
| 345 String get _tagName => 'div'; |
| 346 |
| 347 static Container _emptyContainer = new Container(); |
| 348 |
| 349 Element get _emptyElement => _emptyContainer; |
| 350 |
| 351 Container({ |
| 352 Object key, |
| 353 List<Node> children, |
| 354 Style style, |
| 355 String inlineStyle, |
| 356 sky.EventListener onClick, |
| 357 sky.EventListener onFlingCancel, |
| 358 sky.EventListener onFlingStart, |
| 359 sky.EventListener onGestureTap, |
| 360 sky.EventListener onPointerCancel, |
| 361 sky.EventListener onPointerDown, |
| 362 sky.EventListener onPointerMove, |
| 363 sky.EventListener onPointerUp, |
| 364 sky.EventListener onScrollEnd, |
| 365 sky.EventListener onScrollStart, |
| 366 sky.EventListener onScrollUpdate, |
| 367 sky.EventListener onWheel |
| 368 }) : super( |
| 369 key: key, |
| 370 children: children, |
| 371 style: style, |
| 372 inlineStyle: inlineStyle, |
| 373 onClick: onClick, |
| 374 onFlingCancel: onFlingCancel, |
| 375 onFlingStart: onFlingStart, |
| 376 onGestureTap: onGestureTap, |
| 377 onPointerCancel: onPointerCancel, |
| 378 onPointerDown: onPointerDown, |
| 379 onPointerMove: onPointerMove, |
| 380 onPointerUp: onPointerUp, |
| 381 onScrollEnd: onScrollEnd, |
| 382 onScrollStart: onScrollStart, |
| 383 onScrollUpdate: onScrollUpdate, |
| 384 onWheel: onWheel |
| 385 ); |
| 386 } |
| 387 |
| 388 class Image extends Element { |
| 389 |
| 390 String get _tagName => 'img'; |
| 391 |
| 392 static Image _emptyImage = new Image(); |
| 393 Element get _emptyElement => _emptyImage; |
| 394 |
| 395 String src; |
| 396 int width; |
| 397 int height; |
| 398 |
| 399 Image({ |
| 400 Object key, |
| 401 List<Node> children, |
| 402 Style style, |
| 403 String inlineStyle, |
| 404 sky.EventListener onClick, |
| 405 sky.EventListener onFlingCancel, |
| 406 sky.EventListener onFlingStart, |
| 407 sky.EventListener onGestureTap, |
| 408 sky.EventListener onPointerCancel, |
| 409 sky.EventListener onPointerDown, |
| 410 sky.EventListener onPointerMove, |
| 411 sky.EventListener onPointerUp, |
| 412 sky.EventListener onScrollEnd, |
| 413 sky.EventListener onScrollStart, |
| 414 sky.EventListener onScrollUpdate, |
| 415 sky.EventListener onWheel, |
| 416 this.width, |
| 417 this.height, |
| 418 this.src |
| 419 }) : super( |
| 420 key: key, |
| 421 children: children, |
| 422 style: style, |
| 423 inlineStyle: inlineStyle, |
| 424 onClick: onClick, |
| 425 onFlingCancel: onFlingCancel, |
| 426 onFlingStart: onFlingStart, |
| 427 onGestureTap: onGestureTap, |
| 428 onPointerCancel: onPointerCancel, |
| 429 onPointerDown: onPointerDown, |
| 430 onPointerMove: onPointerMove, |
| 431 onPointerUp: onPointerUp, |
| 432 onScrollEnd: onScrollEnd, |
| 433 onScrollStart: onScrollStart, |
| 434 onScrollUpdate: onScrollUpdate, |
| 435 onWheel: onWheel |
| 436 ); |
| 437 |
| 438 void _syncNode([Element old]) { |
| 439 super._syncNode(old); |
| 440 |
| 441 Image oldImage = old != null ? old : _emptyImage; |
| 442 sky.HTMLImageElement skyImage = _root as sky.HTMLImageElement; |
| 443 if (src != oldImage.src) { |
| 444 skyImage.src = src; |
| 445 } |
| 446 |
| 447 if (width != oldImage.width) { |
| 448 skyImage.style['width'] = '${width}px'; |
| 449 } |
| 450 if (height != oldImage.height) { |
| 451 skyImage.style['height'] = '${height}px'; |
| 452 } |
| 453 } |
| 454 } |
| 455 |
| 456 class Anchor extends Element { |
| 457 |
| 458 String get _tagName => 'a'; |
| 459 |
| 460 static Anchor _emptyAnchor = new Anchor(); |
| 461 |
| 462 String href; |
| 463 |
| 464 Anchor({ |
| 465 Object key, |
| 466 List<Node> children, |
| 467 Style style, |
| 468 String inlineStyle, |
| 469 sky.EventListener onClick, |
| 470 sky.EventListener onFlingCancel, |
| 471 sky.EventListener onFlingStart, |
| 472 sky.EventListener onGestureTap, |
| 473 sky.EventListener onPointerCancel, |
| 474 sky.EventListener onPointerDown, |
| 475 sky.EventListener onPointerMove, |
| 476 sky.EventListener onPointerUp, |
| 477 sky.EventListener onScrollEnd, |
| 478 sky.EventListener onScrollStart, |
| 479 sky.EventListener onScrollUpdate, |
| 480 sky.EventListener onWheel, |
| 481 this.width, |
| 482 this.height, |
| 483 this.href |
| 484 }) : super( |
| 485 key: key, |
| 486 children: children, |
| 487 style: style, |
| 488 inlineStyle: inlineStyle, |
| 489 onClick: onClick, |
| 490 onFlingCancel: onFlingCancel, |
| 491 onFlingStart: onFlingStart, |
| 492 onGestureTap: onGestureTap, |
| 493 onPointerCancel: onPointerCancel, |
| 494 onPointerDown: onPointerDown, |
| 495 onPointerMove: onPointerMove, |
| 496 onPointerUp: onPointerUp, |
| 497 onScrollEnd: onScrollEnd, |
| 498 onScrollStart: onScrollStart, |
| 499 onScrollUpdate: onScrollUpdate, |
| 500 onWheel: onWheel |
| 501 ); |
| 502 |
| 503 void _syncNode([Element old]) { |
| 504 Anchor oldAnchor = old != null ? old as Anchor : _emptyAnchor; |
| 505 super._syncNode(oldAnchor); |
| 506 |
| 507 sky.HTMLAnchorElement skyAnchor = _root as sky.HTMLAnchorElement; |
| 508 if (href != oldAnchor.href) { |
| 509 skyAnchor.href = href; |
| 510 } |
| 511 } |
| 512 } |
OLD | NEW |