blob: cea6c0530a0b159c0dcac49b644d73f29efdf8c5 [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;
class _ChildrenElementList extends ListBase<Element> {
// Raw Element.
final Element _element;
final HtmlCollection _childElements;
_ChildrenElementList._wrap(Element element)
: _childElements = element.$dom_children,
_element = element;
bool contains(Element element) => _childElements.contains(element);
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('Cannot resize element lists');
}
Element add(Element value) {
_element.append(value);
return 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.append(element);
}
}
void sort([int compare(Element a, Element b)]) {
throw new UnsupportedError('Cannot sort element lists');
}
void removeWhere(bool test(Element element)) {
_filter(test, false);
}
void retainWhere(bool test(Element element)) {
_filter(test, true);
}
void _filter(bool test(var element), bool retainMatching) {
var removed;
if (retainMatching) {
removed = _element.children.where((e) => !test(e));
} else {
removed = _element.children.where(test);
}
for (var e in removed) e.remove();
}
void setRange(int start, int end, Iterable<Element> iterable,
[int skipCount = 0]) {
throw new UnimplementedError();
}
void replaceRange(int start, int end, Iterable<Element> iterable) {
throw new UnimplementedError();
}
void fillRange(int start, int end, [Element fillValue]) {
throw new UnimplementedError();
}
bool remove(Object object) {
if (object is Element) {
Element element = object;
if (identical(element.parentNode, _element)) {
_element.$dom_removeChild(element);
return true;
}
}
return false;
}
void insert(int index, Element element) {
if (index < 0 || index > length) {
throw new RangeError.range(index, 0, length);
}
if (index == length) {
_element.append(element);
} else {
_element.insertBefore(element, this[index]);
}
}
void setAll(int index, Iterable<Element> iterable) {
throw new UnimplementedError();
}
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;
}
}
/**
* An immutable list containing HTML elements. This list contains some
* additional methods for ease of CSS manipulation on a group of elements.
*/
abstract class ElementList<T extends Element> extends ListBase<T> {
/**
* The union of all CSS classes applied to the elements in this list.
*
* This set makes it easy to add, remove or toggle (add if not present, remove
* if present) the classes applied to a collection of elements.
*
* htmlList.classes.add('selected');
* htmlList.classes.toggle('isOnline');
* htmlList.classes.remove('selected');
*/
CssClassSet get classes;
/** Replace the classes with `value` for every element in this list. */
set classes(Iterable<String> value);
}
// 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<T extends Element> extends ListBase<T> implements ElementList {
final List<Node> _nodeList;
_FrozenElementList._wrap(this._nodeList);
int get length => _nodeList.length;
Element operator [](int index) => _nodeList[index];
void operator []=(int index, Element value) {
throw new UnsupportedError('Cannot modify list');
}
void set length(int newLength) {
throw new UnsupportedError('Cannot modify list');
}
void sort([Comparator<Element> compare]) {
throw new UnsupportedError('Cannot sort list');
}
Element get first => _nodeList.first;
Element get last => _nodeList.last;
Element get single => _nodeList.single;
CssClassSet get classes => new _MultiElementCssClassSet(
_nodeList.where((e) => e is Element));
void set classes(Iterable<String> value) {
_nodeList.where((e) => e is Element).forEach((e) => e.classes = value);
}
}
/**
* 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 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');
*/
ElementList 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(Iterable<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);
}
/**
* Gets the position of this element relative to the client area of the page.
*/
Rect get client => new Rect(clientLeft, clientTop, clientWidth, clientHeight);
/**
* Gets the offset of this element relative to its offsetParent.
*/
Rect get offset => new Rect(offsetLeft, offsetTop, offsetWidth, offsetHeight);
/**
* 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.
$if DART2JS
@Creates('Null') // Set from Dart code; does not instantiate a native type.
$endif
Element _xtag;
/**
* 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.
*
* If xtag has not been set, it will simply return `this` [Element].
*
* [wc]: http://dvcs.w3.org/hg/webcomponents/raw-file/tip/explainer/index.html
* [x-tags]: http://x-tags.org/
*/
// Note: return type is `dynamic` for convenience to suppress warnings when
// members of the component are used. The actual type is a subtype of Element.
get xtag => _xtag != null ? _xtag : this;
void set xtag(Element value) {
_xtag = value;
}
@DomName('Element.localName')
@DocsEditable
String get localName => $dom_localName;
@DomName('Element.namespaceUri')
@DocsEditable
String get namespaceUri => $dom_namespaceUri;
String toString() => localName;
/**
* 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.transitionEndEvent')
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 html) {
if (JS('bool', '!!#.insertAdjacentHTML', this)) {
_insertAdjacentHtml(where, html);
} else {
_insertAdjacentNode(where, new DocumentFragment.html(html));
}
}
@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.append(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
$if DART2JS
@Creates('Null') // Set from Dart code; does not instantiate a native type.
$endif
Map<String, StreamSubscription> _attributeBindings;
// TODO(jmesserly): I'm concerned about adding these to every element.
// Conceptually all of these belong on TemplateElement. They are here to
// support browsers that don't have <template> yet.
// However even in the polyfill they're restricted to certain tags
// (see [isTemplate]). So we can probably convert it to a (public) mixin, and
// only mix it in to the elements that need it.
$if DART2JS
@Creates('Null') // Set from Dart code; does not instantiate a native type.
$endif
var _model;
$if DART2JS
@Creates('Null') // Set from Dart code; does not instantiate a native type.
$endif
_TemplateIterator _templateIterator;
$if DART2JS
@Creates('Null') // Set from Dart code; does not instantiate a native type.
$endif
Element _templateInstanceRef;
// Note: only used if `this is! TemplateElement`
$if DART2JS
@Creates('Null') // Set from Dart code; does not instantiate a native type.
$endif
DocumentFragment _templateContent;
bool _templateIsDecorated;
// TODO(jmesserly): should path be optional, and default to empty path?
// It is used that way in at least one path in JS TemplateElement tests
// (see "BindImperative" test in original JS code).
@Experimental
void bind(String name, model, String path) {
_bindElement(this, name, model, path);
}
// TODO(jmesserly): this is static to work around http://dartbug.com/10166
// Similar issue for unbind/unbindAll below.
static void _bindElement(Element self, String name, model, String path) {
if (self._bindTemplate(name, model, path)) return;
if (self._attributeBindings == null) {
self._attributeBindings = new Map<String, StreamSubscription>();
}
self.xtag.attributes.remove(name);
var changed;
if (name.endsWith('?')) {
name = name.substring(0, name.length - 1);
changed = (value) {
if (_Bindings._toBoolean(value)) {
self.xtag.attributes[name] = '';
} else {
self.xtag.attributes.remove(name);
}
};
} else {
changed = (value) {
// TODO(jmesserly): escape value if needed to protect against XSS.
// See https://github.com/polymer-project/mdv/issues/58
self.xtag.attributes[name] = value == null ? '' : '$value';
};
}
self.unbind(name);
self._attributeBindings[name] =
new PathObserver(model, path).bindSync(changed);
}
@Experimental
void unbind(String name) {
_unbindElement(this, name);
}
static _unbindElement(Element self, String name) {
if (self._unbindTemplate(name)) return;
if (self._attributeBindings != null) {
var binding = self._attributeBindings.remove(name);
if (binding != null) binding.cancel();
}
}
@Experimental
void unbindAll() {
_unbindAllElement(this);
}
static void _unbindAllElement(Element self) {
self._unbindAllTemplate();
if (self._attributeBindings != null) {
for (var binding in self._attributeBindings.values) {
binding.cancel();
}
self._attributeBindings = null;
}
}
// TODO(jmesserly): unlike the JS polyfill, we can't mixin
// HTMLTemplateElement at runtime into things that are semantically template
// elements. So instead we implement it here with a runtime check.
// If the bind succeeds, we return true, otherwise we return false and let
// the normal Element.bind logic kick in.
bool _bindTemplate(String name, model, String path) {
if (isTemplate) {
switch (name) {
case 'bind':
case 'repeat':
case 'if':
_ensureTemplate();
if (_templateIterator == null) {
_templateIterator = new _TemplateIterator(this);
}
_templateIterator.inputs.bind(name, model, path);
return true;
}
}
return false;
}
bool _unbindTemplate(String name) {
if (isTemplate) {
switch (name) {
case 'bind':
case 'repeat':
case 'if':
_ensureTemplate();
if (_templateIterator != null) {
_templateIterator.inputs.unbind(name);
}
return true;
}
}
return false;
}
void _unbindAllTemplate() {
if (isTemplate) {
unbind('bind');
unbind('repeat');
unbind('if');
}
}
/**
* Gets the template this node refers to.
* This is only supported if [isTemplate] is true.
*/
@Experimental
Element get ref {
_ensureTemplate();
Element ref = null;
var refId = attributes['ref'];
if (refId != null) {
ref = document.getElementById(refId);
}
return ref != null ? ref : _templateInstanceRef;
}
/**
* Gets the content of this template.
* This is only supported if [isTemplate] is true.
*/
@Experimental
DocumentFragment get content {
_ensureTemplate();
return _templateContent;
}
/**
* Creates an instance of the template.
* This is only supported if [isTemplate] is true.
*/
@Experimental
DocumentFragment createInstance() {
_ensureTemplate();
var template = ref;
if (template == null) template = this;
var instance = _Bindings._createDeepCloneAndDecorateTemplates(
template.content, attributes['syntax']);
if (TemplateElement._instanceCreated != null) {
TemplateElement._instanceCreated.add(instance);
}
return instance;
}
/**
* The data model which is inherited through the tree.
* This is only supported if [isTemplate] is true.
*
* Setting this will destructive propagate the value to all descendant nodes,
* and reinstantiate all of the nodes expanded by this template.
*
* Currently this does not support propagation through Shadow DOMs.
*/
@Experimental
get model => _model;
@Experimental
void set model(value) {
_ensureTemplate();
var syntax = TemplateElement.syntax[attributes['syntax']];
_model = value;
_Bindings._addBindings(this, model, syntax);
}
// 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,
};
bool get _isAttributeTemplate => attributes.containsKey('template') &&
(localName == 'option' || _TABLE_TAGS.containsKey(localName));
/**
* Returns true if this node is a template.
*
* A node is a template if [tagName] is TEMPLATE, or the node has the
* 'template' attribute and this tag supports attribute form for backwards
* compatibility with existing HTML parsers. The nodes that can use attribute
* form are table elments (THEAD, TBODY, TFOOT, TH, TR, TD, CAPTION, COLGROUP
* and COL) and OPTION.
*/
// TODO(jmesserly): this is not a public MDV API, but it seems like a useful
// place to document which tags our polyfill considers to be templates.
// Otherwise I'd be repeating it in several other places.
// See if we can replace this with a TemplateMixin.
@Experimental
bool get isTemplate => tagName == 'TEMPLATE' || _isAttributeTemplate;
void _ensureTemplate() {
if (!isTemplate) {
throw new UnsupportedError('$this is not a template.');
}
TemplateElement.decorate(this);
}
$!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',
};
@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 && Element._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':
TableRowElement row = _singleNode(table.rows);
element = _singleNode(row.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');
}