Index: sky/examples/fn/lib/node.dart |
diff --git a/sky/examples/fn/lib/node.dart b/sky/examples/fn/lib/node.dart |
new file mode 100644 |
index 0000000000000000000000000000000000000000..f7ea1a301428b6fd3156f878ab8a6d139fdfa7ea |
--- /dev/null |
+++ b/sky/examples/fn/lib/node.dart |
@@ -0,0 +1,512 @@ |
+part of fn; |
+ |
+void parentInsertBefore(sky.ParentNode parent, |
+ sky.Node node, |
+ sky.Node ref) { |
+ if (ref != null) { |
+ ref.insertBefore([node]); |
+ } else { |
+ parent.appendChild(node); |
+ } |
+} |
+ |
+abstract class Node { |
+ String _key = null; |
+ sky.Node _root = null; |
+ |
+ Node({ Object key }) { |
+ _key = key == null ? "$runtimeType" : "$runtimeType-$key"; |
+ } |
+ |
+ // Return true IFF the old node has *become* the new node (should be |
+ // retained because it is stateful) |
+ bool _sync(Node old, sky.ParentNode host, sky.Node insertBefore); |
+ |
+ void _remove() { |
+ assert(_root != null); |
+ _root.remove(); |
+ _root = null; |
+ } |
+} |
+ |
+class Text extends Node { |
+ String data; |
+ |
+ // Text nodes are special cases of having non-unique keys (which don't need |
+ // to be assigned as part of the API). Since they are unique in not having |
+ // children, there's little point to reordering, so we always just re-assign |
+ // the data. |
+ Text(this.data) : super(key:'*text*'); |
+ |
+ bool _sync(Node old, sky.ParentNode host, sky.Node insertBefore) { |
+ if (old == null) { |
+ _root = new sky.Text(data); |
+ parentInsertBefore(host, _root, insertBefore); |
+ return false; |
+ } |
+ |
+ _root = old._root; |
+ (_root as sky.Text).data = data; |
+ return false; |
+ } |
+} |
+ |
+var _emptyList = new List<Node>(); |
+ |
+abstract class Element extends Node { |
+ |
+ String get _tagName; |
+ |
+ Element get _emptyElement; |
+ |
+ String inlineStyle; |
+ |
+ sky.EventListener onClick; |
+ sky.EventListener onFlingCancel; |
+ sky.EventListener onFlingStart; |
+ sky.EventListener onGestureTap; |
+ sky.EventListener onPointerCancel; |
+ sky.EventListener onPointerDown; |
+ sky.EventListener onPointerMove; |
+ sky.EventListener onPointerUp; |
+ sky.EventListener onScrollEnd; |
+ sky.EventListener onScrollStart; |
+ sky.EventListener onScrollUpdate; |
+ sky.EventListener onWheel; |
+ |
+ List<Node> _children = null; |
+ String _className = ''; |
+ |
+ Element({ |
+ Object key, |
+ List<Node> children, |
+ Style style, |
+ |
+ this.inlineStyle, |
+ |
+ // Events |
+ this.onClick, |
+ this.onFlingCancel, |
+ this.onFlingStart, |
+ this.onGestureTap, |
+ this.onPointerCancel, |
+ this.onPointerDown, |
+ this.onPointerMove, |
+ this.onPointerUp, |
+ this.onScrollEnd, |
+ this.onScrollStart, |
+ this.onScrollUpdate, |
+ this.onWheel |
+ }) : super(key:key) { |
+ |
+ _className = style == null ? '': style._className; |
+ _children = children == null ? _emptyList : children; |
+ |
+ if (debugWarnings()) { |
+ _debugReportDuplicateIds(); |
+ } |
+ } |
+ |
+ void _remove() { |
+ super._remove(); |
+ if (_children != null) { |
+ for (var child in _children) { |
+ child._remove(); |
+ } |
+ } |
+ _children = null; |
+ } |
+ |
+ void _debugReportDuplicateIds() { |
+ var idSet = new HashSet<String>(); |
+ for (var child in _children) { |
+ if (child is Text) { |
+ continue; // Text nodes all have the same key and are never reordered. |
+ } |
+ |
+ if (!idSet.add(child._key)) { |
+ throw '''If multiple (non-Text) nodes of the same type exist as children |
+ of another node, they must have unique keys.'''; |
+ } |
+ } |
+ } |
+ |
+ void _syncEvent(String eventName, sky.EventListener listener, |
+ sky.EventListener oldListener) { |
+ sky.Element root = _root as sky.Element; |
+ if (listener == oldListener) |
+ return; |
+ |
+ if (oldListener != null) { |
+ root.removeEventListener(eventName, oldListener); |
+ } |
+ |
+ if (listener != null) { |
+ root.addEventListener(eventName, listener); |
+ } |
+ } |
+ |
+ void _syncEvents([Element old]) { |
+ _syncEvent('click', onClick, old.onClick); |
+ _syncEvent('gestureflingcancel', onFlingCancel, old.onFlingCancel); |
+ _syncEvent('gestureflingstart', onFlingStart, old.onFlingStart); |
+ _syncEvent('gesturescrollend', onScrollEnd, old.onScrollEnd); |
+ _syncEvent('gesturescrollstart', onScrollStart, old.onScrollStart); |
+ _syncEvent('gesturescrollupdate', onScrollUpdate, old.onScrollUpdate); |
+ _syncEvent('gesturetap', onGestureTap, old.onGestureTap); |
+ _syncEvent('pointercancel', onPointerCancel, old.onPointerCancel); |
+ _syncEvent('pointerdown', onPointerDown, old.onPointerDown); |
+ _syncEvent('pointermove', onPointerMove, old.onPointerMove); |
+ _syncEvent('pointerup', onPointerUp, old.onPointerUp); |
+ _syncEvent('wheel', onWheel, old.onWheel); |
+ } |
+ |
+ void _syncNode([Element old]) { |
+ if (old == null) { |
+ old = _emptyElement; |
+ } |
+ |
+ _syncEvents(old); |
+ |
+ sky.Element root = _root as sky.Element; |
+ if (_className != old._className) { |
+ root.setAttribute('class', _className); |
+ } |
+ |
+ if (inlineStyle != old.inlineStyle) { |
+ root.setAttribute('style', inlineStyle); |
+ } |
+ } |
+ |
+ bool _sync(Node old, sky.ParentNode host, sky.Node insertBefore) { |
+ // print("---Syncing children of $_key"); |
+ |
+ Element oldElement = old as Element; |
+ |
+ if (oldElement == null) { |
+ // print("...no oldElement, initial render"); |
+ |
+ _root = sky.document.createElement(_tagName); |
+ _syncNode(); |
+ |
+ for (var child in _children) { |
+ child._sync(null, _root, null); |
+ assert(child._root is sky.Node); |
+ } |
+ |
+ parentInsertBefore(host, _root, insertBefore); |
+ return false; |
+ } |
+ |
+ _root = oldElement._root; |
+ oldElement._root = null; |
+ sky.Element root = (_root as sky.Element); |
+ |
+ _syncNode(oldElement); |
+ |
+ var startIndex = 0; |
+ var endIndex = _children.length; |
+ |
+ var oldChildren = oldElement._children; |
+ var oldStartIndex = 0; |
+ var oldEndIndex = oldChildren.length; |
+ |
+ sky.Node nextSibling = null; |
+ Node currentNode = null; |
+ Node oldNode = null; |
+ |
+ void sync(int atIndex) { |
+ if (currentNode._sync(oldNode, root, nextSibling)) { |
+ // oldNode was stateful and must be retained. |
+ assert(oldNode != null); |
+ currentNode = oldNode; |
+ _children[atIndex] = currentNode; |
+ } |
+ assert(currentNode._root is sky.Node); |
+ } |
+ |
+ // Scan backwards from end of list while nodes can be directly synced |
+ // without reordering. |
+ // print("...scanning backwards"); |
+ while (endIndex > startIndex && oldEndIndex > oldStartIndex) { |
+ currentNode = _children[endIndex - 1]; |
+ oldNode = oldChildren[oldEndIndex - 1]; |
+ |
+ if (currentNode._key != oldNode._key) { |
+ break; |
+ } |
+ |
+ // print('> syncing matched at: $endIndex : $oldEndIndex'); |
+ endIndex--; |
+ oldEndIndex--; |
+ sync(endIndex); |
+ nextSibling = currentNode._root; |
+ } |
+ |
+ HashMap<String, Node> oldNodeIdMap = null; |
+ |
+ bool oldNodeReordered(String key) { |
+ return oldNodeIdMap != null && |
+ oldNodeIdMap.containsKey(key) && |
+ oldNodeIdMap[key] == null; |
+ } |
+ |
+ void advanceOldStartIndex() { |
+ oldStartIndex++; |
+ while (oldStartIndex < oldEndIndex && |
+ oldNodeReordered(oldChildren[oldStartIndex]._key)) { |
+ oldStartIndex++; |
+ } |
+ } |
+ |
+ void ensureOldIdMap() { |
+ if (oldNodeIdMap != null) |
+ return; |
+ |
+ oldNodeIdMap = new HashMap<String, Node>(); |
+ for (int i = oldStartIndex; i < oldEndIndex; i++) { |
+ var node = oldChildren[i]; |
+ if (node is! Text) { |
+ oldNodeIdMap.putIfAbsent(node._key, () => node); |
+ } |
+ } |
+ } |
+ |
+ bool searchForOldNode() { |
+ if (currentNode is Text) |
+ return false; // Never re-order Text nodes. |
+ |
+ ensureOldIdMap(); |
+ oldNode = oldNodeIdMap[currentNode._key]; |
+ if (oldNode == null) |
+ return false; |
+ |
+ oldNodeIdMap[currentNode._key] = null; // mark it reordered. |
+ // print("Reparenting ${currentNode._key}"); |
+ parentInsertBefore(root, oldNode._root, nextSibling); |
+ return true; |
+ } |
+ |
+ // Scan forwards, this time we may re-order; |
+ // print("...scanning forward"); |
+ nextSibling = root.firstChild; |
+ while (startIndex < endIndex && oldStartIndex < oldEndIndex) { |
+ currentNode = _children[startIndex]; |
+ oldNode = oldChildren[oldStartIndex]; |
+ |
+ if (currentNode._key == oldNode._key) { |
+ // print('> syncing matched at: $startIndex : $oldStartIndex'); |
+ assert(currentNode.runtimeType == oldNode.runtimeType); |
+ nextSibling = nextSibling.nextSibling; |
+ sync(startIndex); |
+ startIndex++; |
+ advanceOldStartIndex(); |
+ continue; |
+ } |
+ |
+ oldNode = null; |
+ if (searchForOldNode()) { |
+ // print('> reordered to $startIndex'); |
+ } else { |
+ // print('> inserting at $startIndex'); |
+ } |
+ |
+ sync(startIndex); |
+ startIndex++; |
+ } |
+ |
+ // New insertions |
+ oldNode = null; |
+ // print('...processing remaining insertions'); |
+ while (startIndex < endIndex) { |
+ // print('> inserting at $startIndex'); |
+ currentNode = _children[startIndex]; |
+ sync(startIndex); |
+ startIndex++; |
+ } |
+ |
+ // Removals |
+ // print('...processing remaining removals'); |
+ currentNode = null; |
+ while (oldStartIndex < oldEndIndex) { |
+ oldNode = oldChildren[oldStartIndex]; |
+ // print('> ${oldNode._key} removing from $oldEndIndex'); |
+ oldNode._remove(); |
+ advanceOldStartIndex(); |
+ } |
+ |
+ oldElement._children = null; |
+ return false; |
+ } |
+} |
+ |
+class Container extends Element { |
+ |
+ String get _tagName => 'div'; |
+ |
+ static Container _emptyContainer = new Container(); |
+ |
+ Element get _emptyElement => _emptyContainer; |
+ |
+ Container({ |
+ Object key, |
+ List<Node> children, |
+ Style style, |
+ String inlineStyle, |
+ sky.EventListener onClick, |
+ sky.EventListener onFlingCancel, |
+ sky.EventListener onFlingStart, |
+ sky.EventListener onGestureTap, |
+ sky.EventListener onPointerCancel, |
+ sky.EventListener onPointerDown, |
+ sky.EventListener onPointerMove, |
+ sky.EventListener onPointerUp, |
+ sky.EventListener onScrollEnd, |
+ sky.EventListener onScrollStart, |
+ sky.EventListener onScrollUpdate, |
+ sky.EventListener onWheel |
+ }) : super( |
+ key: key, |
+ children: children, |
+ style: style, |
+ inlineStyle: inlineStyle, |
+ onClick: onClick, |
+ onFlingCancel: onFlingCancel, |
+ onFlingStart: onFlingStart, |
+ onGestureTap: onGestureTap, |
+ onPointerCancel: onPointerCancel, |
+ onPointerDown: onPointerDown, |
+ onPointerMove: onPointerMove, |
+ onPointerUp: onPointerUp, |
+ onScrollEnd: onScrollEnd, |
+ onScrollStart: onScrollStart, |
+ onScrollUpdate: onScrollUpdate, |
+ onWheel: onWheel |
+ ); |
+} |
+ |
+class Image extends Element { |
+ |
+ String get _tagName => 'img'; |
+ |
+ static Image _emptyImage = new Image(); |
+ Element get _emptyElement => _emptyImage; |
+ |
+ String src; |
+ int width; |
+ int height; |
+ |
+ Image({ |
+ Object key, |
+ List<Node> children, |
+ Style style, |
+ String inlineStyle, |
+ sky.EventListener onClick, |
+ sky.EventListener onFlingCancel, |
+ sky.EventListener onFlingStart, |
+ sky.EventListener onGestureTap, |
+ sky.EventListener onPointerCancel, |
+ sky.EventListener onPointerDown, |
+ sky.EventListener onPointerMove, |
+ sky.EventListener onPointerUp, |
+ sky.EventListener onScrollEnd, |
+ sky.EventListener onScrollStart, |
+ sky.EventListener onScrollUpdate, |
+ sky.EventListener onWheel, |
+ this.width, |
+ this.height, |
+ this.src |
+ }) : super( |
+ key: key, |
+ children: children, |
+ style: style, |
+ inlineStyle: inlineStyle, |
+ onClick: onClick, |
+ onFlingCancel: onFlingCancel, |
+ onFlingStart: onFlingStart, |
+ onGestureTap: onGestureTap, |
+ onPointerCancel: onPointerCancel, |
+ onPointerDown: onPointerDown, |
+ onPointerMove: onPointerMove, |
+ onPointerUp: onPointerUp, |
+ onScrollEnd: onScrollEnd, |
+ onScrollStart: onScrollStart, |
+ onScrollUpdate: onScrollUpdate, |
+ onWheel: onWheel |
+ ); |
+ |
+ void _syncNode([Element old]) { |
+ super._syncNode(old); |
+ |
+ Image oldImage = old != null ? old : _emptyImage; |
+ sky.HTMLImageElement skyImage = _root as sky.HTMLImageElement; |
+ if (src != oldImage.src) { |
+ skyImage.src = src; |
+ } |
+ |
+ if (width != oldImage.width) { |
+ skyImage.style['width'] = '${width}px'; |
+ } |
+ if (height != oldImage.height) { |
+ skyImage.style['height'] = '${height}px'; |
+ } |
+ } |
+} |
+ |
+class Anchor extends Element { |
+ |
+ String get _tagName => 'a'; |
+ |
+ static Anchor _emptyAnchor = new Anchor(); |
+ |
+ String href; |
+ |
+ Anchor({ |
+ Object key, |
+ List<Node> children, |
+ Style style, |
+ String inlineStyle, |
+ sky.EventListener onClick, |
+ sky.EventListener onFlingCancel, |
+ sky.EventListener onFlingStart, |
+ sky.EventListener onGestureTap, |
+ sky.EventListener onPointerCancel, |
+ sky.EventListener onPointerDown, |
+ sky.EventListener onPointerMove, |
+ sky.EventListener onPointerUp, |
+ sky.EventListener onScrollEnd, |
+ sky.EventListener onScrollStart, |
+ sky.EventListener onScrollUpdate, |
+ sky.EventListener onWheel, |
+ this.width, |
+ this.height, |
+ this.href |
+ }) : super( |
+ key: key, |
+ children: children, |
+ style: style, |
+ inlineStyle: inlineStyle, |
+ onClick: onClick, |
+ onFlingCancel: onFlingCancel, |
+ onFlingStart: onFlingStart, |
+ onGestureTap: onGestureTap, |
+ onPointerCancel: onPointerCancel, |
+ onPointerDown: onPointerDown, |
+ onPointerMove: onPointerMove, |
+ onPointerUp: onPointerUp, |
+ onScrollEnd: onScrollEnd, |
+ onScrollStart: onScrollStart, |
+ onScrollUpdate: onScrollUpdate, |
+ onWheel: onWheel |
+ ); |
+ |
+ void _syncNode([Element old]) { |
+ Anchor oldAnchor = old != null ? old as Anchor : _emptyAnchor; |
+ super._syncNode(oldAnchor); |
+ |
+ sky.HTMLAnchorElement skyAnchor = _root as sky.HTMLAnchorElement; |
+ if (href != oldAnchor.href) { |
+ skyAnchor.href = href; |
+ } |
+ } |
+} |