Sky DOM APIs

// ELEMENT TREE API

abstract class Node extends EventTarget {
  @override
  external List<EventTarget> getEventDispatchChain(); // O(N) in number of ancestors across shadow trees
  // implements EventTarget.getEventDispatchChain()
  // returns the event dispatch chain (including handling shadow trees)

  external Root get owner; // O(1)

  external ParentNode get parentNode; // O(1)
  Element get parentElement {
    if (parentNode is Element)
      return parentNode as Element;
    return null;
  }

  external Node get previousSibling; // O(1)
  Element get previousElementSibling {
    var result = previousSibling;
    while (result != null && result is! Element)
      result = result.previousSibling;
    return result as Element;
  }

  external Node get nextSibling; // O(1)
  Element get nextElementSibling {
    var result = nextSibling;
    while (result != null && result is! Element)
      result = result.nextSibling;
    return result as Element;
  }

  // TODO(ianh): rename insertBefore() and insertAfter() since the Web
  // has an insertBefore() that means something else. What's a good
  // name, though?

  external void _insertBefore(Node node); // O(N) in number of descendants
  // node must be Text or Element, parentNode must be non-null
  void insertBefore(List nodes) {
    List.forEach((node) {
      if (node is String)
        node = new Text(node);
      _insertBefore(node);
    });    
  }

  external void _insertAfter(Node node); // O(N) in number of arguments plus all their descendants
  // node must be Text or Element, parentNode must be non-null
  void insertAfter(List nodes) {
    var lastNode = this;
    List.forEach((node) {
      if (node is String)
        node = new Text(node);
      lastNode._insertAfter(node);
      lastNode = node;
    });
  }

  void replaceWith(List nodes) {
    if (nextSibling != null) {
      var anchor = nextSibling;
      remove(); // parentNode can't be null here, so this won't throw
      anchor.insertBefore(nodes);
    } else {
      var anchor = parentNode;
      remove(); // throws if parentNode is null
      anchor.append(nodes);
    }
  }

  external void remove(); // O(N) in number of descendants
  // parentNode must be non-null

  // called when parentNode changes
  // this is why insertBefore(), append(), et al, are O(N) -- the whole affected subtree is walked
  // mutating the element tree from within this is strongly discouraged, since it will result in the
  // callbacks being invoked while the element tree is in a different state than implied by the callbacks
  external void parentChangedCallback(ParentNode oldParent, ParentNode newParent); // O(N) in descendants
  // default implementation calls attached/detached
  void attachedCallback() { }
  void detachedCallback() { }

  external List<ContentElement> getDestinationInsertionPoints(); // O(N) in number of insertion points the node is in
  // returns the <content> elements to which this element was distributed

  external Node cloneNode({bool deep: false}); // O(1) if deep=false, O(N) in the number of descendants if deep=true

  external ElementStyleDeclarationList get style; // O(1)
  // for nodes that aren't in the ApplicationRoot's composed tree,
  // returns null (so in particular orphaned subtrees and nodes in
  // module Roots don't have one, nor do shadow tree Roots)
  // also always returns null for ContentElement elements
  //  -- should be (lazily) updated when the node's parent chain
  //     changes (same time as, e.g., the id hashtable is marked
  //     dirty)

  external RenderNode get renderNode; // O(1)
  // this will be null until the first time it is rendered
  // it becomes null again when it is taken out of the rendering (see style.md)

  Type getLayoutManager() => null; // O(1)

  void resetLayoutManager() { // O(1)
    if (renderNode != null) {
      renderNode._layoutManager = null;
      renderNode._needsManager = true;
    }
  }
}

abstract class ParentNode extends Node {
  external Node get firstChild; // O(1)
  Element get firstElementChild {
    var result = firstChild;
    while (result != null && result is! Element)
      result = result.nextSibling;
    return result as Element;
  }

  external Node get lastChild; // O(1)
  Element get lastElementChild {
    var result = lastChild;
    while (result != null && result is! Element)
      result = result.previousSibling;
    return result as Element;
  }

  // Returns a new List every time.
  external List<Node> getChildren(); // O(N) in number of child nodes
  List<Element> getChildElements() {
    // that the following works without a cast is absurd
    return getChildren().where((node) => node is Element).toList();
  }

  external void _appendChild(Node node); // O(N) in number of descendants
  // node must be Text or Element
  void appendChild(node) {
    if (node is String)
      node = new Text(node);
    _appendChild(node);
  }
  void append(List nodes) {
    nodes.forEach(appendChild);
  }

  external void _prependChild(Node node); // O(N) in number of descendants
  // node must be Text or Element
  void prependChild(node) {
    if (node is String)
      node = new Text(node);
    _prependChild(node);
  }
  void prepend(List nodes) {
    // note: not implemented in terms of _prependChild()
    if (firstChild != null)
      firstChild.insertBefore(nodes);
    else
      append(nodes);
  }

  external void removeChildren(); // O(N) in number of descendants
  void setChild(node) {
    removeChildren();
    appendChild(node);
  }
  void setChildren(List nodes) {
    removeChildren();
    append(nodes);
  }
}

class Attr {
  const Attr (this.name, [this.value = '']); // O(1)
  final String name; // O(1)
  final String value; // O(1)
}

// @hasShadow annotation for registering elements
class _HasShadow {
  const _HasShadow();
}
const hasShadow = const _HasShadow();

abstract class Element extends ParentNode {
  Element({Map<String, String> attributes: null,
           List children: null,
           Module hostModule: null}) { // O(M+N), M = number of attributes, N = number of children nodes plus all their descendants
    var shadowClass = reflectClass(hasShadow.runtimeType);
    var shadowAnnotations = reflect(this).type.metadata.where((mirror) => mirror.type == shadowClass);
    if (shadowAnnotations.length > 2)
      throw new StateError('@hasShadow specified multiple times on ' + currentMirrorSystem().getName(reflectClass(this.runtimeType).simpleName));
    bool needsShadow = shadowAnnotations.length == 1;
    if (children != null)
      children = children.map((node) => node is String ? new Text(node) : node).toList();
    this._initElement(attributes, children, hostModule, needsShadow);
  }
  external void _initElement(Map<String, String> attributes, List children, Module hostModule, bool needsShadow);
  // initialises the internal attributes table, which is a ordered list
  // appends the given children nodes
  // children must be Text or Element
  // if needsShadow is true, creates a shadow tree

  external bool hasAttribute(String name); // O(N) in number of attributes
  external String getAttribute(String name); // O(N) in number of attributes
  external void setAttribute(String name, [String value = '']); // O(N) in number of attributes
  external void removeAttribute(String name); // O(N) in number of attributes
  // calling setAttribute() with a null value removes the attribute
  // (calling it without a value sets it to the empty string)

  // Returns a new Array and new Attr instances every time.
  external List<Attr> getAttributes(); // O(N) in number of attributes

  external Root get shadowRoot; // O(1)
  // returns the shadow root

  void endTagParsedCallback() { }
  void attributeChangedCallback(String name, String oldValue, String newValue) { }
  // name will never be null when this is called by sky

  // TODO(ianh): does a node ever need to know when it's been redistributed?

  @override
  Type getLayoutManager() { // O(1)
    if (renderNode)
      return renderNode.getProperty(phDisplay);
    return super.getLayoutManager();
  }
}

class Text extends Node {
  external Text([String value = '']); // O(1)

  external String get value; // O(1)
  external void set (String value); // O(1)

  void valueChangedCallback(String oldValue, String newValue) { }

  @override
  Type getLayoutManager() => TextLayoutManager; // O(1)
}

class Fragment extends ParentNode {
  Fragment({List children}); // O(N) in number of arguments plus all their descendants
  // children must be String, Text, or Element
}

class Root extends ParentNode {
  Root({List children: null, this.host}) { // O(N) in number of children nodes plus all their descendants
    if (children != null)
      children = children.map((node) => node is String ? new Text(node) : node).toList();
    this._initRoot(children);
  }
  external void _initRoot(List children);
  // appends the given children nodes
  // children must be Text or Element

  final Element host;

  external Element findId(String id); // O(1)
  // throws if id is null
}

class ApplicationRoot extends Root {
  ApplicationRoot ({List children}) : super(children: children); // O(N) in number of children nodes arguments plus all their descendants

  @override
  Type getLayoutManager() => rootLayoutManager; // O(1)
}

Type rootLayoutManager = BlockLayoutManager; // O(1)

class SelectorQuery {
  external SelectorQuery(String selector); // O(F()) where F() is the complexity of the selector

  external bool matches(Element element); // O(F())
  external Element find(Node root); // O(N*F())+O(M) where N is the number of descendants and M the average depth of the tree
  external List<Element> findAll(Node root); // O(N*F())+O(N*M) where N is the number of descendants and M the average depth of the tree
  // find() and findAll() throw if the root is not one of the following:
  //  - Element
  //  - Fragment
  //  - Root
}