| // 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 LinkedHashSet<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'); |
| } |