0672317bec
Committed: https://code.google.com/p/dart/source/detail?r=18575 Reverted: http://code.google.com/p/dart/source/detail?r=18576 Review URL: https://codereview.chromium.org//12212213 git-svn-id: https://dart.googlecode.com/svn/branches/bleeding_edge/dart@18579 260f80e4-7a28-3924-810f-c04153c831b5
1118 lines
31 KiB
Plaintext
1118 lines
31 KiB
Plaintext
// 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;
|
|
|
|
// 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<Element>.fixedLength(_childElements.length);
|
|
for (int i = 0, len = _childElements.length; i < len; i++) {
|
|
output[i] = _childElements[i];
|
|
}
|
|
return output;
|
|
}
|
|
|
|
Set<Element> toSet() {
|
|
final output = new Set<Element>();
|
|
for (int i = 0, len = _childElements.length; i < len; i++) {
|
|
output.add(_childElements[i]);
|
|
}
|
|
return output;
|
|
}
|
|
|
|
bool contains(Element element) => _childElements.contains(element);
|
|
|
|
void forEach(void f(Element element)) {
|
|
for (Element element in _childElements) {
|
|
f(element);
|
|
}
|
|
}
|
|
|
|
bool every(bool f(Element element)) {
|
|
for (Element element in this) {
|
|
if (!f(element)) {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool any(bool f(Element element)) {
|
|
for (Element element in this) {
|
|
if (f(element)) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
String join([String separator]) {
|
|
return IterableMixinWorkaround.joinList(this, separator);
|
|
}
|
|
|
|
Iterable map(f(Element element)) {
|
|
return IterableMixinWorkaround.mapList(this, f);
|
|
}
|
|
|
|
Iterable<Element> where(bool f(Element element)) {
|
|
return IterableMixinWorkaround.where(this, f);
|
|
}
|
|
|
|
Iterable expand(Iterable f(Element element)) {
|
|
return IterableMixinWorkaround.expand(this, f);
|
|
}
|
|
|
|
bool get isEmpty {
|
|
return _element.$dom_firstElementChild == null;
|
|
}
|
|
|
|
Iterable<Element> take(int n) {
|
|
return IterableMixinWorkaround.takeList(this, n);
|
|
}
|
|
|
|
Iterable<Element> takeWhile(bool test(Element value)) {
|
|
return IterableMixinWorkaround.takeWhile(this, test);
|
|
}
|
|
|
|
Iterable<Element> skip(int n) {
|
|
return IterableMixinWorkaround.skipList(this, n);
|
|
}
|
|
|
|
Iterable<Element> skipWhile(bool test(Element value)) {
|
|
return IterableMixinWorkaround.skipWhile(this, test);
|
|
}
|
|
|
|
Element firstMatching(bool test(Element value), {Element orElse()}) {
|
|
return IterableMixinWorkaround.firstMatching(this, test, orElse);
|
|
}
|
|
|
|
Element lastMatching(bool test(Element value), {Element orElse()}) {
|
|
return IterableMixinWorkaround.lastMatchingInList(this, test, orElse);
|
|
}
|
|
|
|
Element singleMatching(bool test(Element value)) {
|
|
return IterableMixinWorkaround.singleMatching(this, test);
|
|
}
|
|
|
|
Element elementAt(int index) {
|
|
return this[index];
|
|
}
|
|
|
|
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> get iterator => toList().iterator;
|
|
|
|
void addAll(Iterable<Element> iterable) {
|
|
if (iterable is _ChildNodeListLazy) {
|
|
iterable = new List.from(iterable);
|
|
}
|
|
|
|
for (Element element in iterable) {
|
|
_element.$dom_appendChild(element);
|
|
}
|
|
}
|
|
|
|
Iterable<Element> get reversed {
|
|
return IterableMixinWorkaround.reversedList(this);
|
|
}
|
|
|
|
void sort([int compare(Element a, Element b)]) {
|
|
throw new UnsupportedError('TODO(jacobr): should we impl?');
|
|
}
|
|
|
|
dynamic reduce(dynamic initialValue,
|
|
dynamic combine(dynamic previousValue, Element element)) {
|
|
return IterableMixinWorkaround.reduce(this, initialValue, combine);
|
|
}
|
|
|
|
void setRange(int start, int rangeLength, List from, [int startFrom = 0]) {
|
|
throw new UnimplementedError();
|
|
}
|
|
|
|
void remove(Object object) {
|
|
if (object is Element) {
|
|
Element element = object;
|
|
if (identical(element.parentNode, _element)) {
|
|
_element.$dom_removeChild(element);
|
|
}
|
|
}
|
|
}
|
|
|
|
void removeAll(Iterable elements) {
|
|
IterableMixinWorkaround.removeAll(this, elements);
|
|
}
|
|
|
|
void retainAll(Iterable elements) {
|
|
IterableMixinWorkaround.retainAll(this, elements);
|
|
}
|
|
|
|
void removeMatching(bool test(Element element)) {
|
|
IterableMixinWorkaround.removeMatching(this, test);
|
|
}
|
|
|
|
void retainMatching(bool test(Element element)) {
|
|
IterableMixinWorkaround.retainMatching(this, test);
|
|
}
|
|
|
|
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 removeAt(int index) {
|
|
final result = this[index];
|
|
if (result != null) {
|
|
_element.$dom_removeChild(result);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
Element removeLast() {
|
|
final result = this.last;
|
|
if (result != null) {
|
|
_element.$dom_removeChild(result);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
Element get first {
|
|
Element result = _element.$dom_firstElementChild;
|
|
if (result == null) throw new StateError("No elements");
|
|
return result;
|
|
}
|
|
|
|
|
|
Element get last {
|
|
Element result = _element.$dom_lastElementChild;
|
|
if (result == null) throw new StateError("No elements");
|
|
return result;
|
|
}
|
|
|
|
Element get single {
|
|
if (length > 1) throw new StateError("More than one element");
|
|
return first;
|
|
}
|
|
|
|
Element min([int compare(Element a, Element b)]) {
|
|
return IterableMixinWorkaround.min(this, compare);
|
|
}
|
|
|
|
Element max([int compare(Element a, Element b)]) {
|
|
return IterableMixinWorkaround.max(this, compare);
|
|
}
|
|
}
|
|
|
|
// 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);
|
|
}
|
|
}
|
|
|
|
String join([String separator]) {
|
|
return IterableMixinWorkaround.joinList(this, separator);
|
|
}
|
|
|
|
Iterable map(f(Element element)) {
|
|
return IterableMixinWorkaround.mapList(this, f);
|
|
}
|
|
|
|
Iterable<Element> where(bool f(Element element)) {
|
|
return IterableMixinWorkaround.where(this, f);
|
|
}
|
|
|
|
Iterable expand(Iterable f(Element element)) {
|
|
return IterableMixinWorkaround.expand(this, f);
|
|
}
|
|
|
|
bool every(bool f(Element element)) {
|
|
for(Element element in this) {
|
|
if (!f(element)) {
|
|
return false;
|
|
}
|
|
};
|
|
return true;
|
|
}
|
|
|
|
bool any(bool f(Element element)) {
|
|
for(Element element in this) {
|
|
if (f(element)) {
|
|
return true;
|
|
}
|
|
};
|
|
return false;
|
|
}
|
|
|
|
List<Element> toList() => new List<Element>.from(this);
|
|
Set<Element> toSet() => new Set<Element>.from(this);
|
|
|
|
Iterable<Element> take(int n) {
|
|
return IterableMixinWorkaround.takeList(this, n);
|
|
}
|
|
|
|
Iterable<Element> takeWhile(bool test(Element value)) {
|
|
return IterableMixinWorkaround.takeWhile(this, test);
|
|
}
|
|
|
|
Iterable<Element> skip(int n) {
|
|
return IterableMixinWorkaround.skipList(this, n);
|
|
}
|
|
|
|
Iterable<Element> skipWhile(bool test(Element value)) {
|
|
return IterableMixinWorkaround.skipWhile(this, test);
|
|
}
|
|
|
|
Element firstMatching(bool test(Element value), {Element orElse()}) {
|
|
return IterableMixinWorkaround.firstMatching(this, test, orElse);
|
|
}
|
|
|
|
Element lastMatching(bool test(Element value), {Element orElse()}) {
|
|
return IterableMixinWorkaround.lastMatchingInList(this, test, orElse);
|
|
}
|
|
|
|
Element singleMatching(bool test(Element value)) {
|
|
return IterableMixinWorkaround.singleMatching(this, test);
|
|
}
|
|
|
|
Element elementAt(int index) {
|
|
return this[index];
|
|
}
|
|
|
|
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> get iterator => new _FrozenElementListIterator(this);
|
|
|
|
void addAll(Iterable<Element> iterable) {
|
|
throw new UnsupportedError('');
|
|
}
|
|
|
|
Iterable<Element> get reversed {
|
|
return IterableMixinWorkaround.reversedList(this);
|
|
}
|
|
|
|
void sort([int compare(Element a, Element b)]) {
|
|
throw new UnsupportedError('');
|
|
}
|
|
|
|
dynamic reduce(dynamic initialValue,
|
|
dynamic combine(dynamic previousValue, Element element)) {
|
|
return IterableMixinWorkaround.reduce(this, initialValue, combine);
|
|
}
|
|
|
|
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 removeAt(int index) {
|
|
throw new UnsupportedError('');
|
|
}
|
|
|
|
Element removeLast() {
|
|
throw new UnsupportedError('');
|
|
}
|
|
|
|
void remove(Object element) {
|
|
throw new UnsupportedError('');
|
|
}
|
|
|
|
void removeAll(Iterable elements) {
|
|
throw new UnsupportedError('');
|
|
}
|
|
|
|
void retainAll(Iterable elements) {
|
|
throw new UnsupportedError('');
|
|
}
|
|
|
|
void removeMatching(bool test(Element element)) {
|
|
throw new UnsupportedError('');
|
|
}
|
|
|
|
void retainMatching(bool test(Element element)) {
|
|
throw new UnsupportedError('');
|
|
}
|
|
|
|
Element get first => _nodeList.first;
|
|
|
|
Element get last => _nodeList.last;
|
|
|
|
Element get single => _nodeList.single;
|
|
|
|
Element min([int compare(Element a, Element b)]) {
|
|
return IterableMixinWorkaround.min(this, compare);
|
|
}
|
|
|
|
Element max([int compare(Element a, Element b)]) {
|
|
return IterableMixinWorkaround.max(this, compare);
|
|
}
|
|
}
|
|
|
|
class _FrozenElementListIterator implements Iterator<Element> {
|
|
final _FrozenElementList _list;
|
|
int _index = -1;
|
|
Element _current;
|
|
|
|
_FrozenElementListIterator(this._list);
|
|
|
|
/**
|
|
* Moves to the next element. Returns true if the iterator is positioned
|
|
* at an element. Returns false if it is positioned after the last element.
|
|
*/
|
|
bool moveNext() {
|
|
int nextIndex = _index + 1;
|
|
if (nextIndex < _list.length) {
|
|
_current = _list[nextIndex];
|
|
_index = nextIndex;
|
|
return true;
|
|
}
|
|
_index = _list.length;
|
|
_current = null;
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Returns the element the [Iterator] is positioned at.
|
|
*
|
|
* Return [:null:] if the iterator is positioned before the first, or
|
|
* after the last element.
|
|
*/
|
|
Element get current => _current;
|
|
}
|
|
|
|
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 = s.join(' ');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* An abstract class, which all HTML elements extend.
|
|
*/
|
|
$(ANNOTATIONS)abstract class $CLASSNAME$EXTENDS$IMPLEMENTS$NATIVESPEC {
|
|
|
|
/**
|
|
* Creates an HTML element from a valid fragment of HTML.
|
|
*
|
|
* The [html] fragment must represent valid HTML with a single element root,
|
|
* which will be parsed and returned.
|
|
*
|
|
* Important: the contents of [html] should not contain any user-supplied
|
|
* data. Without strict data validation it is impossible to prevent script
|
|
* injection exploits.
|
|
*
|
|
* It is instead recommended that elements be constructed via [Element.tag]
|
|
* and text be added via [text].
|
|
*
|
|
* var element = new Element.html('<div class="foo">content</div>');
|
|
*/
|
|
factory $CLASSNAME.html(String html) =>
|
|
_$(CLASSNAME)FactoryProvider.createElement_html(html);
|
|
|
|
/**
|
|
* Creates the HTML element specified by the tag name.
|
|
*
|
|
* This is similar to [Document.createElement].
|
|
* [tag] should be a valid HTML tag name. If [tag] is an unknown tag then
|
|
* this will create an [UnknownElement].
|
|
*
|
|
* var divElement = new Element.tag('div');
|
|
* print(divElement is DivElement); // 'true'
|
|
* var myElement = new Element.tag('unknownTag');
|
|
* print(myElement is UnknownElement); // 'true'
|
|
*
|
|
* For standard elements it is more preferable to use the type constructors:
|
|
* var element = new DivElement();
|
|
*
|
|
* See also:
|
|
*
|
|
* * [isTagSupported]
|
|
*/
|
|
factory $CLASSNAME.tag(String tag) =>
|
|
_$(CLASSNAME)FactoryProvider.createElement_tag(tag);
|
|
|
|
/**
|
|
* All attributes on this element.
|
|
*
|
|
* Any modifications to the attribute map will automatically be applied to
|
|
* this element.
|
|
*
|
|
* This only includes attributes which are not in a namespace
|
|
* (such as 'xlink:href'), additional attributes can be accessed via
|
|
* [getNamespacedAttributes].
|
|
*/
|
|
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];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* List of the direct children of this element.
|
|
*
|
|
* This collection can be used to add and remove elements from the document.
|
|
*
|
|
* var item = new DivElement();
|
|
* item.text = 'Something';
|
|
* document.body.children.add(item) // Item is now displayed on the page.
|
|
* for (var element in document.body.children) {
|
|
* element.style.background = 'red'; // Turns every child of body red.
|
|
* }
|
|
*/
|
|
List<Element> get children => new _ChildrenElementList._wrap(this);
|
|
|
|
void set children(List<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);
|
|
}
|
|
|
|
/**
|
|
* Finds the first descendant element of this element that matches the
|
|
* specified group of selectors.
|
|
*
|
|
* [selectors] should be a string using CSS selector syntax.
|
|
*
|
|
* // Gets the first descendant with the class 'classname'
|
|
* var element = element.query('.className');
|
|
* // Gets the element with id 'id'
|
|
* var element = element.query('#id');
|
|
* // Gets the first descendant [ImageElement]
|
|
* var img = element.query('img');
|
|
*
|
|
* See also:
|
|
*
|
|
* * [CSS Selectors](http://docs.webplatform.org/wiki/css/selectors)
|
|
*/
|
|
Element query(String selectors) => $dom_querySelector(selectors);
|
|
|
|
/**
|
|
* Finds all descendent elements of this element that match the specified
|
|
* group of selectors.
|
|
*
|
|
* [selectors] should be a string using CSS selector syntax.
|
|
*
|
|
* var items = element.query('.itemClassName');
|
|
*/
|
|
List<Element> queryAll(String selectors) =>
|
|
new _FrozenElementList._wrap($dom_querySelectorAll(selectors));
|
|
|
|
/**
|
|
* The set of CSS classes applied to this element.
|
|
*
|
|
* This set makes it easy to add, remove or toggle the classes applied to
|
|
* this element.
|
|
*
|
|
* element.classes.add('selected');
|
|
* element.classes.toggle('isOnline');
|
|
* element.classes.remove('selected');
|
|
*/
|
|
CssClassSet get classes => new _ElementCssClassSet(this);
|
|
|
|
void set classes(Collection<String> value) {
|
|
CssClassSet classSet = classes;
|
|
classSet.clear();
|
|
classSet.addAll(value);
|
|
}
|
|
|
|
/**
|
|
* Allows access to all custom data attributes (data-*) set on this element.
|
|
*
|
|
* The keys for the map must follow these rules:
|
|
*
|
|
* * The name must not begin with 'xml'.
|
|
* * The name cannot contain a semi-colon (';').
|
|
* * The name cannot contain any capital letters.
|
|
*
|
|
* Any keys from markup will be converted to camel-cased keys in the map.
|
|
*
|
|
* For example, HTML specified as:
|
|
*
|
|
* <div data-my-random-value='value'></div>
|
|
*
|
|
* Would be accessed in Dart as:
|
|
*
|
|
* var value = element.dataset['myRandomValue'];
|
|
*
|
|
* See also:
|
|
*
|
|
* * [Custom data attributes](http://www.w3.org/TR/html5/global-attributes.html#custom-data-attribute)
|
|
*/
|
|
Map<String, String> get dataset =>
|
|
new _DataAttributeMap(attributes);
|
|
|
|
void set dataset(Map<String, String> value) {
|
|
final data = this.dataset;
|
|
data.clear();
|
|
for (String key in value.keys) {
|
|
data[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);
|
|
}
|
|
|
|
/**
|
|
* The set of all CSS values applied to this element, including inherited
|
|
* and default values.
|
|
*
|
|
* The computedStyle contains values that are inherited from other
|
|
* sources, such as parent elements or stylesheets. This differs from the
|
|
* [style] property, which contains only the values specified directly on this
|
|
* element.
|
|
*
|
|
* PseudoElement can be values such as `::after`, `::before`, `::marker`,
|
|
* `::line-marker`.
|
|
*
|
|
* See also:
|
|
*
|
|
* * [CSS Inheritance and Cascade](http://docs.webplatform.org/wiki/tutorials/inheritance_and_cascade)
|
|
* * [Pseudo-elements](http://docs.webplatform.org/wiki/css/selectors/pseudo-elements)
|
|
*/
|
|
CssStyleDeclaration getComputedStyle([String pseudoElement]) {
|
|
if (pseudoElement == null) {
|
|
pseudoElement = '';
|
|
}
|
|
// TODO(jacobr): last param should be null, see b/5045788
|
|
return window.$dom_getComputedStyle(this, pseudoElement);
|
|
}
|
|
|
|
/**
|
|
* Adds the specified element to after the last child of this element.
|
|
*/
|
|
void append(Element e) {
|
|
this.children.add(e);
|
|
}
|
|
|
|
/**
|
|
* Adds the specified text as a text node after the last child of this
|
|
* element.
|
|
*/
|
|
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 element.
|
|
*/
|
|
void appendHtml(String text) {
|
|
this.insertAdjacentHtml('beforeend', text);
|
|
}
|
|
|
|
/**
|
|
* Checks to see if the tag name is supported by the current platform.
|
|
*
|
|
* The tag should be a valid HTML tag name.
|
|
*/
|
|
static bool isTagSupported(String tag) {
|
|
var e = _ElementFactoryProvider.createElement_tag(tag);
|
|
return e is Element && !(e is UnknownElement);
|
|
}
|
|
|
|
/**
|
|
* Called by the DOM when this element has been instantiated.
|
|
*/
|
|
@Experimental
|
|
void onCreated() {}
|
|
|
|
// 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;
|
|
|
|
/**
|
|
* Scrolls this element into view.
|
|
*
|
|
* Only one of of the alignment options may be specified at a time.
|
|
*
|
|
* If no options are specified then this will attempt to scroll the minimum
|
|
* amount needed to bring the element into view.
|
|
*
|
|
* Note that alignCenter is currently only supported on WebKit platforms. If
|
|
* alignCenter is specified but not supported then this will fall back to
|
|
* alignTop.
|
|
*
|
|
* See also:
|
|
*
|
|
* * [scrollIntoView](http://docs.webplatform.org/wiki/dom/methods/scrollIntoView)
|
|
* * [scrollIntoViewIfNeeded](http://docs.webplatform.org/wiki/dom/methods/scrollIntoViewIfNeeded)
|
|
*/
|
|
void scrollIntoView([ScrollAlignment alignment]) {
|
|
var hasScrollIntoViewIfNeeded = false;
|
|
$if DART2JS
|
|
hasScrollIntoViewIfNeeded =
|
|
JS('bool', '!!(#.scrollIntoViewIfNeeded)', this);
|
|
$endif
|
|
if (alignment == ScrollAlignment.TOP) {
|
|
this.$dom_scrollIntoView(true);
|
|
} else if (alignment == ScrollAlignment.BOTTOM) {
|
|
this.$dom_scrollIntoView(false);
|
|
} else if (hasScrollIntoViewIfNeeded) {
|
|
if (alignment == ScrollAlignment.CENTER) {
|
|
this.$dom_scrollIntoViewIfNeeded(true);
|
|
} else {
|
|
this.$dom_scrollIntoViewIfNeeded();
|
|
}
|
|
} else {
|
|
this.$dom_scrollIntoView();
|
|
}
|
|
}
|
|
|
|
$if DART2JS
|
|
@DomName('Element.mouseWheelEvent')
|
|
static const EventStreamProvider<WheelEvent> mouseWheelEvent =
|
|
const _CustomEventStreamProvider<WheelEvent>(
|
|
Element._determineMouseWheelEventType);
|
|
|
|
static String _determineMouseWheelEventType(EventTarget e) {
|
|
if (JS('bool', '#.onwheel !== undefined', e)) {
|
|
// W3C spec, and should be IE9+, but IE has a bug exposing onwheel.
|
|
return 'wheel';
|
|
} else if (JS('bool', '#.onmousewheel !== undefined', e)) {
|
|
// Chrome & IE
|
|
return 'mousewheel';
|
|
} else {
|
|
// Firefox
|
|
return 'DOMMouseScroll';
|
|
}
|
|
}
|
|
|
|
@DomName('Element.webkitTransitionEndEvent')
|
|
static const EventStreamProvider<TransitionEvent> transitionEndEvent =
|
|
const _CustomEventStreamProvider<TransitionEvent>(
|
|
Element._determineTransitionEventType);
|
|
|
|
static String _determineTransitionEventType(EventTarget e) {
|
|
// Unfortunately the normal 'ontransitionend' style checks don't work here.
|
|
if (_Device.isWebKit) {
|
|
return 'webkitTransitionEnd';
|
|
} else if (_Device.isOpera) {
|
|
return 'oTransitionEnd';
|
|
}
|
|
return 'transitionend';
|
|
}
|
|
/**
|
|
* Creates a text node and inserts it into the DOM at the specified location.
|
|
*
|
|
* To see the possible values for [where], read the doc for
|
|
* [insertAdjacentHtml].
|
|
*
|
|
* See also:
|
|
*
|
|
* * [insertAdjacentHtml]
|
|
*/
|
|
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;
|
|
|
|
/**
|
|
* Parses text as an HTML fragment and inserts it into the DOM at the
|
|
* specified location.
|
|
*
|
|
* The [where] parameter indicates where to insert the HTML fragment:
|
|
*
|
|
* * 'beforeBegin': Immediately before this element.
|
|
* * 'afterBegin': As the first child of this element.
|
|
* * 'beforeEnd': As the last child of this element.
|
|
* * 'afterEnd': Immediately after this element.
|
|
*
|
|
* var html = '<div class="something">content</div>';
|
|
* // Inserts as the first child
|
|
* document.body.insertAdjacentHtml('afterBegin', html);
|
|
* var createdElement = document.body.children[0];
|
|
* print(createdElement.classes[0]); // Prints 'something'
|
|
*
|
|
* See also:
|
|
*
|
|
* * [insertAdjacentText]
|
|
* * [insertAdjacentElement]
|
|
*/
|
|
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;
|
|
|
|
/**
|
|
* Inserts [element] into the DOM at the specified location.
|
|
*
|
|
* To see the possible values for [where], read the doc for
|
|
* [insertAdjacentHtml].
|
|
*
|
|
* See also:
|
|
*
|
|
* * [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.parentNode.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.parentNode.insertBefore(node, this.nextNode);
|
|
break;
|
|
default:
|
|
throw new ArgumentError("Invalid position ${where}");
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Checks if this element matches the CSS selectors.
|
|
*/
|
|
@Experimental
|
|
bool matches(String selectors) {
|
|
if (JS('bool', '!!#.matches', this)) {
|
|
return JS('bool', '#.matches(#)', this, selectors);
|
|
} else if (JS('bool', '!!#.webkitMatchesSelector', this)) {
|
|
return JS('bool', '#.webkitMatchesSelector(#)', this, selectors);
|
|
} else if (JS('bool', '!!#.mozMatchesSelector', this)) {
|
|
return JS('bool', '#.mozMatchesSelector(#)', this, selectors);
|
|
} else if (JS('bool', '!!#.msMatchesSelector', this)) {
|
|
return JS('bool', '#.msMatchesSelector(#)', this, selectors);
|
|
}
|
|
throw new UnsupportedError("Not supported on this platform");
|
|
}
|
|
$else
|
|
$endif
|
|
|
|
$!MEMBERS
|
|
}
|
|
|
|
final _START_TAG_REGEXP = new RegExp('<(\\w+)');
|
|
class _ElementFactoryProvider {
|
|
static const _CUSTOM_PARENT_TAG_MAP = const {
|
|
'body' : 'html',
|
|
'head' : 'html',
|
|
'caption' : 'table',
|
|
'td': 'tr',
|
|
'th': 'tr',
|
|
'colgroup': 'table',
|
|
'col' : 'colgroup',
|
|
'tr' : 'tbody',
|
|
'tbody' : 'table',
|
|
'tfoot' : 'table',
|
|
'thead' : 'table',
|
|
'track' : 'audio',
|
|
};
|
|
|
|
// TODO(jmesserly): const set would be better
|
|
static const _TABLE_TAGS = const {
|
|
'caption': null,
|
|
'col': null,
|
|
'colgroup': null,
|
|
'tbody': null,
|
|
'td': null,
|
|
'tfoot': null,
|
|
'th': null,
|
|
'thead': null,
|
|
'tr': null,
|
|
};
|
|
|
|
@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 (_Device.isIE && _TABLE_TAGS.containsKey(tag)) {
|
|
return _createTableForIE(html, tag);
|
|
}
|
|
parentTag = _CUSTOM_PARENT_TAG_MAP[tag];
|
|
if (parentTag == null) parentTag = 'div';
|
|
}
|
|
|
|
final 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) {
|
|
// In html5 the root <html> tag will always have a <body> and a <head>,
|
|
// even though the inner html only contains one of them.
|
|
element = temp.children[tag == 'head' ? 0 : 1];
|
|
} else {
|
|
_singleNode(temp.children);
|
|
}
|
|
element.remove();
|
|
return element;
|
|
}
|
|
|
|
/**
|
|
* IE table elements don't support innerHTML (even in standards mode).
|
|
* Instead we use a div and inject the table element in the innerHtml string.
|
|
* This technique works on other browsers too, but it's probably slower,
|
|
* so we only use it when running on IE.
|
|
*
|
|
* See also innerHTML:
|
|
* <http://msdn.microsoft.com/en-us/library/ie/ms533897(v=vs.85).aspx>
|
|
* and Building Tables Dynamically:
|
|
* <http://msdn.microsoft.com/en-us/library/ie/ms532998(v=vs.85).aspx>.
|
|
*/
|
|
static Element _createTableForIE(String html, String tag) {
|
|
var div = new Element.tag('div');
|
|
div.innerHtml = '<table>$html</table>';
|
|
var table = _singleNode(div.children);
|
|
Element element;
|
|
switch (tag) {
|
|
case 'td':
|
|
case 'th':
|
|
element = _singleNode(_singleNode(table.rows).cells);
|
|
break;
|
|
case 'tr':
|
|
element = _singleNode(table.rows);
|
|
break;
|
|
case 'tbody':
|
|
element = _singleNode(table.tBodies);
|
|
break;
|
|
case 'thead':
|
|
element = table.tHead;
|
|
break;
|
|
case 'tfoot':
|
|
element = table.tFoot;
|
|
break;
|
|
case 'caption':
|
|
element = table.caption;
|
|
break;
|
|
case 'colgroup':
|
|
element = _getColgroup(table);
|
|
break;
|
|
case 'col':
|
|
element = _singleNode(_getColgroup(table).children);
|
|
break;
|
|
}
|
|
element.remove();
|
|
return element;
|
|
}
|
|
|
|
static TableColElement _getColgroup(TableElement table) {
|
|
// TODO(jmesserly): is there a better way to do this?
|
|
return _singleNode(table.children.where((n) => n.tagName == 'COLGROUP')
|
|
.toList());
|
|
}
|
|
|
|
static Node _singleNode(List<Node> list) {
|
|
if (list.length == 1) return list[0];
|
|
throw new ArgumentError('HTML had ${list.length} '
|
|
'top level elements but 1 expected');
|
|
}
|
|
|
|
@DomName('Document.createElement')
|
|
$if DART2JS
|
|
// Optimization to improve performance until the dart2js compiler inlines this
|
|
// method.
|
|
static dynamic createElement_tag(String tag) =>
|
|
// Firefox may return a JS function for some types (Embed, Object).
|
|
JS('Element|=Object', 'document.createElement(#)', tag);
|
|
$else
|
|
static Element createElement_tag(String tag) =>
|
|
document.$dom_createElement(tag);
|
|
$endif
|
|
}
|
|
|
|
|
|
/**
|
|
* Options for Element.scrollIntoView.
|
|
*/
|
|
class ScrollAlignment {
|
|
final _value;
|
|
const ScrollAlignment._internal(this._value);
|
|
toString() => 'ScrollAlignment.$_value';
|
|
|
|
/// Attempt to align the element to the top of the scrollable area.
|
|
static const TOP = const ScrollAlignment._internal('TOP');
|
|
/// Attempt to center the element in the scrollable area.
|
|
static const CENTER = const ScrollAlignment._internal('CENTER');
|
|
/// Attempt to align the element to the bottom of the scrollable area.
|
|
static const BOTTOM = const ScrollAlignment._internal('BOTTOM');
|
|
}
|