blob: bae3ab0bb9b1a015a7903a5d26e6c8683d14512d [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 html;
// TODO(jacobr): use _Lists.dart to remove some of the duplicated
// functionality.
class _ChildrenElementList implements List {
// Raw Element.
final Element _element;
final HTMLCollection _childElements;
_ChildrenElementList._wrap(Element element)
: _childElements = element.$dom_children,
_element = element;
List<Element> _toList() {
final output = new List(_childElements.length);
for (int i = 0, len = _childElements.length; i < len; i++) {
output[i] = _childElements[i];
}
return output;
}
bool contains(Element element) => _childElements.contains(element);
void forEach(void f(Element element)) {
for (Element element in _childElements) {
f(element);
}
}
List<Element> filter(bool f(Element element)) {
final output = [];
forEach((Element element) {
if (f(element)) {
output.add(element);
}
});
return new _FrozenElementList._wrap(output);
}
bool every(bool f(Element element)) {
for (Element element in this) {
if (!f(element)) {
return false;
}
};
return true;
}
bool some(bool f(Element element)) {
for (Element element in this) {
if (f(element)) {
return true;
}
};
return false;
}
Collection map(f(Element element)) {
final out = [];
for (Element el in this) {
out.add(f(el));
}
return out;
}
bool get isEmpty {
return _element.$dom_firstElementChild == null;
}
int get length {
return _childElements.length;
}
Element operator [](int index) {
return _childElements[index];
}
void operator []=(int index, Element value) {
_element.$dom_replaceChild(value, _childElements[index]);
}
void set length(int newLength) {
// TODO(jacobr): remove children when length is reduced.
throw new UnsupportedError('');
}
Element add(Element value) {
_element.$dom_appendChild(value);
return value;
}
Element addLast(Element value) => add(value);
Iterator<Element> iterator() => _toList().iterator();
void addAll(Collection<Element> collection) {
for (Element element in collection) {
_element.$dom_appendChild(element);
}
}
void sort([Comparator<Element> compare = Comparable.compare]) {
throw new UnsupportedError('TODO(jacobr): should we impl?');
}
void setRange(int start, int rangeLength, List from, [int startFrom = 0]) {
throw new UnimplementedError();
}
void removeRange(int start, int rangeLength) {
throw new UnimplementedError();
}
void insertRange(int start, int rangeLength, [initialValue = null]) {
throw new UnimplementedError();
}
List getRange(int start, int rangeLength) =>
new _FrozenElementList._wrap(_Lists.getRange(this, start, rangeLength,
[]));
int indexOf(Element element, [int start = 0]) {
return _Lists.indexOf(this, element, start, this.length);
}
int lastIndexOf(Element element, [int start = null]) {
if (start == null) start = length - 1;
return _Lists.lastIndexOf(this, element, start);
}
void clear() {
// It is unclear if we want to keep non element nodes?
_element.text = '';
}
Element removeLast() {
final result = this.last;
if (result != null) {
_element.$dom_removeChild(result);
}
return result;
}
Element get first {
return _element.$dom_firstElementChild;
}
Element get last {
return _element.$dom_lastElementChild;
}
}
// TODO(jacobr): this is an inefficient implementation but it is hard to see
// a better option given that we cannot quite force NodeList to be an
// ElementList as there are valid cases where a NodeList JavaScript object
// contains Node objects that are not Elements.
class _FrozenElementList implements List {
final List<Node> _nodeList;
_FrozenElementList._wrap(this._nodeList);
bool contains(Element element) {
for (Element el in this) {
if (el == element) return true;
}
return false;
}
void forEach(void f(Element element)) {
for (Element el in this) {
f(el);
}
}
Collection map(f(Element element)) {
final out = [];
for (Element el in this) {
out.add(f(el));
}
return out;
}
List<Element> filter(bool f(Element element)) {
final out = [];
for (Element el in this) {
if (f(el)) out.add(el);
}
return out;
}
bool every(bool f(Element element)) {
for(Element element in this) {
if (!f(element)) {
return false;
}
};
return true;
}
bool some(bool f(Element element)) {
for(Element element in this) {
if (f(element)) {
return true;
}
};
return false;
}
bool get isEmpty => _nodeList.isEmpty;
int get length => _nodeList.length;
Element operator [](int index) => _nodeList[index];
void operator []=(int index, Element value) {
throw new UnsupportedError('');
}
void set length(int newLength) {
_nodeList.length = newLength;
}
void add(Element value) {
throw new UnsupportedError('');
}
void addLast(Element value) {
throw new UnsupportedError('');
}
Iterator<Element> iterator() => new _FrozenElementListIterator(this);
void addAll(Collection<Element> collection) {
throw new UnsupportedError('');
}
void sort([Comparator<Element> compare = Comparable.compare]) {
throw new UnsupportedError('');
}
void setRange(int start, int rangeLength, List from, [int startFrom = 0]) {
throw new UnsupportedError('');
}
void removeRange(int start, int rangeLength) {
throw new UnsupportedError('');
}
void insertRange(int start, int rangeLength, [initialValue = null]) {
throw new UnsupportedError('');
}
List<Element> getRange(int start, int rangeLength) =>
new _FrozenElementList._wrap(_nodeList.getRange(start, rangeLength));
int indexOf(Element element, [int start = 0]) =>
_nodeList.indexOf(element, start);
int lastIndexOf(Element element, [int start = null]) =>
_nodeList.lastIndexOf(element, start);
void clear() {
throw new UnsupportedError('');
}
Element removeLast() {
throw new UnsupportedError('');
}
Element get first => _nodeList.first;
Element get last => _nodeList.last;
}
class _FrozenElementListIterator implements Iterator<Element> {
final _FrozenElementList _list;
int _index = 0;
_FrozenElementListIterator(this._list);
/**
* Gets the next element in the iteration. Throws a
* [StateError("No more elements")] if no element is left.
*/
Element next() {
if (!hasNext) {
throw new StateError("No more elements");
}
return _list[_index++];
}
/**
* Returns whether the [Iterator] has elements left.
*/
bool get hasNext => _index < _list.length;
}
class _ElementCssClassSet extends CssClassSet {
final Element _element;
_ElementCssClassSet(this._element);
Set<String> readClasses() {
var s = new Set<String>();
var classname = _element.$dom_className;
for (String name in classname.split(' ')) {
String trimmed = name.trim();
if (!trimmed.isEmpty) {
s.add(trimmed);
}
}
return s;
}
void writeClasses(Set<String> s) {
List list = new List.from(s);
_element.$dom_className = Strings.join(list, ' ');
}
}
/// @domName $DOMNAME
abstract class $CLASSNAME$EXTENDS$IMPLEMENTS$NATIVESPEC {
factory $CLASSNAME.html(String html) =>
_$(CLASSNAME)FactoryProvider.createElement_html(html);
factory $CLASSNAME.tag(String tag) =>
_$(CLASSNAME)FactoryProvider.createElement_tag(tag);
/**
* @domName Element.hasAttribute, Element.getAttribute, Element.setAttribute,
* Element.removeAttribute
*/
Map<String, String> get attributes => new _ElementAttributeMap(this);
void set attributes(Map<String, String> value) {
Map<String, String> attributes = this.attributes;
attributes.clear();
for (String key in value.keys) {
attributes[key] = value[key];
}
}
/**
* Deprecated, use innerHtml instead.
*/
String get innerHTML => this.innerHtml;
void set innerHTML(String value) {
this.innerHtml = value;
}
void set elements(Collection<Element> value) {
this.children = value;
}
/**
* Deprecated, use [children] instead.
*/
List<Element> get elements => this.children;
/**
* @domName childElementCount, firstElementChild, lastElementChild,
* children, Node.nodes.add
*/
List<Element> get children => new _ChildrenElementList._wrap(this);
void set children(Collection<Element> value) {
// Copy list first since we don't want liveness during iteration.
List copy = new List.from(value);
var children = this.children;
children.clear();
children.addAll(copy);
}
Element query(String selectors) => $dom_querySelector(selectors);
List<Element> queryAll(String selectors) =>
new _FrozenElementList._wrap($dom_querySelectorAll(selectors));
/** @domName className, classList */
CssClassSet get classes => new _ElementCssClassSet(this);
void set classes(Collection<String> value) {
CssClassSet classSet = classes;
classSet.clear();
classSet.addAll(value);
}
Map<String, String> get dataAttributes =>
new _DataAttributeMap(attributes);
void set dataAttributes(Map<String, String> value) {
final dataAttributes = this.dataAttributes;
dataAttributes.clear();
for (String key in value.keys) {
dataAttributes[key] = value[key];
}
}
/**
* Gets a map for manipulating the attributes of a particular namespace.
* This is primarily useful for SVG attributes such as xref:link.
*/
Map<String, String> getNamespacedAttributes(String namespace) {
return new _NamespacedAttributeMap(this, namespace);
}
/** @domName Window.getComputedStyle */
Future<CSSStyleDeclaration> get computedStyle {
// TODO(jacobr): last param should be null, see b/5045788
return getComputedStyle('');
}
/** @domName Window.getComputedStyle */
Future<CSSStyleDeclaration> getComputedStyle(String pseudoElement) {
return _createMeasurementFuture(
() => window.$dom_getComputedStyle(this, pseudoElement),
new Completer<CSSStyleDeclaration>());
}
/**
* Adds the specified element to after the last child of this.
*/
void append(Element e) {
this.children.add(e);
}
/**
* Adds the specified text as a text node after the last child of this.
*/
void appendText(String text) {
this.insertAdjacentText('beforeend', text);
}
/**
* Parses the specified text as HTML and adds the resulting node after the
* last child of this.
*/
void appendHtml(String text) {
this.insertAdjacentHtml('beforeend', text);
}
// Hooks to support custom WebComponents.
/**
* Experimental support for [web components][wc]. This field stores a
* reference to the component implementation. It was inspired by Mozilla's
* [x-tags][] project. Please note: in the future it may be possible to
* `extend Element` from your class, in which case this field will be
* deprecated and will simply return this [Element] object.
*
* [wc]: http://dvcs.w3.org/hg/webcomponents/raw-file/tip/explainer/index.html
* [x-tags]: http://x-tags.org/
*/
$if DART2JS
@Creates('Null') // Set from Dart code; does not instantiate a native type.
$endif
var xtag;
$if DARTIUM
noSuchMethod(InvocationMirror invocation) {
if (dynamicUnknownElementDispatcher == null) {
throw new NoSuchMethodError(this, invocation.memberName,
invocation.positionalArguments,
invocation.namedArguments);
} else {
String hackedName = invocation.memberName;
if (invocation.isGetter) hackedName = "get:$hackedName";
if (invocation.isSetter) hackedName = "set:$hackedName";
return dynamicUnknownElementDispatcher(this,
hackedName,
invocation.positionalArguments);
}
}
$else
// TODO(vsm): Implement noSuchMethod or similar for dart2js.
$endif
$if DART2JS
/** @domName Element.insertAdjacentText */
void insertAdjacentText(String where, String text) {
if (JS('bool', '!!#.insertAdjacentText', this)) {
_insertAdjacentText(where, text);
} else {
_insertAdjacentNode(where, new Text(text));
}
}
@JSName('insertAdjacentText')
void _insertAdjacentText(String where, String text) native;
/** @domName Element.insertAdjacentHTML */
void insertAdjacentHtml(String where, String text) {
if (JS('bool', '!!#.insertAdjacentHtml', this)) {
_insertAdjacentHtml(where, text);
} else {
_insertAdjacentNode(where, new DocumentFragment.html(text));
}
}
@JSName('insertAdjacentHTML')
void _insertAdjacentHTML(String where, String text) native;
/** @domName Element.insertAdjacentHTML */
Element insertAdjacentElement(String where, Element element) {
if (JS('bool', '!!#.insertAdjacentElement', this)) {
_insertAdjacentElement(where, element);
} else {
_insertAdjacentNode(where, element);
}
return element;
}
@JSName('insertAdjacentElement')
void _insertAdjacentElement(String where, Element element) native;
void _insertAdjacentNode(String where, Node node) {
switch (where.toLowerCase()) {
case 'beforebegin':
this.parent.insertBefore(node, this);
break;
case 'afterbegin':
var first = this.nodes.length > 0 ? this.nodes[0] : null;
this.insertBefore(node, first);
break;
case 'beforeend':
this.nodes.add(node);
break;
case 'afterend':
this.parent.insertBefore(node, this.nextNode);
break;
default:
throw new ArgumentError("Invalid position ${where}");
}
}
$else
$endif
$!MEMBERS
}
// Temporary dispatch hook to support WebComponents.
Function dynamicUnknownElementDispatcher;
final _START_TAG_REGEXP = new RegExp('<(\\w+)');
class _ElementFactoryProvider {
static final _CUSTOM_PARENT_TAG_MAP = const {
'body' : 'html',
'head' : 'html',
'caption' : 'table',
'td': 'tr',
'colgroup': 'table',
'col' : 'colgroup',
'tr' : 'tbody',
'tbody' : 'table',
'tfoot' : 'table',
'thead' : 'table',
'track' : 'audio',
};
/** @domName Document.createElement */
static Element createElement_html(String html) {
// TODO(jacobr): this method can be made more robust and performant.
// 1) Cache the dummy parent elements required to use innerHTML rather than
// creating them every call.
// 2) Verify that the html does not contain leading or trailing text nodes.
// 3) Verify that the html does not contain both <head> and <body> tags.
// 4) Detatch the created element from its dummy parent.
String parentTag = 'div';
String tag;
final match = _START_TAG_REGEXP.firstMatch(html);
if (match != null) {
tag = match.group(1).toLowerCase();
if (_CUSTOM_PARENT_TAG_MAP.containsKey(tag)) {
parentTag = _CUSTOM_PARENT_TAG_MAP[tag];
}
}
final Element temp = new Element.tag(parentTag);
temp.innerHtml = html;
Element element;
if (temp.children.length == 1) {
element = temp.children[0];
} else if (parentTag == 'html' && temp.children.length == 2) {
// Work around for edge case in WebKit and possibly other browsers where
// both body and head elements are created even though the inner html
// only contains a head or body element.
element = temp.children[tag == 'head' ? 0 : 1];
} else {
throw new ArgumentError('HTML had ${temp.children.length} '
'top level elements but 1 expected');
}
element.remove();
return element;
}
/** @domName Document.createElement */
$if DART2JS
// Optimization to improve performance until the dart2js compiler inlines this
// method.
static Element createElement_tag(String tag) =>
JS('Element', 'document.createElement(#)', tag);
$else
static Element createElement_tag(String tag) =>
document.$dom_createElement(tag);
$endif
}