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