blob: af1644763a7f2b71f4ede4194513b671918cbfbe [file] [log] [blame]
// Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
part of $LIBRARYNAME;
/**
* Lazy implementation of the child nodes of an element that does not request
* the actual child nodes of an element until strictly necessary greatly
* improving performance for the typical cases where it is not required.
*/
class _ChildNodeListLazy extends ListBase<Node> {
final Node _this;
_ChildNodeListLazy(this._this);
$if DART2JS
Node get first {
Node result = JS('Node|Null', '#.firstChild', _this);
if (result == null) throw new StateError("No elements");
return result;
}
Node get last {
Node result = JS('Node|Null', '#.lastChild', _this);
if (result == null) throw new StateError("No elements");
return result;
}
Node get single {
int l = this.length;
if (l == 0) throw new StateError("No elements");
if (l > 1) throw new StateError("More than one element");
return JS('Node|Null', '#.firstChild', _this);
}
$else
Node get first {
Node result = _this.$dom_firstChild;
if (result == null) throw new StateError("No elements");
return result;
}
Node get last {
Node result = _this.$dom_lastChild;
if (result == null) throw new StateError("No elements");
return result;
}
Node get single {
int l = this.length;
if (l == 0) throw new StateError("No elements");
if (l > 1) throw new StateError("More than one element");
return _this.$dom_firstChild;
}
$endif
void add(Node value) {
_this.append(value);
}
void addAll(Iterable<Node> iterable) {
if (iterable is _ChildNodeListLazy) {
if (!identical(iterable._this, _this)) {
// Optimized route for copying between nodes.
for (var i = 0, len = iterable.length; i < len; ++i) {
// Should use $dom_firstChild, Bug 8886.
_this.append(iterable[0]);
}
}
return;
}
for (Node node in iterable) {
_this.append(node);
}
}
void insert(int index, Node node) {
if (index < 0 || index > length) {
throw new RangeError.range(index, 0, length);
}
if (index == length) {
_this.append(node);
} else {
_this.insertBefore(node, this[index]);
}
}
void insertAll(int index, Iterable<Node> iterable) {
throw new UnimplementedError();
}
void setAll(int index, Iterable<Node> iterable) {
throw new UnimplementedError();
}
Node removeLast() {
final result = last;
if (result != null) {
_this.$dom_removeChild(result);
}
return result;
}
Node removeAt(int index) {
var result = this[index];
if (result != null) {
_this.$dom_removeChild(result);
}
return result;
}
void remove(Object object) {
if (object is! Node) return;
Node node = object;
if (!identical(_this, node.parentNode)) return;
_this.$dom_removeChild(node);
}
void _filter(bool test(Node node), bool removeMatching) {
// This implementation of removeWhere/retainWhere is more efficient
// than the default in ListBase. Child nodes can be removed in constant
// time.
Node child = _this.$dom_firstChild;
while (child != null) {
Node nextChild = child.nextSibling;
if (test(child) == removeMatching) {
_this.$dom_removeChild(child);
}
child = nextChild;
}
}
void removeWhere(bool test(Node node)) {
_filter(test, true);
}
void retainWhere(bool test(Node node)) {
_filter(test, false);
}
void clear() {
_this.text = '';
}
void operator []=(int index, Node value) {
_this.$dom_replaceChild(value, this[index]);
}
Iterator<Node> get iterator => _this.$dom_childNodes.iterator;
List<Node> toList({ bool growable: true }) =>
new List<Node>.from(this, growable: growable);
Set<Node> toSet() => new Set<Node>.from(this);
bool get isEmpty => this.length == 0;
// From List<Node>:
// TODO(jacobr): this could be implemented for child node lists.
// The exception we throw here is misleading.
void sort([int compare(Node a, Node b)]) {
throw new UnsupportedError("Cannot sort immutable List.");
}
// FIXME: implement these.
void setRange(int start, int end, Iterable<Node> iterable,
[int skipCount = 0]) {
throw new UnsupportedError(
"Cannot setRange on immutable List.");
}
void removeRange(int start, int end) {
throw new UnsupportedError(
"Cannot removeRange on immutable List.");
}
Iterable<Node> getRange(int start, int end) {
throw new UnimplementedError("NodeList.getRange");
}
void replaceRange(int start, int end, Iterable<Node> iterable) {
throw new UnimplementedError("NodeList.replaceRange");
}
void fillRange(int start, int end, [Node fillValue]) {
throw new UnimplementedError("NodeList.fillRange");
}
List<Node> sublist(int start, [int end]) {
if (end == null) end == length;
return Lists.getRange(this, start, end, <Node>[]);
}
String toString() {
StringBuffer buffer = new StringBuffer('[');
buffer.writeAll(this, ', ');
buffer.write(']');
return buffer.toString();
}
// -- end List<Node> mixins.
// TODO(jacobr): benchmark whether this is more efficient or whether caching
// a local copy of $dom_childNodes is more efficient.
int get length => _this.$dom_childNodes.length;
void set length(int value) {
throw new UnsupportedError(
"Cannot set length on immutable List.");
}
Node operator[](int index) => _this.$dom_childNodes[index];
}
$(ANNOTATIONS)class $CLASSNAME$EXTENDS$IMPLEMENTS$NATIVESPEC {
List<Node> get nodes {
return new _ChildNodeListLazy(this);
}
void set nodes(Iterable<Node> value) {
// Copy list first since we don't want liveness during iteration.
// TODO(jacobr): there is a better way to do this.
List copy = new List.from(value);
text = '';
for (Node node in copy) {
append(node);
}
}
/**
* Removes this node from the DOM.
*/
@DomName('Node.removeChild')
void remove() {
// TODO(jacobr): should we throw an exception if parent is already null?
// TODO(vsm): Use the native remove when available.
if (this.parentNode != null) {
final Node parent = this.parentNode;
parentNode.$dom_removeChild(this);
}
}
/**
* Replaces this node with another node.
*/
@DomName('Node.replaceChild')
Node replaceWith(Node otherNode) {
try {
final Node parent = this.parentNode;
parent.$dom_replaceChild(otherNode, this);
} catch (e) {
};
return this;
}
/**
* Inserts all of the nodes into this node directly before refChild.
*
* See also:
*
* * [insertBefore]
*/
Node insertAllBefore(Iterable<Node> newNodes, Node refChild) {
if (newNodes is _ChildNodeListLazy) {
if (identical(newNodes._this, this)) {
throw new ArgumentError(newNodes);
}
// Optimized route for copying between nodes.
for (var i = 0, len = newNodes.length; i < len; ++i) {
// Should use $dom_firstChild, Bug 8886.
this.insertBefore(newNodes[0], refChild);
}
} else {
for (var node in newNodes) {
this.insertBefore(node, refChild);
}
}
}
// Note that this may either be the locally set model or a cached value
// of the inherited model. This is cached to minimize model change
// notifications.
$if DART2JS
@Creates('Null')
$endif
var _model;
bool _hasLocalModel;
Set<StreamController<Node>> _modelChangedStreams;
/**
* The data model which is inherited through the tree.
*
* Setting this will propagate the value to all descendant nodes. If the
* model is not set on this node then it will be inherited from ancestor
* nodes.
*
* Currently this does not support propagation through Shadow DOMs.
*
* [clearModel] must be used to remove the model property from this node
* and have the model inherit from ancestor nodes.
*/
@Experimental
get model {
// If we have a change handler then we've cached the model locally.
if (_modelChangedStreams != null && !_modelChangedStreams.isEmpty) {
return _model;
}
// Otherwise start looking up the tree.
for (var node = this; node != null; node = node.parentNode) {
if (node._hasLocalModel == true) {
return node._model;
}
}
return null;
}
@Experimental
void set model(value) {
var changed = model != value;
_model = value;
_hasLocalModel = true;
_ModelTreeObserver.initialize();
if (changed) {
if (_modelChangedStreams != null && !_modelChangedStreams.isEmpty) {
_modelChangedStreams.toList().forEach((stream) => stream.add(this));
}
// Propagate new model to all descendants.
_ModelTreeObserver.propagateModel(this, value, false);
}
}
/**
* Clears the locally set model and makes this model be inherited from parent
* nodes.
*/
@Experimental
void clearModel() {
if (_hasLocalModel == true) {
_hasLocalModel = false;
// Propagate new model to all descendants.
if (parentNode != null) {
_ModelTreeObserver.propagateModel(this, parentNode.model, false);
} else {
_ModelTreeObserver.propagateModel(this, null, false);
}
}
}
/**
* Get a stream of models, whenever the model changes.
*/
Stream<Node> get onModelChanged {
if (_modelChangedStreams == null) {
_modelChangedStreams = new Set<StreamController<Node>>();
}
var controller;
controller = new StreamController(
onListen: () { _modelChangedStreams.add(controller); },
onCancel: () { _modelChangedStreams.remove(controller); });
return controller.stream;
}
/**
* Print out a String representation of this Node.
*/
String toString() => localName == null ?
(nodeValue == null ? super.toString() : nodeValue) : localName;
$!MEMBERS
}