Index: sdk/lib/html/dartium/html_dartium.dart |
diff --git a/sdk/lib/html/dartium/html_dartium.dart b/sdk/lib/html/dartium/html_dartium.dart |
index f5a4b1a199cb20ce0631965f750924e977921399..e09eaec10775be7be698caed47a2bb2ebbf45a10 100644 |
--- a/sdk/lib/html/dartium/html_dartium.dart |
+++ b/sdk/lib/html/dartium/html_dartium.dart |
@@ -7308,21 +7308,18 @@ class Document extends Node |
class DocumentFragment extends Node { |
factory DocumentFragment() => document.createDocumentFragment(); |
- factory DocumentFragment.html(String html) { |
- final fragment = new DocumentFragment(); |
- fragment.innerHtml = html; |
- return fragment; |
+ factory DocumentFragment.html(String html, |
+ {NodeValidator validator, NodeTreeSanitizer treeSanitizer}) { |
Jacob
2013/06/05 23:42:50
it would be interesting to see how much this slows
Jennifer Messerly
2013/06/06 05:55:53
For what it's worth, I think we'll move away from
blois
2013/06/06 16:59:42
In general I think that there will still be a dece
|
+ |
+ return document.body.createFragment(html, |
+ validator: validator, treeSanitizer: treeSanitizer); |
} |
- factory DocumentFragment.svg(String svgContent) { |
- final fragment = new DocumentFragment(); |
- final e = new svg.SvgSvgElement(); |
- e.innerHtml = svgContent; |
+ factory DocumentFragment.svg(String svgContent, |
+ {NodeValidator validator, NodeTreeSanitizer treeSanitizer}) { |
- // Copy list first since we don't want liveness during iteration. |
- final List nodes = new List.from(e.nodes); |
- fragment.nodes.addAll(nodes); |
- return fragment; |
+ return new svg.SvgSvgElement().createFragment(svgContent, |
+ validator: validator, treeSanitizer: treeSanitizer); |
} |
List<Element> _children; |
@@ -7353,17 +7350,16 @@ class DocumentFragment extends Node { |
return e.innerHtml; |
} |
- // TODO(nweiz): Do we want to support some variant of innerHtml for XML and/or |
- // SVG strings? |
void set innerHtml(String value) { |
- this.nodes.clear(); |
+ this.setInnerHtml(value); |
+ } |
- final e = new Element.tag("div"); |
- e.innerHtml = value; |
+ void setInnerHtml(String html, |
+ {NodeValidator validator, NodeTreeSanitizer treeSanitizer}) { |
- // Copy list first since we don't want liveness during iteration. |
- List nodes = new List.from(e.nodes, growable: false); |
- this.nodes.addAll(nodes); |
+ this.nodes.clear(); |
+ append(document.body.createFragment( |
+ html, validator: validator, treeSanitizer: treeSanitizer)); |
} |
/** |
@@ -7931,8 +7927,12 @@ abstract class Element extends Node implements ElementTraversal { |
* |
* var element = new Element.html('<div class="foo">content</div>'); |
*/ |
- factory Element.html(String html) => |
- _ElementFactoryProvider.createElement_html(html); |
+ factory Element.html(String html, |
+ {NodeValidator validator, NodeTreeSanitizer treeSanitizer}) { |
+ var fragment = document.body.createFragment(html, validator: validator, |
+ treeSanitizer: treeSanitizer); |
+ return fragment.nodes.where((e) => e is Element).single; |
+ } |
/** |
* Creates the HTML element specified by the tag name. |
@@ -8451,6 +8451,52 @@ abstract class Element extends Node implements ElementTraversal { |
TemplateElement.decorate(this); |
} |
+ /** |
+ * Create a DocumentFragment from the HTML fragment and ensure that it follows |
+ * the sanitization rules specified by the validator or treeSanitizer. |
+ * |
+ * If the default validation behavior is too restrictive then a new |
+ * NodeValidator should be created, either extending or wrapping a default |
+ * validator and overriding the validation APIs. |
+ * |
+ * The treeSanitizer is used to walk the generated node tree and sanitize it. |
+ * A custom treeSanitizer can also be provided to perform special validation |
+ * rules but since the API is more complex to implement this is discouraged. |
+ * |
+ * The returned tree is guaranteed to only contain nodes and attributes which |
+ * are allowed by the provided validator. |
+ * |
+ * See also: |
+ * |
+ * * [NodeValidator] |
+ * * [NodeTreeSanitizer] |
+ */ |
+ DocumentFragment createFragment(String html, |
+ {NodeValidator validator, NodeTreeSanitizer treeSanitizer}) { |
+ |
+ if (treeSanitizer == null) { |
+ if (validator == null) { |
+ validator = new NodeValidatorBuilder.common(); |
+ } |
+ treeSanitizer = new NodeTreeSanitizer(validator); |
+ } |
+ |
+ return _ElementFactoryProvider._parseHtml(this, html, treeSanitizer); |
+ } |
+ |
+ void set innerHtml(String html) { |
+ this.setInnerHtml(html); |
+ } |
+ |
+ void setInnerHtml(String html, |
+ {NodeValidator validator, NodeTreeSanitizer treeSanitizer}) { |
+ text = null; |
+ append(createFragment( |
+ html, validator: validator, treeSanitizer: treeSanitizer)); |
+ } |
+ |
+ String get innerHtml => deprecatedInnerHtml; |
+ |
Element.internal() : super.internal(); |
@DomName('Element.abortEvent') |
@@ -8680,7 +8726,7 @@ abstract class Element extends Node implements ElementTraversal { |
String id; |
- String innerHtml; |
+ String deprecatedInnerHtml; |
bool get isContentEditable; |
@@ -9244,118 +9290,38 @@ abstract class Element extends Node implements ElementTraversal { |
} |
-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; |
+ static DocumentFragment _parseHtml(Element context, String html, |
+ NodeTreeSanitizer treeSanitizer) { |
- 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]; |
+ var doc = document.implementation.createHtmlDocument(''); |
Jacob
2013/06/05 23:42:50
creating a document every time seems painfully slo
blois
2013/06/06 16:59:42
Hoping not painfully slow, but yes, this should be
|
+ var contextElement; |
+ if (context == null || context is BodyElement) { |
+ contextElement = doc.body; |
} else { |
- _singleNode(temp.children); |
+ contextElement = doc.$dom_createElement(context.tagName); |
+ doc.body.append(contextElement); |
} |
- 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; |
- } |
+ if (Range.supportsCreateContextualFragment) { |
Jacob
2013/06/05 23:42:50
nice... I was just writing a comment on the previo
|
+ var range = doc.$dom_createRange(); |
+ range.selectNodeContents(contextElement); |
+ var fragment = range.createContextualFragment(html); |
- 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()); |
- } |
+ treeSanitizer.sanitizeTree(fragment); |
+ return fragment; |
+ } else { |
+ contextElement.deprecatedInnerHtml = html; |
+ |
+ treeSanitizer.sanitizeTree(contextElement); |
- 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'); |
+ var fragment = new DocumentFragment(); |
+ while (contextElement.$dom_firstChild != null) { |
+ fragment.append(contextElement.$dom_firstChild); |
+ } |
+ return fragment; |
+ } |
} |
@DomName('Document.createElement') |
@@ -22174,6 +22140,16 @@ option[template] { |
*/ |
@Experimental |
static Map<String, CustomBindingSyntax> syntax = {}; |
+ |
+ // An override to place the contents into content rather than as child nodes. |
+ void setInnerHtml(String html, |
+ {NodeValidator validator, NodeTreeSanitizer treeSanitizer}) { |
+ text = null; |
+ var fragment = createFragment( |
+ html, validator: validator, treeSanitizer: treeSanitizer); |
+ |
+ content.append(fragment); |
+ } |
} |
// 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 |
@@ -25868,11 +25844,11 @@ class _Element_Merged extends Element { |
@DomName('HTMLElement.innerHTML') |
@DocsEditable |
- String get innerHtml native "HTMLElement_innerHTML_Getter"; |
+ String get deprecatedInnerHtml native "HTMLElement_innerHTML_Getter"; |
@DomName('HTMLElement.innerHTML') |
@DocsEditable |
- void set innerHtml(String value) native "HTMLElement_innerHTML_Setter"; |
+ void set deprecatedInnerHtml(String value) native "HTMLElement_innerHTML_Setter"; |
@DomName('HTMLElement.isContentEditable') |
@DocsEditable |
@@ -27567,6 +27543,454 @@ class _CustomEventStreamProvider<T extends Event> |
return _eventTypeGetter(target); |
} |
} |
+// DO NOT EDIT- this file is generated from running tool/generator.sh. |
+ |
+// Copyright (c) 2013, 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. |
+ |
+ |
+/** |
+ * A Dart DOM validator generated from Caja whitelists. |
+ * |
+ * This contains a whitelist of known HTML tagNames and attributes and will only |
+ * accept known good values. |
+ * |
+ * See also: |
+ * |
+ * * https://code.google.com/p/google-caja/wiki/CajaWhitelists |
+ */ |
+class _Html5NodeValidator implements NodeValidator { |
+ static final Set<String> _allowedElements = new Set.from([ |
+ 'A', |
+ 'ABBR', |
+ 'ACRONYM', |
+ 'ADDRESS', |
+ 'AREA', |
+ 'ARTICLE', |
+ 'ASIDE', |
+ 'AUDIO', |
+ 'B', |
+ 'BDI', |
+ 'BDO', |
+ 'BIG', |
+ 'BLOCKQUOTE', |
+ 'BR', |
+ 'BUTTON', |
+ 'CANVAS', |
+ 'CAPTION', |
+ 'CENTER', |
+ 'CITE', |
+ 'CODE', |
+ 'COL', |
+ 'COLGROUP', |
+ 'COMMAND', |
+ 'DATA', |
+ 'DATALIST', |
+ 'DD', |
+ 'DEL', |
+ 'DETAILS', |
+ 'DFN', |
+ 'DIR', |
+ 'DIV', |
+ 'DL', |
+ 'DT', |
+ 'EM', |
+ 'FIELDSET', |
+ 'FIGCAPTION', |
+ 'FIGURE', |
+ 'FONT', |
+ 'FOOTER', |
+ 'FORM', |
+ 'H1', |
+ 'H2', |
+ 'H3', |
+ 'H4', |
+ 'H5', |
+ 'H6', |
+ 'HEADER', |
+ 'HGROUP', |
+ 'HR', |
+ 'I', |
+ 'IFRAME', |
+ 'IMG', |
+ 'INPUT', |
+ 'INS', |
+ 'KBD', |
+ 'LABEL', |
+ 'LEGEND', |
+ 'LI', |
+ 'MAP', |
+ 'MARK', |
+ 'MENU', |
+ 'METER', |
+ 'NAV', |
+ 'NOBR', |
+ 'OL', |
+ 'OPTGROUP', |
+ 'OPTION', |
+ 'OUTPUT', |
+ 'P', |
+ 'PRE', |
+ 'PROGRESS', |
+ 'Q', |
+ 'S', |
+ 'SAMP', |
+ 'SECTION', |
+ 'SELECT', |
+ 'SMALL', |
+ 'SOURCE', |
+ 'SPAN', |
+ 'STRIKE', |
+ 'STRONG', |
+ 'SUB', |
+ 'SUMMARY', |
+ 'SUP', |
+ 'TABLE', |
+ 'TBODY', |
+ 'TD', |
+ 'TEXTAREA', |
+ 'TFOOT', |
+ 'TH', |
+ 'THEAD', |
+ 'TIME', |
+ 'TR', |
+ 'TRACK', |
+ 'TT', |
+ 'U', |
+ 'UL', |
+ 'VAR', |
+ 'VIDEO', |
+ 'WBR', |
+ ]); |
+ |
+ static const _standardAttributes = const <String>[ |
+ '*::class', |
+ '*::dir', |
+ '*::draggable', |
+ '*::hidden', |
+ '*::id', |
+ '*::inert', |
+ '*::itemprop', |
+ '*::itemref', |
+ '*::itemscope', |
+ '*::lang', |
+ '*::spellcheck', |
+ '*::title', |
+ '*::translate', |
+ 'A::accesskey', |
+ 'A::coords', |
+ 'A::hreflang', |
+ 'A::name', |
+ 'A::shape', |
+ 'A::tabindex', |
+ 'A::target', |
+ 'A::type', |
+ 'AREA::accesskey', |
+ 'AREA::alt', |
+ 'AREA::coords', |
+ 'AREA::nohref', |
+ 'AREA::shape', |
+ 'AREA::tabindex', |
+ 'AREA::target', |
+ 'AUDIO::controls', |
+ 'AUDIO::loop', |
+ 'AUDIO::mediagroup', |
+ 'AUDIO::muted', |
+ 'AUDIO::preload', |
+ 'BDO::dir', |
+ 'BODY::alink', |
+ 'BODY::bgcolor', |
+ 'BODY::link', |
+ 'BODY::text', |
+ 'BODY::vlink', |
+ 'BR::clear', |
+ 'BUTTON::accesskey', |
+ 'BUTTON::disabled', |
+ 'BUTTON::name', |
+ 'BUTTON::tabindex', |
+ 'BUTTON::type', |
+ 'BUTTON::value', |
+ 'CANVAS::height', |
+ 'CANVAS::width', |
+ 'CAPTION::align', |
+ 'COL::align', |
+ 'COL::char', |
+ 'COL::charoff', |
+ 'COL::span', |
+ 'COL::valign', |
+ 'COL::width', |
+ 'COLGROUP::align', |
+ 'COLGROUP::char', |
+ 'COLGROUP::charoff', |
+ 'COLGROUP::span', |
+ 'COLGROUP::valign', |
+ 'COLGROUP::width', |
+ 'COMMAND::checked', |
+ 'COMMAND::command', |
+ 'COMMAND::disabled', |
+ 'COMMAND::label', |
+ 'COMMAND::radiogroup', |
+ 'COMMAND::type', |
+ 'DATA::value', |
+ 'DEL::datetime', |
+ 'DETAILS::open', |
+ 'DIR::compact', |
+ 'DIV::align', |
+ 'DL::compact', |
+ 'FIELDSET::disabled', |
+ 'FONT::color', |
+ 'FONT::face', |
+ 'FONT::size', |
+ 'FORM::accept', |
+ 'FORM::autocomplete', |
+ 'FORM::enctype', |
+ 'FORM::method', |
+ 'FORM::name', |
+ 'FORM::novalidate', |
+ 'FORM::target', |
+ 'FRAME::name', |
+ 'H1::align', |
+ 'H2::align', |
+ 'H3::align', |
+ 'H4::align', |
+ 'H5::align', |
+ 'H6::align', |
+ 'HR::align', |
+ 'HR::noshade', |
+ 'HR::size', |
+ 'HR::width', |
+ 'HTML::version', |
+ 'IFRAME::align', |
+ 'IFRAME::frameborder', |
+ 'IFRAME::height', |
+ 'IFRAME::marginheight', |
+ 'IFRAME::marginwidth', |
+ 'IFRAME::width', |
+ 'IMG::align', |
+ 'IMG::alt', |
+ 'IMG::border', |
+ 'IMG::height', |
+ 'IMG::hspace', |
+ 'IMG::ismap', |
+ 'IMG::name', |
+ 'IMG::usemap', |
+ 'IMG::vspace', |
+ 'IMG::width', |
+ 'INPUT::accept', |
+ 'INPUT::accesskey', |
+ 'INPUT::align', |
+ 'INPUT::alt', |
+ 'INPUT::autocomplete', |
+ 'INPUT::checked', |
+ 'INPUT::disabled', |
+ 'INPUT::inputmode', |
+ 'INPUT::ismap', |
+ 'INPUT::list', |
+ 'INPUT::max', |
+ 'INPUT::maxlength', |
+ 'INPUT::min', |
+ 'INPUT::multiple', |
+ 'INPUT::name', |
+ 'INPUT::placeholder', |
+ 'INPUT::readonly', |
+ 'INPUT::required', |
+ 'INPUT::size', |
+ 'INPUT::step', |
+ 'INPUT::tabindex', |
+ 'INPUT::type', |
+ 'INPUT::usemap', |
+ 'INPUT::value', |
+ 'INS::datetime', |
+ 'KEYGEN::disabled', |
+ 'KEYGEN::keytype', |
+ 'KEYGEN::name', |
+ 'LABEL::accesskey', |
+ 'LABEL::for', |
+ 'LEGEND::accesskey', |
+ 'LEGEND::align', |
+ 'LI::type', |
+ 'LI::value', |
+ 'LINK::sizes', |
+ 'MAP::name', |
+ 'MENU::compact', |
+ 'MENU::label', |
+ 'MENU::type', |
+ 'METER::high', |
+ 'METER::low', |
+ 'METER::max', |
+ 'METER::min', |
+ 'METER::value', |
+ 'OBJECT::typemustmatch', |
+ 'OL::compact', |
+ 'OL::reversed', |
+ 'OL::start', |
+ 'OL::type', |
+ 'OPTGROUP::disabled', |
+ 'OPTGROUP::label', |
+ 'OPTION::disabled', |
+ 'OPTION::label', |
+ 'OPTION::selected', |
+ 'OPTION::value', |
+ 'OUTPUT::for', |
+ 'OUTPUT::name', |
+ 'P::align', |
+ 'PRE::width', |
+ 'PROGRESS::max', |
+ 'PROGRESS::min', |
+ 'PROGRESS::value', |
+ 'SELECT::autocomplete', |
+ 'SELECT::disabled', |
+ 'SELECT::multiple', |
+ 'SELECT::name', |
+ 'SELECT::required', |
+ 'SELECT::size', |
+ 'SELECT::tabindex', |
+ 'SOURCE::type', |
+ 'TABLE::align', |
+ 'TABLE::bgcolor', |
+ 'TABLE::border', |
+ 'TABLE::cellpadding', |
+ 'TABLE::cellspacing', |
+ 'TABLE::frame', |
+ 'TABLE::rules', |
+ 'TABLE::summary', |
+ 'TABLE::width', |
+ 'TBODY::align', |
+ 'TBODY::char', |
+ 'TBODY::charoff', |
+ 'TBODY::valign', |
+ 'TD::abbr', |
+ 'TD::align', |
+ 'TD::axis', |
+ 'TD::bgcolor', |
+ 'TD::char', |
+ 'TD::charoff', |
+ 'TD::colspan', |
+ 'TD::headers', |
+ 'TD::height', |
+ 'TD::nowrap', |
+ 'TD::rowspan', |
+ 'TD::scope', |
+ 'TD::valign', |
+ 'TD::width', |
+ 'TEXTAREA::accesskey', |
+ 'TEXTAREA::autocomplete', |
+ 'TEXTAREA::cols', |
+ 'TEXTAREA::disabled', |
+ 'TEXTAREA::inputmode', |
+ 'TEXTAREA::name', |
+ 'TEXTAREA::placeholder', |
+ 'TEXTAREA::readonly', |
+ 'TEXTAREA::required', |
+ 'TEXTAREA::rows', |
+ 'TEXTAREA::tabindex', |
+ 'TEXTAREA::wrap', |
+ 'TFOOT::align', |
+ 'TFOOT::char', |
+ 'TFOOT::charoff', |
+ 'TFOOT::valign', |
+ 'TH::abbr', |
+ 'TH::align', |
+ 'TH::axis', |
+ 'TH::bgcolor', |
+ 'TH::char', |
+ 'TH::charoff', |
+ 'TH::colspan', |
+ 'TH::headers', |
+ 'TH::height', |
+ 'TH::nowrap', |
+ 'TH::rowspan', |
+ 'TH::scope', |
+ 'TH::valign', |
+ 'TH::width', |
+ 'THEAD::align', |
+ 'THEAD::char', |
+ 'THEAD::charoff', |
+ 'THEAD::valign', |
+ 'TR::align', |
+ 'TR::bgcolor', |
+ 'TR::char', |
+ 'TR::charoff', |
+ 'TR::valign', |
+ 'TRACK::default', |
+ 'TRACK::kind', |
+ 'TRACK::label', |
+ 'TRACK::srclang', |
+ 'UL::compact', |
+ 'UL::type', |
+ 'VIDEO::controls', |
+ 'VIDEO::height', |
+ 'VIDEO::loop', |
+ 'VIDEO::mediagroup', |
+ 'VIDEO::muted', |
+ 'VIDEO::preload', |
+ 'VIDEO::width', |
+ ]; |
+ |
+ static const _uriAttributes = const <String>[ |
+ 'A::href', |
+ 'AREA::href', |
+ 'BLOCKQUOTE::cite', |
+ 'BODY::background', |
+ 'COMMAND::icon', |
+ 'DEL::cite', |
+ 'FORM::action', |
+ 'IMG::src', |
+ 'INPUT::src', |
+ 'INS::cite', |
+ 'Q::cite', |
+ 'VIDEO::poster', |
+ ]; |
+ |
+ final UriPolicy uriPolicy; |
+ |
+ static final Map<String, Function> _attributeValidators = {}; |
+ |
+ /** |
+ * All known URI attributes will be validated against the UriPolicy, if |
+ * [uriPolicy] is null then a default UriPolicy will be used. |
+ */ |
+ _Html5NodeValidator({UriPolicy uriPolicy}): |
+ this.uriPolicy = uriPolicy != null ? uriPolicy : new UriPolicy() { |
+ |
+ if (_attributeValidators.isEmpty) { |
+ for (var attr in _standardAttributes) { |
+ _attributeValidators[attr] = _standardAttributeValidator; |
+ } |
+ |
+ for (var attr in _uriAttributes) { |
+ _attributeValidators[attr] = _uriAttributeValidator; |
+ } |
+ } |
+ } |
+ |
+ bool allowsElement(Element element) { |
+ return _allowedElements.contains(element.tagName); |
+ } |
+ |
+ bool allowsAttribute(Element element, String attributeName, String value) { |
+ var tagName = element.tagName; |
+ var validator = _attributeValidators['$tagName::$attributeName']; |
+ if (validator == null) { |
+ validator = _attributeValidators['*::$attributeName']; |
+ } |
+ if (validator == null) { |
+ return false; |
+ } |
+ return validator(element, attributeName, value, this); |
+ } |
+ |
+ static bool _standardAttributeValidator(Element element, String attributeName, |
+ String value, _Html5NodeValidator context) { |
+ return true; |
+ } |
+ |
+ static bool _uriAttributeValidator(Element element, String attributeName, |
+ String value, _Html5NodeValidator context) { |
+ return context.uriPolicy.allowsUri(value); |
+ } |
+} |
// 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. |
@@ -27648,404 +28072,222 @@ abstract class ImmutableListMixin<E> implements List<E> { |
// BSD-style license that can be found in the LICENSE file. |
-/** |
- * Internal class that does the actual calculations to determine keyCode and |
- * charCode for keydown, keypress, and keyup events for all browsers. |
- */ |
-class _KeyboardEventHandler extends EventStreamProvider<KeyEvent> { |
- // This code inspired by Closure's KeyHandling library. |
- // http://closure-library.googlecode.com/svn/docs/closure_goog_events_keyhandler.js.source.html |
+_serialize(var message) { |
+ return new _JsSerializer().traverse(message); |
+} |
- /** |
- * The set of keys that have been pressed down without seeing their |
- * corresponding keyup event. |
- */ |
- final List<KeyboardEvent> _keyDownList = <KeyboardEvent>[]; |
+class _JsSerializer extends _Serializer { |
- /** The type of KeyEvent we are tracking (keyup, keydown, keypress). */ |
- final String _type; |
+ visitSendPortSync(SendPortSync x) { |
+ if (x is _JsSendPortSync) return visitJsSendPortSync(x); |
+ if (x is _LocalSendPortSync) return visitLocalSendPortSync(x); |
+ if (x is _RemoteSendPortSync) return visitRemoteSendPortSync(x); |
+ throw "Unknown port type $x"; |
+ } |
- /** The element we are watching for events to happen on. */ |
- final EventTarget _target; |
+ visitJsSendPortSync(_JsSendPortSync x) { |
+ return [ 'sendport', 'nativejs', x._id ]; |
+ } |
- // The distance to shift from upper case alphabet Roman letters to lower case. |
- static final int _ROMAN_ALPHABET_OFFSET = "a".codeUnits[0] - "A".codeUnits[0]; |
+ visitLocalSendPortSync(_LocalSendPortSync x) { |
+ return [ 'sendport', 'dart', |
+ ReceivePortSync._isolateId, x._receivePort._portId ]; |
+ } |
- /** Controller to produce KeyEvents for the stream. */ |
- final StreamController _controller = new StreamController(sync: true); |
+ visitSendPort(SendPort x) { |
+ throw new UnimplementedError('Asynchronous send port not yet implemented.'); |
+ } |
- static const _EVENT_TYPE = 'KeyEvent'; |
+ visitRemoteSendPortSync(_RemoteSendPortSync x) { |
+ return [ 'sendport', 'dart', x._isolateId, x._portId ]; |
+ } |
+} |
- /** |
- * An enumeration of key identifiers currently part of the W3C draft for DOM3 |
- * and their mappings to keyCodes. |
- * http://www.w3.org/TR/DOM-Level-3-Events/keyset.html#KeySet-Set |
- */ |
- static const Map<String, int> _keyIdentifier = const { |
- 'Up': KeyCode.UP, |
- 'Down': KeyCode.DOWN, |
- 'Left': KeyCode.LEFT, |
- 'Right': KeyCode.RIGHT, |
- 'Enter': KeyCode.ENTER, |
- 'F1': KeyCode.F1, |
- 'F2': KeyCode.F2, |
- 'F3': KeyCode.F3, |
- 'F4': KeyCode.F4, |
- 'F5': KeyCode.F5, |
- 'F6': KeyCode.F6, |
- 'F7': KeyCode.F7, |
- 'F8': KeyCode.F8, |
- 'F9': KeyCode.F9, |
- 'F10': KeyCode.F10, |
- 'F11': KeyCode.F11, |
- 'F12': KeyCode.F12, |
- 'U+007F': KeyCode.DELETE, |
- 'Home': KeyCode.HOME, |
- 'End': KeyCode.END, |
- 'PageUp': KeyCode.PAGE_UP, |
- 'PageDown': KeyCode.PAGE_DOWN, |
- 'Insert': KeyCode.INSERT |
- }; |
+_deserialize(var message) { |
+ return new _JsDeserializer().deserialize(message); |
+} |
- /** Return a stream for KeyEvents for the specified target. */ |
- Stream<KeyEvent> forTarget(EventTarget e, {bool useCapture: false}) { |
- return new _KeyboardEventHandler.initializeAllEventListeners( |
- _type, e).stream; |
- } |
- /** |
- * Accessor to the stream associated with a particular KeyboardEvent |
- * EventTarget. |
- * |
- * [forTarget] must be called to initialize this stream to listen to a |
- * particular EventTarget. |
- */ |
- Stream<KeyEvent> get stream { |
- if(_target != null) { |
- return _controller.stream; |
- } else { |
- throw new StateError("Not initialized. Call forTarget to access a stream " |
- "initialized with a particular EventTarget."); |
+class _JsDeserializer extends _Deserializer { |
+ |
+ static const _UNSPECIFIED = const Object(); |
+ |
+ deserializeSendPort(List x) { |
+ String tag = x[1]; |
+ switch (tag) { |
+ case 'nativejs': |
+ num id = x[2]; |
+ return new _JsSendPortSync(id); |
+ case 'dart': |
+ num isolateId = x[2]; |
+ num portId = x[3]; |
+ return ReceivePortSync._lookup(isolateId, portId); |
+ default: |
+ throw 'Illegal SendPortSync type: $tag'; |
} |
} |
+} |
- /** |
- * General constructor, performs basic initialization for our improved |
- * KeyboardEvent controller. |
- */ |
- _KeyboardEventHandler(this._type) : |
- _target = null, super(_EVENT_TYPE) { |
- } |
+// The receiver is JS. |
+class _JsSendPortSync implements SendPortSync { |
- /** |
- * Hook up all event listeners under the covers so we can estimate keycodes |
- * and charcodes when they are not provided. |
- */ |
- _KeyboardEventHandler.initializeAllEventListeners(this._type, this._target) : |
- super(_EVENT_TYPE) { |
- Element.keyDownEvent.forTarget(_target, useCapture: true).listen( |
- processKeyDown); |
- Element.keyPressEvent.forTarget(_target, useCapture: true).listen( |
- processKeyPress); |
- Element.keyUpEvent.forTarget(_target, useCapture: true).listen( |
- processKeyUp); |
+ final num _id; |
+ _JsSendPortSync(this._id); |
+ |
+ callSync(var message) { |
+ var serialized = _serialize(message); |
+ var result = _callPortSync(_id, serialized); |
+ return _deserialize(result); |
} |
- /** |
- * Notify all callback listeners that a KeyEvent of the relevant type has |
- * occurred. |
- */ |
- bool _dispatch(KeyEvent event) { |
- if (event.type == _type) |
- _controller.add(event); |
+ bool operator==(var other) { |
+ return (other is _JsSendPortSync) && (_id == other._id); |
} |
- /** Determine if caps lock is one of the currently depressed keys. */ |
- bool get _capsLockOn => |
- _keyDownList.any((var element) => element.keyCode == KeyCode.CAPS_LOCK); |
+ int get hashCode => _id; |
+} |
- /** |
- * Given the previously recorded keydown key codes, see if we can determine |
- * the keycode of this keypress [event]. (Generally browsers only provide |
- * charCode information for keypress events, but with a little |
- * reverse-engineering, we can also determine the keyCode.) Returns |
- * KeyCode.UNKNOWN if the keycode could not be determined. |
- */ |
- int _determineKeyCodeForKeypress(KeyboardEvent event) { |
- // Note: This function is a work in progress. We'll expand this function |
- // once we get more information about other keyboards. |
- for (var prevEvent in _keyDownList) { |
- if (prevEvent._shadowCharCode == event.charCode) { |
- return prevEvent.keyCode; |
- } |
- if ((event.shiftKey || _capsLockOn) && event.charCode >= "A".codeUnits[0] |
- && event.charCode <= "Z".codeUnits[0] && event.charCode + |
- _ROMAN_ALPHABET_OFFSET == prevEvent._shadowCharCode) { |
- return prevEvent.keyCode; |
- } |
- } |
- return KeyCode.UNKNOWN; |
+// TODO(vsm): Differentiate between Dart2Js and Dartium isolates. |
+// The receiver is a different Dart isolate, compiled to JS. |
+class _RemoteSendPortSync implements SendPortSync { |
+ |
+ int _isolateId; |
+ int _portId; |
+ _RemoteSendPortSync(this._isolateId, this._portId); |
+ |
+ callSync(var message) { |
+ var serialized = _serialize(message); |
+ var result = _call(_isolateId, _portId, serialized); |
+ return _deserialize(result); |
} |
- /** |
- * Given the charater code returned from a keyDown [event], try to ascertain |
- * and return the corresponding charCode for the character that was pressed. |
- * This information is not shown to the user, but used to help polyfill |
- * keypress events. |
- */ |
- int _findCharCodeKeyDown(KeyboardEvent event) { |
- if (event.keyLocation == 3) { // Numpad keys. |
- switch (event.keyCode) { |
- case KeyCode.NUM_ZERO: |
- // Even though this function returns _charCodes_, for some cases the |
- // KeyCode == the charCode we want, in which case we use the keycode |
- // constant for readability. |
- return KeyCode.ZERO; |
- case KeyCode.NUM_ONE: |
- return KeyCode.ONE; |
- case KeyCode.NUM_TWO: |
- return KeyCode.TWO; |
- case KeyCode.NUM_THREE: |
- return KeyCode.THREE; |
- case KeyCode.NUM_FOUR: |
- return KeyCode.FOUR; |
- case KeyCode.NUM_FIVE: |
- return KeyCode.FIVE; |
- case KeyCode.NUM_SIX: |
- return KeyCode.SIX; |
- case KeyCode.NUM_SEVEN: |
- return KeyCode.SEVEN; |
- case KeyCode.NUM_EIGHT: |
- return KeyCode.EIGHT; |
- case KeyCode.NUM_NINE: |
- return KeyCode.NINE; |
- case KeyCode.NUM_MULTIPLY: |
- return 42; // Char code for * |
- case KeyCode.NUM_PLUS: |
- return 43; // + |
- case KeyCode.NUM_MINUS: |
- return 45; // - |
- case KeyCode.NUM_PERIOD: |
- return 46; // . |
- case KeyCode.NUM_DIVISION: |
- return 47; // / |
- } |
- } else if (event.keyCode >= 65 && event.keyCode <= 90) { |
- // Set the "char code" for key down as the lower case letter. Again, this |
- // will not show up for the user, but will be helpful in estimating |
- // keyCode locations and other information during the keyPress event. |
- return event.keyCode + _ROMAN_ALPHABET_OFFSET; |
- } |
- switch(event.keyCode) { |
- case KeyCode.SEMICOLON: |
- return KeyCode.FF_SEMICOLON; |
- case KeyCode.EQUALS: |
- return KeyCode.FF_EQUALS; |
- case KeyCode.COMMA: |
- return 44; // Ascii value for , |
- case KeyCode.DASH: |
- return 45; // - |
- case KeyCode.PERIOD: |
- return 46; // . |
- case KeyCode.SLASH: |
- return 47; // / |
- case KeyCode.APOSTROPHE: |
- return 96; // ` |
- case KeyCode.OPEN_SQUARE_BRACKET: |
- return 91; // [ |
- case KeyCode.BACKSLASH: |
- return 92; // \ |
- case KeyCode.CLOSE_SQUARE_BRACKET: |
- return 93; // ] |
- case KeyCode.SINGLE_QUOTE: |
- return 39; // ' |
- } |
- return event.keyCode; |
+ static _call(int isolateId, int portId, var message) { |
+ var target = 'dart-port-$isolateId-$portId'; |
+ // TODO(vsm): Make this re-entrant. |
+ // TODO(vsm): Set this up set once, on the first call. |
+ var source = '$target-result'; |
+ var result = null; |
+ window.on[source].first.then((Event e) { |
+ result = json.parse(_getPortSyncEventData(e)); |
+ }); |
+ _dispatchEvent(target, [source, message]); |
+ return result; |
} |
- /** |
- * Returns true if the key fires a keypress event in the current browser. |
- */ |
- bool _firesKeyPressEvent(KeyEvent event) { |
- if (!Device.isIE && !Device.isWebKit) { |
- return true; |
- } |
+ bool operator==(var other) { |
+ return (other is _RemoteSendPortSync) && (_isolateId == other._isolateId) |
+ && (_portId == other._portId); |
+ } |
- if (Device.userAgent.contains('Mac') && event.altKey) { |
- return KeyCode.isCharacterKey(event.keyCode); |
- } |
+ int get hashCode => _isolateId >> 16 + _portId; |
+} |
- // Alt but not AltGr which is represented as Alt+Ctrl. |
- if (event.altKey && !event.ctrlKey) { |
- return false; |
- } |
+// The receiver is in the same Dart isolate, compiled to JS. |
+class _LocalSendPortSync implements SendPortSync { |
- // Saves Ctrl or Alt + key for IE and WebKit, which won't fire keypress. |
- if (!event.shiftKey && |
- (_keyDownList.last.keyCode == KeyCode.CTRL || |
- _keyDownList.last.keyCode == KeyCode.ALT || |
- Device.userAgent.contains('Mac') && |
- _keyDownList.last.keyCode == KeyCode.META)) { |
- return false; |
- } |
+ ReceivePortSync _receivePort; |
- // Some keys with Ctrl/Shift do not issue keypress in WebKit. |
- if (Device.isWebKit && event.ctrlKey && event.shiftKey && ( |
- event.keyCode == KeyCode.BACKSLASH || |
- event.keyCode == KeyCode.OPEN_SQUARE_BRACKET || |
- event.keyCode == KeyCode.CLOSE_SQUARE_BRACKET || |
- event.keyCode == KeyCode.TILDE || |
- event.keyCode == KeyCode.SEMICOLON || event.keyCode == KeyCode.DASH || |
- event.keyCode == KeyCode.EQUALS || event.keyCode == KeyCode.COMMA || |
- event.keyCode == KeyCode.PERIOD || event.keyCode == KeyCode.SLASH || |
- event.keyCode == KeyCode.APOSTROPHE || |
- event.keyCode == KeyCode.SINGLE_QUOTE)) { |
- return false; |
- } |
+ _LocalSendPortSync._internal(this._receivePort); |
- switch (event.keyCode) { |
- case KeyCode.ENTER: |
- // IE9 does not fire keypress on ENTER. |
- return !Device.isIE; |
- case KeyCode.ESC: |
- return !Device.isWebKit; |
- } |
+ callSync(var message) { |
+ // TODO(vsm): Do a more efficient deep copy. |
+ var copy = _deserialize(_serialize(message)); |
+ var result = _receivePort._callback(copy); |
+ return _deserialize(_serialize(result)); |
+ } |
- return KeyCode.isCharacterKey(event.keyCode); |
+ bool operator==(var other) { |
+ return (other is _LocalSendPortSync) |
+ && (_receivePort == other._receivePort); |
} |
- /** |
- * Normalize the keycodes to the IE KeyCodes (this is what Chrome, IE, and |
- * Opera all use). |
- */ |
- int _normalizeKeyCodes(KeyboardEvent event) { |
- // Note: This may change once we get input about non-US keyboards. |
- if (Device.isFirefox) { |
- switch(event.keyCode) { |
- case KeyCode.FF_EQUALS: |
- return KeyCode.EQUALS; |
- case KeyCode.FF_SEMICOLON: |
- return KeyCode.SEMICOLON; |
- case KeyCode.MAC_FF_META: |
- return KeyCode.META; |
- case KeyCode.WIN_KEY_FF_LINUX: |
- return KeyCode.WIN_KEY; |
- } |
+ int get hashCode => _receivePort.hashCode; |
+} |
+ |
+// TODO(vsm): Move this to dart:isolate. This will take some |
+// refactoring as there are dependences here on the DOM. Users |
+// interact with this class (or interface if we change it) directly - |
+// new ReceivePortSync. I think most of the DOM logic could be |
+// delayed until the corresponding SendPort is registered on the |
+// window. |
+ |
+// A Dart ReceivePortSync (tagged 'dart' when serialized) is |
+// identifiable / resolvable by the combination of its isolateid and |
+// portid. When a corresponding SendPort is used within the same |
+// isolate, the _portMap below can be used to obtain the |
+// ReceivePortSync directly. Across isolates (or from JS), an |
+// EventListener can be used to communicate with the port indirectly. |
+class ReceivePortSync { |
+ |
+ static Map<int, ReceivePortSync> _portMap; |
+ static int _portIdCount; |
+ static int _cachedIsolateId; |
+ |
+ num _portId; |
+ Function _callback; |
+ StreamSubscription _portSubscription; |
+ |
+ ReceivePortSync() { |
+ if (_portIdCount == null) { |
+ _portIdCount = 0; |
+ _portMap = new Map<int, ReceivePortSync>(); |
} |
- return event.keyCode; |
+ _portId = _portIdCount++; |
+ _portMap[_portId] = this; |
} |
- /** Handle keydown events. */ |
- void processKeyDown(KeyboardEvent e) { |
- // Ctrl-Tab and Alt-Tab can cause the focus to be moved to another window |
- // before we've caught a key-up event. If the last-key was one of these |
- // we reset the state. |
- if (_keyDownList.length > 0 && |
- (_keyDownList.last.keyCode == KeyCode.CTRL && !e.ctrlKey || |
- _keyDownList.last.keyCode == KeyCode.ALT && !e.altKey || |
- Device.userAgent.contains('Mac') && |
- _keyDownList.last.keyCode == KeyCode.META && !e.metaKey)) { |
- _keyDownList.clear(); |
+ static int get _isolateId { |
+ // TODO(vsm): Make this coherent with existing isolate code. |
+ if (_cachedIsolateId == null) { |
+ _cachedIsolateId = _getNewIsolateId(); |
} |
+ return _cachedIsolateId; |
+ } |
- var event = new KeyEvent(e); |
- event._shadowKeyCode = _normalizeKeyCodes(event); |
- // Technically a "keydown" event doesn't have a charCode. This is |
- // calculated nonetheless to provide us with more information in giving |
- // as much information as possible on keypress about keycode and also |
- // charCode. |
- event._shadowCharCode = _findCharCodeKeyDown(event); |
- if (_keyDownList.length > 0 && event.keyCode != _keyDownList.last.keyCode && |
- !_firesKeyPressEvent(event)) { |
- // Some browsers have quirks not firing keypress events where all other |
- // browsers do. This makes them more consistent. |
- processKeyPress(event); |
+ static String _getListenerName(isolateId, portId) => |
+ 'dart-port-$isolateId-$portId'; |
+ String get _listenerName => _getListenerName(_isolateId, _portId); |
+ |
+ void receive(callback(var message)) { |
+ _callback = callback; |
+ if (_portSubscription == null) { |
+ _portSubscription = window.on[_listenerName].listen((Event e) { |
+ var data = json.parse(_getPortSyncEventData(e)); |
+ var replyTo = data[0]; |
+ var message = _deserialize(data[1]); |
+ var result = _callback(message); |
+ _dispatchEvent(replyTo, _serialize(result)); |
+ }); |
} |
- _keyDownList.add(event); |
- _dispatch(event); |
} |
- /** Handle keypress events. */ |
- void processKeyPress(KeyboardEvent event) { |
- var e = new KeyEvent(event); |
- // IE reports the character code in the keyCode field for keypress events. |
- // There are two exceptions however, Enter and Escape. |
- if (Device.isIE) { |
- if (e.keyCode == KeyCode.ENTER || e.keyCode == KeyCode.ESC) { |
- e._shadowCharCode = 0; |
- } else { |
- e._shadowCharCode = e.keyCode; |
- } |
- } else if (Device.isOpera) { |
- // Opera reports the character code in the keyCode field. |
- e._shadowCharCode = KeyCode.isCharacterKey(e.keyCode) ? e.keyCode : 0; |
- } |
- // Now we guestimate about what the keycode is that was actually |
- // pressed, given previous keydown information. |
- e._shadowKeyCode = _determineKeyCodeForKeypress(e); |
+ void close() { |
+ _portMap.remove(_portId); |
+ if (_portSubscription != null) _portSubscription.cancel(); |
+ } |
- // Correct the key value for certain browser-specific quirks. |
- if (e._shadowKeyIdentifier != null && |
- _keyIdentifier.containsKey(e._shadowKeyIdentifier)) { |
- // This is needed for Safari Windows because it currently doesn't give a |
- // keyCode/which for non printable keys. |
- e._shadowKeyCode = _keyIdentifier[e._shadowKeyIdentifier]; |
- } |
- e._shadowAltKey = _keyDownList.any((var element) => element.altKey); |
- _dispatch(e); |
+ SendPortSync toSendPort() { |
+ return new _LocalSendPortSync._internal(this); |
} |
- /** Handle keyup events. */ |
- void processKeyUp(KeyboardEvent event) { |
- var e = new KeyEvent(event); |
- KeyboardEvent toRemove = null; |
- for (var key in _keyDownList) { |
- if (key.keyCode == e.keyCode) { |
- toRemove = key; |
- } |
- } |
- if (toRemove != null) { |
- _keyDownList.removeWhere((element) => element == toRemove); |
- } else if (_keyDownList.length > 0) { |
- // This happens when we've reached some international keyboard case we |
- // haven't accounted for or we haven't correctly eliminated all browser |
- // inconsistencies. Filing bugs on when this is reached is welcome! |
- _keyDownList.removeLast(); |
+ static SendPortSync _lookup(int isolateId, int portId) { |
+ if (isolateId == _isolateId) { |
+ return _portMap[portId].toSendPort(); |
+ } else { |
+ return new _RemoteSendPortSync(isolateId, portId); |
} |
- _dispatch(e); |
} |
} |
+get _isolateId => ReceivePortSync._isolateId; |
-/** |
- * Records KeyboardEvents that occur on a particular element, and provides a |
- * stream of outgoing KeyEvents with cross-browser consistent keyCode and |
- * charCode values despite the fact that a multitude of browsers that have |
- * varying keyboard default behavior. |
- * |
- * Example usage: |
- * |
- * KeyboardEventStream.onKeyDown(document.body).listen( |
- * keydownHandlerTest); |
- * |
- * This class is very much a work in progress, and we'd love to get information |
- * on how we can make this class work with as many international keyboards as |
- * possible. Bugs welcome! |
- */ |
-class KeyboardEventStream { |
- |
- /** Named constructor to produce a stream for onKeyPress events. */ |
- static Stream<KeyEvent> onKeyPress(EventTarget target) => |
- new _KeyboardEventHandler('keypress').forTarget(target); |
- |
- /** Named constructor to produce a stream for onKeyUp events. */ |
- static Stream<KeyEvent> onKeyUp(EventTarget target) => |
- new _KeyboardEventHandler('keyup').forTarget(target); |
- |
- /** Named constructor to produce a stream for onKeyDown events. */ |
- static Stream<KeyEvent> onKeyDown(EventTarget target) => |
- new _KeyboardEventHandler('keydown').forTarget(target); |
+void _dispatchEvent(String receiver, var message) { |
+ var event = new CustomEvent(receiver, canBubble: false, cancelable:false, |
+ detail: json.stringify(message)); |
+ window.dispatchEvent(event); |
} |
+ |
+String _getPortSyncEventData(CustomEvent event) => event.detail; |
// 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. |
@@ -28787,2163 +29029,2816 @@ abstract class KeyName { |
/** The Combining Cedilla (Dead Cedilla) key */ |
static const String DEAD_CEDILLA = "DeadCedilla"; |
- /** The Combining Ogonek (Nasal Hook, Dead Ogonek) key */ |
- static const String DEAD_OGONEK = "DeadOgonek"; |
+ /** The Combining Ogonek (Nasal Hook, Dead Ogonek) key */ |
+ static const String DEAD_OGONEK = "DeadOgonek"; |
+ |
+ /** |
+ * The Combining Greek Ypogegrammeni (Greek Non-Spacing Iota Below, Iota |
+ * Subscript, Dead Iota) key |
+ */ |
+ static const String DEAD_IOTA = "DeadIota"; |
+ |
+ /** |
+ * The Combining Katakana-Hiragana Voiced Sound Mark (Dead Voiced Sound) key |
+ */ |
+ static const String DEAD_VOICED_SOUND = "DeadVoicedSound"; |
+ |
+ /** |
+ * The Combining Katakana-Hiragana Semi-Voiced Sound Mark (Dead Semivoiced |
+ * Sound) key |
+ */ |
+ static const String DEC_SEMIVOICED_SOUND= "DeadSemivoicedSound"; |
+ |
+ /** |
+ * Key value used when an implementation is unable to identify another key |
+ * value, due to either hardware, platform, or software constraints |
+ */ |
+ static const String UNIDENTIFIED = "Unidentified"; |
+} |
+// 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. |
+ |
+ |
+/** |
+ * Internal class that does the actual calculations to determine keyCode and |
+ * charCode for keydown, keypress, and keyup events for all browsers. |
+ */ |
+class _KeyboardEventHandler extends EventStreamProvider<KeyEvent> { |
+ // This code inspired by Closure's KeyHandling library. |
+ // http://closure-library.googlecode.com/svn/docs/closure_goog_events_keyhandler.js.source.html |
+ |
+ /** |
+ * The set of keys that have been pressed down without seeing their |
+ * corresponding keyup event. |
+ */ |
+ final List<KeyboardEvent> _keyDownList = <KeyboardEvent>[]; |
+ |
+ /** The type of KeyEvent we are tracking (keyup, keydown, keypress). */ |
+ final String _type; |
+ |
+ /** The element we are watching for events to happen on. */ |
+ final EventTarget _target; |
+ |
+ // The distance to shift from upper case alphabet Roman letters to lower case. |
+ static final int _ROMAN_ALPHABET_OFFSET = "a".codeUnits[0] - "A".codeUnits[0]; |
+ |
+ /** Controller to produce KeyEvents for the stream. */ |
+ final StreamController _controller = new StreamController(sync: true); |
+ |
+ static const _EVENT_TYPE = 'KeyEvent'; |
+ |
+ /** |
+ * An enumeration of key identifiers currently part of the W3C draft for DOM3 |
+ * and their mappings to keyCodes. |
+ * http://www.w3.org/TR/DOM-Level-3-Events/keyset.html#KeySet-Set |
+ */ |
+ static const Map<String, int> _keyIdentifier = const { |
+ 'Up': KeyCode.UP, |
+ 'Down': KeyCode.DOWN, |
+ 'Left': KeyCode.LEFT, |
+ 'Right': KeyCode.RIGHT, |
+ 'Enter': KeyCode.ENTER, |
+ 'F1': KeyCode.F1, |
+ 'F2': KeyCode.F2, |
+ 'F3': KeyCode.F3, |
+ 'F4': KeyCode.F4, |
+ 'F5': KeyCode.F5, |
+ 'F6': KeyCode.F6, |
+ 'F7': KeyCode.F7, |
+ 'F8': KeyCode.F8, |
+ 'F9': KeyCode.F9, |
+ 'F10': KeyCode.F10, |
+ 'F11': KeyCode.F11, |
+ 'F12': KeyCode.F12, |
+ 'U+007F': KeyCode.DELETE, |
+ 'Home': KeyCode.HOME, |
+ 'End': KeyCode.END, |
+ 'PageUp': KeyCode.PAGE_UP, |
+ 'PageDown': KeyCode.PAGE_DOWN, |
+ 'Insert': KeyCode.INSERT |
+ }; |
+ |
+ /** Return a stream for KeyEvents for the specified target. */ |
+ Stream<KeyEvent> forTarget(EventTarget e, {bool useCapture: false}) { |
+ return new _KeyboardEventHandler.initializeAllEventListeners( |
+ _type, e).stream; |
+ } |
/** |
- * The Combining Greek Ypogegrammeni (Greek Non-Spacing Iota Below, Iota |
- * Subscript, Dead Iota) key |
+ * Accessor to the stream associated with a particular KeyboardEvent |
+ * EventTarget. |
+ * |
+ * [forTarget] must be called to initialize this stream to listen to a |
+ * particular EventTarget. |
*/ |
- static const String DEAD_IOTA = "DeadIota"; |
+ Stream<KeyEvent> get stream { |
+ if(_target != null) { |
+ return _controller.stream; |
+ } else { |
+ throw new StateError("Not initialized. Call forTarget to access a stream " |
+ "initialized with a particular EventTarget."); |
+ } |
+ } |
/** |
- * The Combining Katakana-Hiragana Voiced Sound Mark (Dead Voiced Sound) key |
+ * General constructor, performs basic initialization for our improved |
+ * KeyboardEvent controller. |
*/ |
- static const String DEAD_VOICED_SOUND = "DeadVoicedSound"; |
+ _KeyboardEventHandler(this._type) : |
+ _target = null, super(_EVENT_TYPE) { |
+ } |
/** |
- * The Combining Katakana-Hiragana Semi-Voiced Sound Mark (Dead Semivoiced |
- * Sound) key |
+ * Hook up all event listeners under the covers so we can estimate keycodes |
+ * and charcodes when they are not provided. |
*/ |
- static const String DEC_SEMIVOICED_SOUND= "DeadSemivoicedSound"; |
+ _KeyboardEventHandler.initializeAllEventListeners(this._type, this._target) : |
+ super(_EVENT_TYPE) { |
+ Element.keyDownEvent.forTarget(_target, useCapture: true).listen( |
+ processKeyDown); |
+ Element.keyPressEvent.forTarget(_target, useCapture: true).listen( |
+ processKeyPress); |
+ Element.keyUpEvent.forTarget(_target, useCapture: true).listen( |
+ processKeyUp); |
+ } |
/** |
- * Key value used when an implementation is unable to identify another key |
- * value, due to either hardware, platform, or software constraints |
+ * Notify all callback listeners that a KeyEvent of the relevant type has |
+ * occurred. |
*/ |
- static const String UNIDENTIFIED = "Unidentified"; |
-} |
-// Copyright (c) 2013, 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. |
- |
- |
-// This code is inspired by ChangeSummary: |
-// https://github.com/rafaelw/ChangeSummary/blob/master/change_summary.js |
-// ...which underlies MDV. Since we don't need the functionality of |
-// ChangeSummary, we just implement what we need for data bindings. |
-// This allows our implementation to be much simpler. |
- |
-// TODO(jmesserly): should we make these types stronger, and require |
-// Observable objects? Currently, it is fine to say something like: |
-// var path = new PathObserver(123, ''); |
-// print(path.value); // "123" |
-// |
-// Furthermore this degenerate case is allowed: |
-// var path = new PathObserver(123, 'foo.bar.baz.qux'); |
-// print(path.value); // "null" |
-// |
-// Here we see that any invalid (i.e. not Observable) value will break the |
-// path chain without producing an error or exception. |
-// |
-// Now the real question: should we do this? For the former case, the behavior |
-// is correct but we could chose to handle it in the dart:html bindings layer. |
-// For the latter case, it might be better to throw an error so users can find |
-// the problem. |
- |
- |
-/** |
- * A data-bound path starting from a view-model or model object, for example |
- * `foo.bar.baz`. |
- * |
- * When the [values] stream is being listened to, this will observe changes to |
- * the object and any intermediate object along the path, and send [values] |
- * accordingly. When all listeners are unregistered it will stop observing |
- * the objects. |
- * |
- * This class is used to implement [Node.bind] and similar functionality. |
- */ |
-// TODO(jmesserly): find a better home for this type. |
-@Experimental |
-class PathObserver { |
- /** The object being observed. */ |
- final object; |
- |
- /** The path string. */ |
- final String path; |
- |
- /** True if the path is valid, otherwise false. */ |
- final bool _isValid; |
- |
- // TODO(jmesserly): same issue here as ObservableMixin: is there an easier |
- // way to get a broadcast stream? |
- StreamController _values; |
- Stream _valueStream; |
- |
- _PropertyObserver _observer, _lastObserver; |
+ bool _dispatch(KeyEvent event) { |
+ if (event.type == _type) |
+ _controller.add(event); |
+ } |
- Object _lastValue; |
- bool _scheduled = false; |
+ /** Determine if caps lock is one of the currently depressed keys. */ |
+ bool get _capsLockOn => |
+ _keyDownList.any((var element) => element.keyCode == KeyCode.CAPS_LOCK); |
/** |
- * Observes [path] on [object] for changes. This returns an object that can be |
- * used to get the changes and get/set the value at this path. |
- * See [PathObserver.values] and [PathObserver.value]. |
+ * Given the previously recorded keydown key codes, see if we can determine |
+ * the keycode of this keypress [event]. (Generally browsers only provide |
+ * charCode information for keypress events, but with a little |
+ * reverse-engineering, we can also determine the keyCode.) Returns |
+ * KeyCode.UNKNOWN if the keycode could not be determined. |
*/ |
- PathObserver(this.object, String path) |
- : path = path, _isValid = _isPathValid(path) { |
- |
- // TODO(jmesserly): if the path is empty, or the object is! Observable, we |
- // can optimize the PathObserver to be more lightweight. |
- |
- _values = new StreamController.broadcast(sync: true, |
- onListen: _observe, |
- onCancel: _unobserve); |
- |
- if (_isValid) { |
- var segments = []; |
- for (var segment in path.trim().split('.')) { |
- if (segment == '') continue; |
- var index = int.parse(segment, onError: (_) {}); |
- segments.add(index != null ? index : new Symbol(segment)); |
+ int _determineKeyCodeForKeypress(KeyboardEvent event) { |
+ // Note: This function is a work in progress. We'll expand this function |
+ // once we get more information about other keyboards. |
+ for (var prevEvent in _keyDownList) { |
+ if (prevEvent._shadowCharCode == event.charCode) { |
+ return prevEvent.keyCode; |
+ } |
+ if ((event.shiftKey || _capsLockOn) && event.charCode >= "A".codeUnits[0] |
+ && event.charCode <= "Z".codeUnits[0] && event.charCode + |
+ _ROMAN_ALPHABET_OFFSET == prevEvent._shadowCharCode) { |
+ return prevEvent.keyCode; |
} |
+ } |
+ return KeyCode.UNKNOWN; |
+ } |
- // Create the property observer linked list. |
- // Note that the structure of a path can't change after it is initially |
- // constructed, even though the objects along the path can change. |
- for (int i = segments.length - 1; i >= 0; i--) { |
- _observer = new _PropertyObserver(this, segments[i], _observer); |
- if (_lastObserver == null) _lastObserver = _observer; |
+ /** |
+ * Given the charater code returned from a keyDown [event], try to ascertain |
+ * and return the corresponding charCode for the character that was pressed. |
+ * This information is not shown to the user, but used to help polyfill |
+ * keypress events. |
+ */ |
+ int _findCharCodeKeyDown(KeyboardEvent event) { |
+ if (event.keyLocation == 3) { // Numpad keys. |
+ switch (event.keyCode) { |
+ case KeyCode.NUM_ZERO: |
+ // Even though this function returns _charCodes_, for some cases the |
+ // KeyCode == the charCode we want, in which case we use the keycode |
+ // constant for readability. |
+ return KeyCode.ZERO; |
+ case KeyCode.NUM_ONE: |
+ return KeyCode.ONE; |
+ case KeyCode.NUM_TWO: |
+ return KeyCode.TWO; |
+ case KeyCode.NUM_THREE: |
+ return KeyCode.THREE; |
+ case KeyCode.NUM_FOUR: |
+ return KeyCode.FOUR; |
+ case KeyCode.NUM_FIVE: |
+ return KeyCode.FIVE; |
+ case KeyCode.NUM_SIX: |
+ return KeyCode.SIX; |
+ case KeyCode.NUM_SEVEN: |
+ return KeyCode.SEVEN; |
+ case KeyCode.NUM_EIGHT: |
+ return KeyCode.EIGHT; |
+ case KeyCode.NUM_NINE: |
+ return KeyCode.NINE; |
+ case KeyCode.NUM_MULTIPLY: |
+ return 42; // Char code for * |
+ case KeyCode.NUM_PLUS: |
+ return 43; // + |
+ case KeyCode.NUM_MINUS: |
+ return 45; // - |
+ case KeyCode.NUM_PERIOD: |
+ return 46; // . |
+ case KeyCode.NUM_DIVISION: |
+ return 47; // / |
} |
+ } else if (event.keyCode >= 65 && event.keyCode <= 90) { |
+ // Set the "char code" for key down as the lower case letter. Again, this |
+ // will not show up for the user, but will be helpful in estimating |
+ // keyCode locations and other information during the keyPress event. |
+ return event.keyCode + _ROMAN_ALPHABET_OFFSET; |
+ } |
+ switch(event.keyCode) { |
+ case KeyCode.SEMICOLON: |
+ return KeyCode.FF_SEMICOLON; |
+ case KeyCode.EQUALS: |
+ return KeyCode.FF_EQUALS; |
+ case KeyCode.COMMA: |
+ return 44; // Ascii value for , |
+ case KeyCode.DASH: |
+ return 45; // - |
+ case KeyCode.PERIOD: |
+ return 46; // . |
+ case KeyCode.SLASH: |
+ return 47; // / |
+ case KeyCode.APOSTROPHE: |
+ return 96; // ` |
+ case KeyCode.OPEN_SQUARE_BRACKET: |
+ return 91; // [ |
+ case KeyCode.BACKSLASH: |
+ return 92; // \ |
+ case KeyCode.CLOSE_SQUARE_BRACKET: |
+ return 93; // ] |
+ case KeyCode.SINGLE_QUOTE: |
+ return 39; // ' |
} |
+ return event.keyCode; |
} |
- // TODO(jmesserly): we could try adding the first value to the stream, but |
- // that delivers the first record async. |
- /** |
- * Listens to the stream, and invokes the [callback] immediately with the |
- * current [value]. This is useful for bindings, which want to be up-to-date |
- * immediately. |
- */ |
- StreamSubscription bindSync(void callback(value)) { |
- var result = values.listen(callback); |
- callback(value); |
- return result; |
- } |
- |
- // TODO(jmesserly): should this be a change record with the old value? |
- // TODO(jmesserly): should this be a broadcast stream? We only need |
- // single-subscription in the bindings system, so single sub saves overhead. |
/** |
- * Gets the stream of values that were observed at this path. |
- * This returns a single-subscription stream. |
+ * Returns true if the key fires a keypress event in the current browser. |
*/ |
- Stream get values => _values.stream; |
- |
- /** Force synchronous delivery of [values]. */ |
- void _deliverValues() { |
- _scheduled = false; |
- |
- var newValue = value; |
- if (!identical(_lastValue, newValue)) { |
- _values.add(newValue); |
- _lastValue = newValue; |
+ bool _firesKeyPressEvent(KeyEvent event) { |
+ if (!Device.isIE && !Device.isWebKit) { |
+ return true; |
} |
- } |
- void _observe() { |
- if (_observer != null) { |
- _lastValue = value; |
- _observer.observe(); |
+ if (Device.userAgent.contains('Mac') && event.altKey) { |
+ return KeyCode.isCharacterKey(event.keyCode); |
} |
- } |
- |
- void _unobserve() { |
- if (_observer != null) _observer.unobserve(); |
- } |
- |
- void _notifyChange() { |
- if (_scheduled) return; |
- _scheduled = true; |
- |
- // TODO(jmesserly): should we have a guarenteed order with respect to other |
- // paths? If so, we could implement this fairly easily by sorting instances |
- // of this class by birth order before delivery. |
- queueChangeRecords(_deliverValues); |
- } |
- |
- /** Gets the last reported value at this path. */ |
- get value { |
- if (!_isValid) return null; |
- if (_observer == null) return object; |
- _observer.ensureValue(object); |
- return _lastObserver.value; |
- } |
- /** Sets the value at this path. */ |
- void set value(Object value) { |
- // TODO(jmesserly): throw if property cannot be set? |
- // MDV seems tolerant of these error. |
- if (_observer == null || !_isValid) return; |
- _observer.ensureValue(object); |
- var last = _lastObserver; |
- if (_setObjectProperty(last._object, last._property, value)) { |
- // Technically, this would get updated asynchronously via a change record. |
- // However, it is nice if calling the getter will yield the same value |
- // that was just set. So we use this opportunity to update our cache. |
- last.value = value; |
+ // Alt but not AltGr which is represented as Alt+Ctrl. |
+ if (event.altKey && !event.ctrlKey) { |
+ return false; |
} |
- } |
-} |
-// TODO(jmesserly): these should go away in favor of mirrors! |
-_getObjectProperty(object, property) { |
- if (object is List && property is int) { |
- if (property >= 0 && property < object.length) { |
- return object[property]; |
- } else { |
- return null; |
+ // Saves Ctrl or Alt + key for IE and WebKit, which won't fire keypress. |
+ if (!event.shiftKey && |
+ (_keyDownList.last.keyCode == KeyCode.CTRL || |
+ _keyDownList.last.keyCode == KeyCode.ALT || |
+ Device.userAgent.contains('Mac') && |
+ _keyDownList.last.keyCode == KeyCode.META)) { |
+ return false; |
} |
- } |
- |
- // TODO(jmesserly): what about length? |
- if (object is Map) return object[property]; |
- if (object is Observable) return object.getValueWorkaround(property); |
+ // Some keys with Ctrl/Shift do not issue keypress in WebKit. |
+ if (Device.isWebKit && event.ctrlKey && event.shiftKey && ( |
+ event.keyCode == KeyCode.BACKSLASH || |
+ event.keyCode == KeyCode.OPEN_SQUARE_BRACKET || |
+ event.keyCode == KeyCode.CLOSE_SQUARE_BRACKET || |
+ event.keyCode == KeyCode.TILDE || |
+ event.keyCode == KeyCode.SEMICOLON || event.keyCode == KeyCode.DASH || |
+ event.keyCode == KeyCode.EQUALS || event.keyCode == KeyCode.COMMA || |
+ event.keyCode == KeyCode.PERIOD || event.keyCode == KeyCode.SLASH || |
+ event.keyCode == KeyCode.APOSTROPHE || |
+ event.keyCode == KeyCode.SINGLE_QUOTE)) { |
+ return false; |
+ } |
- return null; |
-} |
+ switch (event.keyCode) { |
+ case KeyCode.ENTER: |
+ // IE9 does not fire keypress on ENTER. |
+ return !Device.isIE; |
+ case KeyCode.ESC: |
+ return !Device.isWebKit; |
+ } |
-bool _setObjectProperty(object, property, value) { |
- if (object is List && property is int) { |
- object[property] = value; |
- } else if (object is Map) { |
- object[property] = value; |
- } else if (object is Observable) { |
- (object as Observable).setValueWorkaround(property, value); |
- } else { |
- return false; |
+ return KeyCode.isCharacterKey(event.keyCode); |
} |
- return true; |
-} |
- |
- |
-class _PropertyObserver { |
- final PathObserver _path; |
- final _property; |
- final _PropertyObserver _next; |
- |
- // TODO(jmesserly): would be nice not to store both of these. |
- Object _object; |
- Object _value; |
- StreamSubscription _sub; |
- |
- _PropertyObserver(this._path, this._property, this._next); |
- |
- get value => _value; |
- void set value(Object newValue) { |
- _value = newValue; |
- if (_next != null) { |
- if (_sub != null) _next.unobserve(); |
- _next.ensureValue(_value); |
- if (_sub != null) _next.observe(); |
+ /** |
+ * Normalize the keycodes to the IE KeyCodes (this is what Chrome, IE, and |
+ * Opera all use). |
+ */ |
+ int _normalizeKeyCodes(KeyboardEvent event) { |
+ // Note: This may change once we get input about non-US keyboards. |
+ if (Device.isFirefox) { |
+ switch(event.keyCode) { |
+ case KeyCode.FF_EQUALS: |
+ return KeyCode.EQUALS; |
+ case KeyCode.FF_SEMICOLON: |
+ return KeyCode.SEMICOLON; |
+ case KeyCode.MAC_FF_META: |
+ return KeyCode.META; |
+ case KeyCode.WIN_KEY_FF_LINUX: |
+ return KeyCode.WIN_KEY; |
+ } |
} |
+ return event.keyCode; |
} |
- void ensureValue(object) { |
- // If we're observing, values should be up to date already. |
- if (_sub != null) return; |
- |
- _object = object; |
- value = _getObjectProperty(object, _property); |
- } |
+ /** Handle keydown events. */ |
+ void processKeyDown(KeyboardEvent e) { |
+ // Ctrl-Tab and Alt-Tab can cause the focus to be moved to another window |
+ // before we've caught a key-up event. If the last-key was one of these |
+ // we reset the state. |
+ if (_keyDownList.length > 0 && |
+ (_keyDownList.last.keyCode == KeyCode.CTRL && !e.ctrlKey || |
+ _keyDownList.last.keyCode == KeyCode.ALT && !e.altKey || |
+ Device.userAgent.contains('Mac') && |
+ _keyDownList.last.keyCode == KeyCode.META && !e.metaKey)) { |
+ _keyDownList.clear(); |
+ } |
- void observe() { |
- if (_object is Observable) { |
- assert(_sub == null); |
- _sub = (_object as Observable).changes.listen(_onChange); |
+ var event = new KeyEvent(e); |
+ event._shadowKeyCode = _normalizeKeyCodes(event); |
+ // Technically a "keydown" event doesn't have a charCode. This is |
+ // calculated nonetheless to provide us with more information in giving |
+ // as much information as possible on keypress about keycode and also |
+ // charCode. |
+ event._shadowCharCode = _findCharCodeKeyDown(event); |
+ if (_keyDownList.length > 0 && event.keyCode != _keyDownList.last.keyCode && |
+ !_firesKeyPressEvent(event)) { |
+ // Some browsers have quirks not firing keypress events where all other |
+ // browsers do. This makes them more consistent. |
+ processKeyPress(event); |
} |
- if (_next != null) _next.observe(); |
+ _keyDownList.add(event); |
+ _dispatch(event); |
} |
- void unobserve() { |
- if (_sub == null) return; |
- |
- _sub.cancel(); |
- _sub = null; |
- if (_next != null) _next.unobserve(); |
- } |
+ /** Handle keypress events. */ |
+ void processKeyPress(KeyboardEvent event) { |
+ var e = new KeyEvent(event); |
+ // IE reports the character code in the keyCode field for keypress events. |
+ // There are two exceptions however, Enter and Escape. |
+ if (Device.isIE) { |
+ if (e.keyCode == KeyCode.ENTER || e.keyCode == KeyCode.ESC) { |
+ e._shadowCharCode = 0; |
+ } else { |
+ e._shadowCharCode = e.keyCode; |
+ } |
+ } else if (Device.isOpera) { |
+ // Opera reports the character code in the keyCode field. |
+ e._shadowCharCode = KeyCode.isCharacterKey(e.keyCode) ? e.keyCode : 0; |
+ } |
+ // Now we guestimate about what the keycode is that was actually |
+ // pressed, given previous keydown information. |
+ e._shadowKeyCode = _determineKeyCodeForKeypress(e); |
- void _onChange(List<ChangeRecord> changes) { |
- for (var change in changes) { |
- // TODO(jmesserly): what to do about "new Symbol" here? |
- // Ideally this would only preserve names if the user has opted in to |
- // them being preserved. |
- // TODO(jmesserly): should we drop observable maps with String keys? |
- // If so then we only need one check here. |
- if (change.changes(_property)) { |
- value = _getObjectProperty(_object, _property); |
- _path._notifyChange(); |
- return; |
+ // Correct the key value for certain browser-specific quirks. |
+ if (e._shadowKeyIdentifier != null && |
+ _keyIdentifier.containsKey(e._shadowKeyIdentifier)) { |
+ // This is needed for Safari Windows because it currently doesn't give a |
+ // keyCode/which for non printable keys. |
+ e._shadowKeyCode = _keyIdentifier[e._shadowKeyIdentifier]; |
+ } |
+ e._shadowAltKey = _keyDownList.any((var element) => element.altKey); |
+ _dispatch(e); |
+ } |
+ |
+ /** Handle keyup events. */ |
+ void processKeyUp(KeyboardEvent event) { |
+ var e = new KeyEvent(event); |
+ KeyboardEvent toRemove = null; |
+ for (var key in _keyDownList) { |
+ if (key.keyCode == e.keyCode) { |
+ toRemove = key; |
} |
} |
+ if (toRemove != null) { |
+ _keyDownList.removeWhere((element) => element == toRemove); |
+ } else if (_keyDownList.length > 0) { |
+ // This happens when we've reached some international keyboard case we |
+ // haven't accounted for or we haven't correctly eliminated all browser |
+ // inconsistencies. Filing bugs on when this is reached is welcome! |
+ _keyDownList.removeLast(); |
+ } |
+ _dispatch(e); |
} |
} |
-// From: https://github.com/rafaelw/ChangeSummary/blob/master/change_summary.js |
-const _pathIndentPart = r'[$a-z0-9_]+[$a-z0-9_\d]*'; |
-final _pathRegExp = new RegExp('^' |
- '(?:#?' + _pathIndentPart + ')?' |
- '(?:' |
- '(?:\\.' + _pathIndentPart + ')' |
- ')*' |
- r'$', caseSensitive: false); |
+/** |
+ * Records KeyboardEvents that occur on a particular element, and provides a |
+ * stream of outgoing KeyEvents with cross-browser consistent keyCode and |
+ * charCode values despite the fact that a multitude of browsers that have |
+ * varying keyboard default behavior. |
+ * |
+ * Example usage: |
+ * |
+ * KeyboardEventStream.onKeyDown(document.body).listen( |
+ * keydownHandlerTest); |
+ * |
+ * This class is very much a work in progress, and we'd love to get information |
+ * on how we can make this class work with as many international keyboards as |
+ * possible. Bugs welcome! |
+ */ |
+class KeyboardEventStream { |
-final _spacesRegExp = new RegExp(r'\s'); |
+ /** Named constructor to produce a stream for onKeyPress events. */ |
+ static Stream<KeyEvent> onKeyPress(EventTarget target) => |
+ new _KeyboardEventHandler('keypress').forTarget(target); |
-bool _isPathValid(String s) { |
- s = s.replaceAll(_spacesRegExp, ''); |
+ /** Named constructor to produce a stream for onKeyUp events. */ |
+ static Stream<KeyEvent> onKeyUp(EventTarget target) => |
+ new _KeyboardEventHandler('keyup').forTarget(target); |
- if (s == '') return true; |
- if (s[0] == '.') return false; |
- return _pathRegExp.hasMatch(s); |
+ /** Named constructor to produce a stream for onKeyDown events. */ |
+ static Stream<KeyEvent> onKeyDown(EventTarget target) => |
+ new _KeyboardEventHandler('keydown').forTarget(target); |
} |
-// Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file |
+// 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. |
+typedef void _MicrotaskCallback(); |
+ |
/** |
- * A utility class for representing two-dimensional positions. |
+ * This class attempts to invoke a callback as soon as the current event stack |
+ * unwinds, but before the browser repaints. |
*/ |
-class Point { |
- final num x; |
- final num y; |
+abstract class _MicrotaskScheduler { |
+ bool _nextMicrotaskFrameScheduled = false; |
+ final _MicrotaskCallback _callback; |
- const Point([num x = 0, num y = 0]): x = x, y = y; |
+ _MicrotaskScheduler(this._callback); |
- String toString() => '($x, $y)'; |
+ /** |
+ * Creates the best possible microtask scheduler for the current platform. |
+ */ |
+ factory _MicrotaskScheduler.best(_MicrotaskCallback callback) { |
+ if (Window._supportsSetImmediate) { |
+ return new _SetImmediateScheduler(callback); |
+ } else if (MutationObserver.supported) { |
+ return new _MutationObserverScheduler(callback); |
+ } |
+ return new _PostMessageScheduler(callback); |
+ } |
- bool operator ==(other) { |
- if (other is !Point) return false; |
- return x == other.x && y == other.y; |
+ /** |
+ * Schedules a microtask callback if one has not been scheduled already. |
+ */ |
+ void maybeSchedule() { |
+ if (this._nextMicrotaskFrameScheduled) { |
+ return; |
+ } |
+ this._nextMicrotaskFrameScheduled = true; |
+ this._schedule(); |
} |
- Point operator +(Point other) { |
- return new Point(x + other.x, y + other.y); |
+ /** |
+ * Does the actual scheduling of the callback. |
+ */ |
+ void _schedule(); |
+ |
+ /** |
+ * Handles the microtask callback and forwards it if necessary. |
+ */ |
+ void _onCallback() { |
+ // Ignore spurious messages. |
+ if (!_nextMicrotaskFrameScheduled) { |
+ return; |
+ } |
+ _nextMicrotaskFrameScheduled = false; |
+ this._callback(); |
} |
+} |
- Point operator -(Point other) { |
- return new Point(x - other.x, y - other.y); |
+/** |
+ * Scheduler which uses window.postMessage to schedule events. |
+ */ |
+class _PostMessageScheduler extends _MicrotaskScheduler { |
+ const _MICROTASK_MESSAGE = "DART-MICROTASK"; |
+ |
+ _PostMessageScheduler(_MicrotaskCallback callback): super(callback) { |
+ // Messages from other windows do not cause a security risk as |
+ // all we care about is that _handleMessage is called |
+ // after the current event loop is unwound and calling the function is |
+ // a noop when zero requests are pending. |
+ window.onMessage.listen(this._handleMessage); |
} |
- Point operator *(num factor) { |
- return new Point(x * factor, y * factor); |
+ void _schedule() { |
+ window.postMessage(_MICROTASK_MESSAGE, "*"); |
} |
- /** |
- * Returns the distance between two points. |
- */ |
- double distanceTo(Point other) { |
- var dx = x - other.x; |
- var dy = y - other.y; |
- return sqrt(dx * dx + dy * dy); |
+ void _handleMessage(e) { |
+ this._onCallback(); |
} |
+} |
- /** |
- * Returns the squared distance between two points. |
- * |
- * Squared distances can be used for comparisons when the actual value is not |
- * required. |
- */ |
- num squaredDistanceTo(Point other) { |
- var dx = x - other.x; |
- var dy = y - other.y; |
- return dx * dx + dy * dy; |
+/** |
+ * Scheduler which uses a MutationObserver to schedule events. |
+ */ |
+class _MutationObserverScheduler extends _MicrotaskScheduler { |
+ MutationObserver _observer; |
+ Element _dummy; |
+ |
+ _MutationObserverScheduler(_MicrotaskCallback callback): super(callback) { |
+ // Mutation events get fired as soon as the current event stack is unwound |
+ // so we just make a dummy event and listen for that. |
+ _observer = new MutationObserver(this._handleMutation); |
+ _dummy = new DivElement(); |
+ _observer.observe(_dummy, attributes: true); |
} |
- Point ceil() => new Point(x.ceil(), y.ceil()); |
- Point floor() => new Point(x.floor(), y.floor()); |
- Point round() => new Point(x.round(), y.round()); |
+ void _schedule() { |
+ // Toggle it to trigger the mutation event. |
+ _dummy.hidden = !_dummy.hidden; |
+ } |
- /** |
- * Truncates x and y to integers and returns the result as a new point. |
- */ |
- Point toInt() => new Point(x.toInt(), y.toInt()); |
+ _handleMutation(List<MutationRecord> mutations, MutationObserver observer) { |
+ this._onCallback(); |
+ } |
} |
-// Copyright (c) 2011, 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. |
- |
/** |
- * Contains the set of standard values returned by HTMLDocument.getReadyState. |
+ * Scheduler which uses window.setImmediate to schedule events. |
*/ |
-abstract class ReadyState { |
- /** |
- * Indicates the document is still loading and parsing. |
- */ |
- static const String LOADING = "loading"; |
+class _SetImmediateScheduler extends _MicrotaskScheduler { |
+ _SetImmediateScheduler(_MicrotaskCallback callback): super(callback); |
- /** |
- * Indicates the document is finished parsing but is still loading |
- * subresources. |
- */ |
- static const String INTERACTIVE = "interactive"; |
+ void _schedule() { |
+ window._setImmediate(_handleImmediate); |
+ } |
- /** |
- * Indicates the document and all subresources have been loaded. |
- */ |
- static const String COMPLETE = "complete"; |
+ void _handleImmediate() { |
+ this._onCallback(); |
+ } |
} |
-// Copyright (c) 2013, 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. |
+List<TimeoutHandler> _pendingMicrotasks; |
+_MicrotaskScheduler _microtaskScheduler = null; |
+ |
+void _maybeScheduleMicrotaskFrame() { |
+ if (_microtaskScheduler == null) { |
+ _microtaskScheduler = |
+ new _MicrotaskScheduler.best(_completeMicrotasks); |
+ } |
+ _microtaskScheduler.maybeSchedule(); |
+} |
/** |
- * A class for representing two-dimensional rectangles. |
+ * Registers a [callback] which is called after the current execution stack |
+ * unwinds. |
*/ |
-class Rect { |
- final num left; |
- final num top; |
- final num width; |
- final num height; |
- |
- const Rect(this.left, this.top, this.width, this.height); |
- |
- factory Rect.fromPoints(Point a, Point b) { |
- var left; |
- var width; |
- if (a.x < b.x) { |
- left = a.x; |
- width = b.x - left; |
- } else { |
- left = b.x; |
- width = a.x - left; |
- } |
- var top; |
- var height; |
- if (a.y < b.y) { |
- top = a.y; |
- height = b.y - top; |
- } else { |
- top = b.y; |
- height = a.y - top; |
- } |
+void _addMicrotaskCallback(TimeoutHandler callback) { |
+ if (_pendingMicrotasks == null) { |
+ _pendingMicrotasks = <TimeoutHandler>[]; |
+ _maybeScheduleMicrotaskFrame(); |
+ } |
+ _pendingMicrotasks.add(callback); |
+} |
- return new Rect(left, top, width, height); |
+ |
+/** |
+ * Complete all pending microtasks. |
+ */ |
+void _completeMicrotasks() { |
+ var callbacks = _pendingMicrotasks; |
+ _pendingMicrotasks = null; |
+ for (var callback in callbacks) { |
+ callback(); |
} |
+} |
+// Copyright (c) 2013, 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. |
- num get right => left + width; |
- num get bottom => top + height; |
- // NOTE! All code below should be common with Rect. |
- // TODO: implement with mixins when available. |
- String toString() { |
- return '($left, $top, $width, $height)'; |
- } |
+/** |
+ * Class which helps construct standard node validation policies. |
+ * |
+ * By default this will not accept anything, but the 'allow*' functions can be |
+ * used to expand what types of elements or attributes are allowed. |
+ * |
+ * All allow functions are additive- elements will be accepted if they are |
+ * accepted by any specific rule. |
+ */ |
+class NodeValidatorBuilder implements NodeValidator { |
- bool operator ==(other) { |
- if (other is !Rect) return false; |
- return left == other.left && top == other.top && width == other.width && |
- height == other.height; |
+ final List<NodeValidator> _validators = <NodeValidator>[]; |
+ |
+ NodeValidatorBuilder() { |
} |
/** |
- * Computes the intersection of this rectangle and the rectangle parameter. |
- * Returns null if there is no intersection. |
+ * Creates a new NodeValidatorBuilder which accepts common constructs. |
+ * |
+ * By default this will accept HTML5 elements and attributes with the default |
+ * [UriPolicy] and templating elements. |
*/ |
- Rect intersection(Rect rect) { |
- var x0 = max(left, rect.left); |
- var x1 = min(left + width, rect.left + rect.width); |
- |
- if (x0 <= x1) { |
- var y0 = max(top, rect.top); |
- var y1 = min(top + height, rect.top + rect.height); |
+ factory NodeValidatorBuilder.common() { |
+ return new NodeValidatorBuilder() |
+ ..allowHtml5() |
+ ..allowTemplating(); |
+ } |
- if (y0 <= y1) { |
- return new Rect(x0, y0, x1 - x0, y1 - y0); |
- } |
+ /** |
+ * Allows navigation elements- Form and Anchor tags, along with common |
+ * attributes. |
+ * |
+ * The UriPolicy can be used to restrict the locations the navigation elements |
+ * are allowed to direct to. By default this will use the default [UriPolicy]. |
+ */ |
+ void allowNavigation([UriPolicy uriPolicy]) { |
+ if (uriPolicy == null) { |
+ uriPolicy = new UriPolicy(); |
} |
- return null; |
+ add(new _SimpleNodeValidator.allowNavigation(uriPolicy)); |
} |
- |
/** |
- * Returns whether a rectangle intersects this rectangle. |
+ * Allows image elements. |
+ * |
+ * The UriPolicy can be used to restrict the locations the images may be |
+ * loaded from. By default this will use the default [UriPolicy]. |
*/ |
- bool intersects(Rect other) { |
- return (left <= other.left + other.width && other.left <= left + width && |
- top <= other.top + other.height && other.top <= top + height); |
+ void allowImages([UriPolicy uriPolicy]) { |
+ if (uriPolicy == null) { |
+ uriPolicy = new UriPolicy(); |
+ } |
+ add(new _SimpleNodeValidator.allowImages(uriPolicy)); |
} |
/** |
- * Returns a new rectangle which completely contains this rectangle and the |
- * input rectangle. |
+ * Allow basic text elements. |
+ * |
+ * This allows a subset of HTML5 elements, specifically just these tags and |
+ * no attributes. |
+ * |
+ * * B |
+ * * BLOCKQUOTE |
+ * * BR |
+ * * EM |
+ * * H1 |
+ * * H2 |
+ * * H3 |
+ * * H4 |
+ * * H5 |
+ * * H6 |
+ * * HR |
+ * * I |
+ * * LI |
+ * * OL |
+ * * P |
+ * * SPAN |
+ * * UL |
*/ |
- Rect union(Rect rect) { |
- var right = max(this.left + this.width, rect.left + rect.width); |
- var bottom = max(this.top + this.height, rect.top + rect.height); |
- |
- var left = min(this.left, rect.left); |
- var top = min(this.top, rect.top); |
- |
- return new Rect(left, top, right - left, bottom - top); |
+ void allowTextElements() { |
+ add(new _SimpleNodeValidator.allowTextElements()); |
} |
/** |
- * Tests whether this rectangle entirely contains another rectangle. |
+ * Allow common safe HTML5 elements and attributes. |
+ * |
+ * This list is based off of the Caja whitelists at: |
+ * https://code.google.com/p/google-caja/wiki/CajaWhitelists. |
+ * |
+ * Common things which are not allowed are script elements, style attributes |
+ * and any script handlers. |
*/ |
- bool containsRect(Rect another) { |
- return left <= another.left && |
- left + width >= another.left + another.width && |
- top <= another.top && |
- top + height >= another.top + another.height; |
+ void allowHtml5({UriPolicy uriPolicy}) { |
+ add(new _Html5NodeValidator(uriPolicy: uriPolicy)); |
} |
/** |
- * Tests whether this rectangle entirely contains a point. |
+ * Allow SVG elements and attributes except for known bad ones. |
*/ |
- bool containsPoint(Point another) { |
- return another.x >= left && |
- another.x <= left + width && |
- another.y >= top && |
- another.y <= top + height; |
+ void allowSvg() { |
+ add(new _SvgNodeValidator()); |
} |
- Rect ceil() => new Rect(left.ceil(), top.ceil(), width.ceil(), height.ceil()); |
- Rect floor() => new Rect(left.floor(), top.floor(), width.floor(), |
- height.floor()); |
- Rect round() => new Rect(left.round(), top.round(), width.round(), |
- height.round()); |
+ /** |
+ * Allow custom elements with the specified tag name and specified attributes. |
+ * |
+ * This will allow the elements as custom tags (such as <x-foo></x-foo>), |
+ * but will not allow tag extensions. Use [allowTagExtension] to allow |
+ * tag extensions. |
+ */ |
+ void allowCustomElement(String tagName, |
+ {UriPolicy uriPolicy, |
+ Iterable<String> attributes, |
+ Iterable<String> uriAttributes}) { |
+ |
+ var tagNameUpper = tagName.toUpperCase(); |
+ var attrs; |
+ if (attributes != null) { |
+ attrs = |
+ attributes.map((name) => '$tagNameUpper::${name.toLowerCase()}'); |
+ } |
+ var uriAttrs; |
+ if (uriAttributes != null) { |
+ uriAttrs = |
+ uriAttributes.map((name) => '$tagNameUpper::${name.toLowerCase()}'); |
+ } |
+ if (uriPolicy == null) { |
+ uriPolicy = new UriPolicy(); |
+ } |
+ |
+ add(new _CustomElementNodeValidator( |
+ uriPolicy, |
+ [tagNameUpper], |
+ attrs, |
+ uriAttrs, |
+ false, |
+ true)); |
+ } |
/** |
- * Truncates coordinates to integers and returns the result as a new |
- * rectangle. |
+ * Allow custom tag extensions with the specified type name and specified |
+ * attributes. |
+ * |
+ * This will allow tag extensions (such as <div is="x-foo"></div>), |
+ * but will not allow custom tags. Use [allowCustomElement] to allow |
+ * custom tags. |
*/ |
- Rect toInt() => new Rect(left.toInt(), top.toInt(), width.toInt(), |
- height.toInt()); |
+ void allowTagExtension(String tagName, String baseName, |
+ {UriPolicy uriPolicy, |
+ Iterable<String> attributes, |
+ Iterable<String> uriAttributes}) { |
- Point get topLeft => new Point(this.left, this.top); |
- Point get bottomRight => new Point(this.left + this.width, |
- this.top + this.height); |
-} |
-// Copyright (c) 2013, 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. |
+ var baseNameUpper = baseName.toUpperCase(); |
+ var tagNameUpper = tagName.toUpperCase(); |
+ var attrs; |
+ if (attributes != null) { |
+ attrs = |
+ attributes.map((name) => '$baseNameUpper::${name.toLowerCase()}'); |
+ } |
+ var uriAttrs; |
+ if (uriAttributes != null) { |
+ uriAttrs = |
+ uriAttributes.map((name) => '$baseNameUpper::${name.toLowerCase()}'); |
+ } |
+ if (uriPolicy == null) { |
+ uriPolicy = new UriPolicy(); |
+ } |
+ add(new _CustomElementNodeValidator( |
+ uriPolicy, |
+ [tagNameUpper, baseNameUpper], |
+ attrs, |
+ uriAttrs, |
+ true, |
+ false)); |
+ } |
-// This code is a port of Model-Driven-Views: |
-// https://github.com/polymer-project/mdv |
-// The code mostly comes from src/template_element.js |
+ void allowElement(String tagName, {UriPolicy uriPolicy, |
+ Iterable<String> attributes, |
+ Iterable<String> uriAttributes}) { |
-typedef void _ChangeHandler(value); |
+ allowCustomElement(tagName, uriPolicy: uriPolicy, |
+ attributes: attributes, |
+ uriAttributes: uriAttributes); |
+ } |
-/** |
- * Model-Driven Views (MDV)'s native features enables a wide-range of use cases, |
- * but (by design) don't attempt to implement a wide array of specialized |
- * behaviors. |
- * |
- * Enabling these features in MDV is a matter of implementing and registering an |
- * MDV Custom Syntax. A Custom Syntax is an object which contains one or more |
- * delegation functions which implement specialized behavior. This object is |
- * registered with MDV via [TemplateElement.syntax]: |
- * |
- * |
- * HTML: |
- * <template bind syntax="MySyntax"> |
- * {{ What!Ever('crazy')->thing^^^I+Want(data) }} |
- * </template> |
- * |
- * Dart: |
- * class MySyntax extends CustomBindingSyntax { |
- * getBinding(model, path, name, node) { |
- * // The magic happens here! |
- * } |
- * } |
- * |
- * ... |
- * |
- * TemplateElement.syntax['MySyntax'] = new MySyntax(); |
- * |
- * See <https://github.com/polymer-project/mdv/blob/master/docs/syntax.md> for more |
- * information about Custom Syntax. |
- */ |
-// TODO(jmesserly): if this is just one method, a function type would make it |
-// more Dart-friendly. |
-@Experimental |
-abstract class CustomBindingSyntax { |
/** |
- * This syntax method allows for a custom interpretation of the contents of |
- * mustaches (`{{` ... `}}`). |
- * |
- * When a template is inserting an instance, it will invoke this method for |
- * each mustache which is encountered. The function is invoked with four |
- * arguments: |
- * |
- * - [model]: The data context for which this instance is being created. |
- * - [path]: The text contents (trimmed of outer whitespace) of the mustache. |
- * - [name]: The context in which the mustache occurs. Within element |
- * attributes, this will be the name of the attribute. Within text, |
- * this will be 'text'. |
- * - [node]: A reference to the node to which this binding will be created. |
- * |
- * If the method wishes to handle binding, it is required to return an object |
- * which has at least a `value` property that can be observed. If it does, |
- * then MDV will call [Node.bind on the node: |
- * |
- * node.bind(name, retval, 'value'); |
+ * Allow templating elements (such as <template> and template-related |
+ * attributes. |
* |
- * If the 'getBinding' does not wish to override the binding, it should return |
- * null. |
+ * This still requires other validators to allow regular attributes to be |
+ * bound (such as [allowHtml5]). |
*/ |
- // TODO(jmesserly): I had to remove type annotations from "name" and "node" |
- // Normally they are String and Node respectively. But sometimes it will pass |
- // (int name, CompoundBinding node). That seems very confusing; we may want |
- // to change this API. |
- getBinding(model, String path, name, node) => null; |
+ void allowTemplating() { |
+ add(new _TemplatingNodeValidator()); |
+ } |
/** |
- * This syntax method allows a syntax to provide an alterate model than the |
- * one the template would otherwise use when producing an instance. |
- * |
- * When a template is about to create an instance, it will invoke this method |
- * The function is invoked with two arguments: |
- * |
- * - [template]: The template element which is about to create and insert an |
- * instance. |
- * - [model]: The data context for which this instance is being created. |
+ * Add an additional validator to the current list of validators. |
* |
- * The template element will always use the return value of `getInstanceModel` |
- * as the model for the new instance. If the syntax does not wish to override |
- * the value, it should simply return the `model` value it was passed. |
+ * Elements and attributes will be accepted if they are accepted by any |
+ * validators. |
*/ |
- getInstanceModel(Element template, model) => model; |
+ void add(NodeValidator validator) { |
+ _validators.add(validator); |
+ } |
+ |
+ bool allowsElement(Element element) { |
+ return _validators.any((v) => v.allowsElement(element)); |
+ } |
+ |
+ bool allowsAttribute(Element element, String attributeName, String value) { |
+ return _validators.any( |
+ (v) => v.allowsAttribute(element, attributeName, value)); |
+ } |
+} |
+ |
+class _SimpleNodeValidator implements NodeValidator { |
+ final Set<String> allowedElements; |
+ final Set<String> allowedAttributes; |
+ final Set<String> allowedUriAttributes; |
+ final UriPolicy uriPolicy; |
+ |
+ factory _SimpleNodeValidator.allowNavigation(UriPolicy uriPolicy) { |
+ return new _SimpleNodeValidator(uriPolicy, |
+ allowedElements: [ |
+ 'A', |
+ 'FORM'], |
+ allowedAttributes: [ |
+ 'A::accesskey', |
+ 'A::coords', |
+ 'A::hreflang', |
+ 'A::name', |
+ 'A::shape', |
+ 'A::tabindex', |
+ 'A::target', |
+ 'A::type', |
+ 'FORM::accept', |
+ 'FORM::autocomplete', |
+ 'FORM::enctype', |
+ 'FORM::method', |
+ 'FORM::name', |
+ 'FORM::novalidate', |
+ 'FORM::target', |
+ ], |
+ allowedUriAttributes: [ |
+ 'A::href', |
+ 'FORM::action', |
+ ]); |
+ } |
+ |
+ factory _SimpleNodeValidator.allowImages(UriPolicy uriPolicy) { |
+ return new _SimpleNodeValidator(uriPolicy, |
+ allowedElements: [ |
+ 'IMG' |
+ ], |
+ allowedAttributes: [ |
+ 'IMG::align', |
+ 'IMG::alt', |
+ 'IMG::border', |
+ 'IMG::height', |
+ 'IMG::hspace', |
+ 'IMG::ismap', |
+ 'IMG::name', |
+ 'IMG::usemap', |
+ 'IMG::vspace', |
+ 'IMG::width', |
+ ], |
+ allowedUriAttributes: [ |
+ 'IMG::src', |
+ ]); |
+ } |
+ |
+ factory _SimpleNodeValidator.allowTextElements() { |
+ return new _SimpleNodeValidator(null, |
+ allowedElements: [ |
+ 'B', |
+ 'BLOCKQUOTE', |
+ 'BR', |
+ 'EM', |
+ 'H1', |
+ 'H2', |
+ 'H3', |
+ 'H4', |
+ 'H5', |
+ 'H6', |
+ 'HR', |
+ 'I', |
+ 'LI', |
+ 'OL', |
+ 'P', |
+ 'SPAN', |
+ 'UL', |
+ ]); |
+ } |
/** |
- * This syntax method allows a syntax to provide an alterate expansion of |
- * the [template] contents. When the template wants to create an instance, |
- * it will call this method with the template element. |
- * |
- * By default this will call `template.createInstance()`. |
+ * Elements must be uppercased tag names. For example `'IMG'`. |
+ * Attributes must be uppercased tag name followed by :: followed by |
+ * lowercase attribute name. For example `'IMG:src'`. |
*/ |
- getInstanceFragment(Element template) => template.createInstance(); |
+ _SimpleNodeValidator(this.uriPolicy, |
+ {Iterable<String> allowedElements, Iterable<String> allowedAttributes, |
+ Iterable<String> allowedUriAttributes}): |
+ this.allowedElements = allowedElements != null ? |
+ new Set.from(allowedElements) : new Set(), |
+ this.allowedAttributes = allowedAttributes != null ? |
+ new Set.from(allowedAttributes) : new Set(), |
+ this.allowedUriAttributes = allowedUriAttributes != null ? |
+ new Set.from(allowedUriAttributes) : new Set(); |
+ |
+ bool allowsElement(Element element) { |
+ return allowedElements.contains(element.tagName); |
+ } |
+ |
+ bool allowsAttribute(Element element, String attributeName, String value) { |
+ var tagName = element.tagName; |
+ if (allowedUriAttributes.contains('$tagName::$attributeName')) { |
+ return uriPolicy.allowsUri(value); |
+ } else if (allowedUriAttributes.contains('*::$attributeName')) { |
+ return uriPolicy.allowsUri(value); |
+ } else if (allowedAttributes.contains('$tagName::$attributeName')) { |
+ return true; |
+ } else if (allowedAttributes.contains('*::$attributeName')) { |
+ return true; |
+ } else if (allowedAttributes.contains('$tagName::*')) { |
+ return true; |
+ } else if (allowedAttributes.contains('*::*')) { |
+ return true; |
+ } |
+ return false; |
+ } |
} |
-/** The callback used in the [CompoundBinding.combinator] field. */ |
-@Experimental |
-typedef Object CompoundBindingCombinator(Map objects); |
+class _CustomElementNodeValidator extends _SimpleNodeValidator { |
+ final bool allowTypeExtension; |
+ final bool allowCustomTag; |
-/** Information about the instantiated template. */ |
-@Experimental |
-class TemplateInstance { |
- // TODO(rafaelw): firstNode & lastNode should be read-synchronous |
- // in cases where script has modified the template instance boundary. |
+ _CustomElementNodeValidator(UriPolicy uriPolicy, |
+ Iterable<String> allowedElements, |
+ Iterable<String> allowedAttributes, |
+ Iterable<String> allowedUriAttributes, |
+ bool allowTypeExtension, |
+ bool allowCustomTag): |
+ |
+ super(uriPolicy, |
+ allowedElements: allowedElements, |
+ allowedAttributes: allowedAttributes, |
+ allowedUriAttributes: allowedUriAttributes), |
+ this.allowTypeExtension = allowTypeExtension == true, |
+ this.allowCustomTag = allowCustomTag == true; |
+ |
+ bool allowsElement(Element element) { |
+ if (allowTypeExtension) { |
+ var isAttr = element.attributes['is']; |
+ if (isAttr != null) { |
+ return allowedElements.contains(isAttr.toUpperCase()) && |
+ allowedElements.contains(element.tagName); |
+ } |
+ } |
+ return allowCustomTag && allowedElements.contains(element.tagName); |
+ } |
+ |
+ bool allowsAttribute(Element element, String attributeName, String value) { |
+ if (allowsElement(element)) { |
+ if (allowTypeExtension && attributeName == 'is' && |
+ allowedElements.contains(value.toUpperCase())) { |
+ return true; |
+ } |
+ return super.allowsAttribute(element, attributeName, value); |
+ } |
+ return false; |
+ } |
+} |
+ |
+class _TemplatingNodeValidator extends _SimpleNodeValidator { |
+ static const _TEMPLATE_ATTRS = |
+ const <String>['bind', 'if', 'ref', 'repeat', 'syntax']; |
+ |
+ final Set<String> _templateAttrs; |
+ |
+ _TemplatingNodeValidator(): |
+ super(null, |
+ allowedElements: [ |
+ 'TEMPLATE' |
+ ], |
+ allowedAttributes: _TEMPLATE_ATTRS.map((attr) => 'TEMPLATE::$attr')), |
+ _templateAttrs = new Set<String>.from(_TEMPLATE_ATTRS) { |
+ } |
+ |
+ bool allowsAttribute(Element element, String attributeName, String value) { |
+ if (super.allowsAttribute(element, attributeName, value)) { |
+ return true; |
+ } |
+ |
+ if (attributeName == 'template' && value == "") { |
+ return true; |
+ } |
+ |
+ if (element.attributes['template'] == "" ) { |
+ return _templateAttrs.contains(attributeName); |
+ } |
+ return false; |
+ } |
+} |
+ |
+ |
+class _SvgNodeValidator implements NodeValidator { |
+ bool allowsElement(Element element) { |
+ if (element is svg.ScriptElement) { |
+ return false; |
+ } |
+ if (element is svg.SvgElement) { |
+ return true; |
+ } |
+ return false; |
+ } |
+ |
+ bool allowsAttribute(Element element, String attributeName, String value) { |
+ if (attributeName == 'is' || attributeName.startsWith('on')) { |
+ return false; |
+ } |
+ return allowsElement(element); |
+ } |
+} |
+// Copyright (c) 2013, 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. |
- /** The first node of this template instantiation. */ |
- final Node firstNode; |
- /** |
- * The last node of this template instantiation. |
- * This could be identical to [firstNode] if the template only expanded to a |
- * single node. |
- */ |
- final Node lastNode; |
+// This code is inspired by ChangeSummary: |
+// https://github.com/rafaelw/ChangeSummary/blob/master/change_summary.js |
+// ...which underlies MDV. Since we don't need the functionality of |
+// ChangeSummary, we just implement what we need for data bindings. |
+// This allows our implementation to be much simpler. |
- /** The model used to instantiate the template. */ |
- final model; |
+// TODO(jmesserly): should we make these types stronger, and require |
+// Observable objects? Currently, it is fine to say something like: |
+// var path = new PathObserver(123, ''); |
+// print(path.value); // "123" |
+// |
+// Furthermore this degenerate case is allowed: |
+// var path = new PathObserver(123, 'foo.bar.baz.qux'); |
+// print(path.value); // "null" |
+// |
+// Here we see that any invalid (i.e. not Observable) value will break the |
+// path chain without producing an error or exception. |
+// |
+// Now the real question: should we do this? For the former case, the behavior |
+// is correct but we could chose to handle it in the dart:html bindings layer. |
+// For the latter case, it might be better to throw an error so users can find |
+// the problem. |
- TemplateInstance(this.firstNode, this.lastNode, this.model); |
-} |
/** |
- * Model-Driven Views contains a helper object which is useful for the |
- * implementation of a Custom Syntax. |
- * |
- * var binding = new CompoundBinding((values) { |
- * var combinedValue; |
- * // compute combinedValue based on the current values which are provided |
- * return combinedValue; |
- * }); |
- * binding.bind('name1', obj1, path1); |
- * binding.bind('name2', obj2, path2); |
- * //... |
- * binding.bind('nameN', objN, pathN); |
+ * A data-bound path starting from a view-model or model object, for example |
+ * `foo.bar.baz`. |
* |
- * CompoundBinding is an object which knows how to listen to multiple path |
- * values (registered via [bind]) and invoke its [combinator] when one or more |
- * of the values have changed and set its [value] property to the return value |
- * of the function. When any value has changed, all current values are provided |
- * to the [combinator] in the single `values` argument. |
+ * When the [values] stream is being listened to, this will observe changes to |
+ * the object and any intermediate object along the path, and send [values] |
+ * accordingly. When all listeners are unregistered it will stop observing |
+ * the objects. |
* |
- * See [CustomBindingSyntax] for more information. |
+ * This class is used to implement [Node.bind] and similar functionality. |
*/ |
-// TODO(jmesserly): what is the public API surface here? I just guessed; |
-// most of it seemed non-public. |
+// TODO(jmesserly): find a better home for this type. |
@Experimental |
-class CompoundBinding extends ObservableBase { |
- CompoundBindingCombinator _combinator; |
+class PathObserver { |
+ /** The object being observed. */ |
+ final object; |
- // TODO(jmesserly): ideally these would be String keys, but sometimes we |
- // use integers. |
- Map<dynamic, StreamSubscription> _bindings = new Map(); |
- Map _values = new Map(); |
- bool _scheduled = false; |
- bool _disposed = false; |
- Object _value; |
+ /** The path string. */ |
+ final String path; |
- CompoundBinding([CompoundBindingCombinator combinator]) { |
- // TODO(jmesserly): this is a tweak to the original code, it seemed to me |
- // that passing the combinator to the constructor should be equivalent to |
- // setting it via the property. |
- // I also added a null check to the combinator setter. |
- this.combinator = combinator; |
- } |
+ /** True if the path is valid, otherwise false. */ |
+ final bool _isValid; |
- CompoundBindingCombinator get combinator => _combinator; |
+ // TODO(jmesserly): same issue here as ObservableMixin: is there an easier |
+ // way to get a broadcast stream? |
+ StreamController _values; |
+ Stream _valueStream; |
- set combinator(CompoundBindingCombinator combinator) { |
- _combinator = combinator; |
- if (combinator != null) _scheduleResolve(); |
- } |
+ _PropertyObserver _observer, _lastObserver; |
- static const _VALUE = const Symbol('value'); |
+ Object _lastValue; |
+ bool _scheduled = false; |
- get value => _value; |
+ /** |
+ * Observes [path] on [object] for changes. This returns an object that can be |
+ * used to get the changes and get/set the value at this path. |
+ * See [PathObserver.values] and [PathObserver.value]. |
+ */ |
+ PathObserver(this.object, String path) |
+ : path = path, _isValid = _isPathValid(path) { |
- void set value(newValue) { |
- _value = notifyPropertyChange(_VALUE, _value, newValue); |
- } |
+ // TODO(jmesserly): if the path is empty, or the object is! Observable, we |
+ // can optimize the PathObserver to be more lightweight. |
- // TODO(jmesserly): remove these workarounds when dart2js supports mirrors! |
- getValueWorkaround(key) { |
- if (key == _VALUE) return value; |
- return null; |
+ _values = new StreamController.broadcast(sync: true, |
+ onListen: _observe, |
+ onCancel: _unobserve); |
+ |
+ if (_isValid) { |
+ var segments = []; |
+ for (var segment in path.trim().split('.')) { |
+ if (segment == '') continue; |
+ var index = int.parse(segment, onError: (_) {}); |
+ segments.add(index != null ? index : new Symbol(segment)); |
+ } |
+ |
+ // Create the property observer linked list. |
+ // Note that the structure of a path can't change after it is initially |
+ // constructed, even though the objects along the path can change. |
+ for (int i = segments.length - 1; i >= 0; i--) { |
+ _observer = new _PropertyObserver(this, segments[i], _observer); |
+ if (_lastObserver == null) _lastObserver = _observer; |
+ } |
+ } |
} |
- setValueWorkaround(key, val) { |
- if (key == _VALUE) value = val; |
+ |
+ // TODO(jmesserly): we could try adding the first value to the stream, but |
+ // that delivers the first record async. |
+ /** |
+ * Listens to the stream, and invokes the [callback] immediately with the |
+ * current [value]. This is useful for bindings, which want to be up-to-date |
+ * immediately. |
+ */ |
+ StreamSubscription bindSync(void callback(value)) { |
+ var result = values.listen(callback); |
+ callback(value); |
+ return result; |
} |
- void bind(name, model, String path) { |
- unbind(name); |
+ // TODO(jmesserly): should this be a change record with the old value? |
+ // TODO(jmesserly): should this be a broadcast stream? We only need |
+ // single-subscription in the bindings system, so single sub saves overhead. |
+ /** |
+ * Gets the stream of values that were observed at this path. |
+ * This returns a single-subscription stream. |
+ */ |
+ Stream get values => _values.stream; |
- _bindings[name] = new PathObserver(model, path).bindSync((value) { |
- _values[name] = value; |
- _scheduleResolve(); |
- }); |
+ /** Force synchronous delivery of [values]. */ |
+ void _deliverValues() { |
+ _scheduled = false; |
+ |
+ var newValue = value; |
+ if (!identical(_lastValue, newValue)) { |
+ _values.add(newValue); |
+ _lastValue = newValue; |
+ } |
} |
- void unbind(name, {bool suppressResolve: false}) { |
- var binding = _bindings.remove(name); |
- if (binding == null) return; |
+ void _observe() { |
+ if (_observer != null) { |
+ _lastValue = value; |
+ _observer.observe(); |
+ } |
+ } |
- binding.cancel(); |
- _values.remove(name); |
- if (!suppressResolve) _scheduleResolve(); |
+ void _unobserve() { |
+ if (_observer != null) _observer.unobserve(); |
} |
- // TODO(rafaelw): Is this the right processing model? |
- // TODO(rafaelw): Consider having a seperate ChangeSummary for |
- // CompoundBindings so to excess dirtyChecks. |
- void _scheduleResolve() { |
+ void _notifyChange() { |
if (_scheduled) return; |
_scheduled = true; |
- queueChangeRecords(resolve); |
+ |
+ // TODO(jmesserly): should we have a guarenteed order with respect to other |
+ // paths? If so, we could implement this fairly easily by sorting instances |
+ // of this class by birth order before delivery. |
+ queueChangeRecords(_deliverValues); |
} |
- void resolve() { |
- if (_disposed) return; |
- _scheduled = false; |
+ /** Gets the last reported value at this path. */ |
+ get value { |
+ if (!_isValid) return null; |
+ if (_observer == null) return object; |
+ _observer.ensureValue(object); |
+ return _lastObserver.value; |
+ } |
- if (_combinator == null) { |
- throw new StateError( |
- 'CompoundBinding attempted to resolve without a combinator'); |
+ /** Sets the value at this path. */ |
+ void set value(Object value) { |
+ // TODO(jmesserly): throw if property cannot be set? |
+ // MDV seems tolerant of these error. |
+ if (_observer == null || !_isValid) return; |
+ _observer.ensureValue(object); |
+ var last = _lastObserver; |
+ if (_setObjectProperty(last._object, last._property, value)) { |
+ // Technically, this would get updated asynchronously via a change record. |
+ // However, it is nice if calling the getter will yield the same value |
+ // that was just set. So we use this opportunity to update our cache. |
+ last.value = value; |
} |
- |
- value = _combinator(_values); |
} |
+} |
- void dispose() { |
- for (var binding in _bindings.values) { |
- binding.cancel(); |
+// TODO(jmesserly): these should go away in favor of mirrors! |
+_getObjectProperty(object, property) { |
+ if (object is List && property is int) { |
+ if (property >= 0 && property < object.length) { |
+ return object[property]; |
+ } else { |
+ return null; |
} |
- _bindings.clear(); |
- _values.clear(); |
+ } |
- _disposed = true; |
- value = null; |
+ // TODO(jmesserly): what about length? |
+ if (object is Map) return object[property]; |
+ |
+ if (object is Observable) return object.getValueWorkaround(property); |
+ |
+ return null; |
+} |
+ |
+bool _setObjectProperty(object, property, value) { |
+ if (object is List && property is int) { |
+ object[property] = value; |
+ } else if (object is Map) { |
+ object[property] = value; |
+ } else if (object is Observable) { |
+ (object as Observable).setValueWorkaround(property, value); |
+ } else { |
+ return false; |
} |
+ return true; |
} |
-abstract class _InputBinding { |
- final InputElement element; |
- PathObserver binding; |
- StreamSubscription _pathSub; |
- StreamSubscription _eventSub; |
- |
- _InputBinding(this.element, model, String path) { |
- binding = new PathObserver(model, path); |
- _pathSub = binding.bindSync(valueChanged); |
- _eventSub = _getStreamForInputType(element).listen(updateBinding); |
- } |
- void valueChanged(newValue); |
+class _PropertyObserver { |
+ final PathObserver _path; |
+ final _property; |
+ final _PropertyObserver _next; |
- void updateBinding(e); |
+ // TODO(jmesserly): would be nice not to store both of these. |
+ Object _object; |
+ Object _value; |
+ StreamSubscription _sub; |
- void unbind() { |
- binding = null; |
- _pathSub.cancel(); |
- _eventSub.cancel(); |
- } |
+ _PropertyObserver(this._path, this._property, this._next); |
+ get value => _value; |
- static Stream<Event> _getStreamForInputType(InputElement element) { |
- switch (element.type) { |
- case 'checkbox': |
- return element.onClick; |
- case 'radio': |
- case 'select-multiple': |
- case 'select-one': |
- return element.onChange; |
- default: |
- return element.onInput; |
+ void set value(Object newValue) { |
+ _value = newValue; |
+ if (_next != null) { |
+ if (_sub != null) _next.unobserve(); |
+ _next.ensureValue(_value); |
+ if (_sub != null) _next.observe(); |
} |
} |
-} |
-class _ValueBinding extends _InputBinding { |
- _ValueBinding(element, model, path) : super(element, model, path); |
+ void ensureValue(object) { |
+ // If we're observing, values should be up to date already. |
+ if (_sub != null) return; |
- void valueChanged(value) { |
- element.value = value == null ? '' : '$value'; |
+ _object = object; |
+ value = _getObjectProperty(object, _property); |
} |
- void updateBinding(e) { |
- binding.value = element.value; |
+ void observe() { |
+ if (_object is Observable) { |
+ assert(_sub == null); |
+ _sub = (_object as Observable).changes.listen(_onChange); |
+ } |
+ if (_next != null) _next.observe(); |
} |
-} |
-class _CheckedBinding extends _InputBinding { |
- _CheckedBinding(element, model, path) : super(element, model, path); |
+ void unobserve() { |
+ if (_sub == null) return; |
- void valueChanged(value) { |
- element.checked = _Bindings._toBoolean(value); |
+ _sub.cancel(); |
+ _sub = null; |
+ if (_next != null) _next.unobserve(); |
} |
- void updateBinding(e) { |
- binding.value = element.checked; |
- |
- // Only the radio button that is getting checked gets an event. We |
- // therefore find all the associated radio buttons and update their |
- // CheckedBinding manually. |
- if (element is InputElement && element.type == 'radio') { |
- for (var r in _getAssociatedRadioButtons(element)) { |
- var checkedBinding = r._checkedBinding; |
- if (checkedBinding != null) { |
- // Set the value directly to avoid an infinite call stack. |
- checkedBinding.binding.value = false; |
- } |
+ void _onChange(List<ChangeRecord> changes) { |
+ for (var change in changes) { |
+ // TODO(jmesserly): what to do about "new Symbol" here? |
+ // Ideally this would only preserve names if the user has opted in to |
+ // them being preserved. |
+ // TODO(jmesserly): should we drop observable maps with String keys? |
+ // If so then we only need one check here. |
+ if (change.changes(_property)) { |
+ value = _getObjectProperty(_object, _property); |
+ _path._notifyChange(); |
+ return; |
} |
} |
} |
- |
- // |element| is assumed to be an HTMLInputElement with |type| == 'radio'. |
- // Returns an array containing all radio buttons other than |element| that |
- // have the same |name|, either in the form that |element| belongs to or, |
- // if no form, in the document tree to which |element| belongs. |
- // |
- // This implementation is based upon the HTML spec definition of a |
- // "radio button group": |
- // http://www.whatwg.org/specs/web-apps/current-work/multipage/number-state.html#radio-button-group |
- // |
- static Iterable _getAssociatedRadioButtons(element) { |
- if (!_isNodeInDocument(element)) return []; |
- if (element.form != null) { |
- return element.form.nodes.where((el) { |
- return el != element && |
- el is InputElement && |
- el.type == 'radio' && |
- el.name == element.name; |
- }); |
- } else { |
- var radios = element.document.queryAll( |
- 'input[type="radio"][name="${element.name}"]'); |
- return radios.where((el) => el != element && el.form == null); |
- } |
- } |
- |
- // TODO(jmesserly): polyfill document.contains API instead of doing it here |
- static bool _isNodeInDocument(Node node) { |
- // On non-IE this works: |
- // return node.document.contains(node); |
- var document = node.document; |
- if (node == document || node.parentNode == document) return true; |
- return document.documentElement.contains(node); |
- } |
} |
-class _Bindings { |
- // TODO(jmesserly): not sure what kind of boolean conversion rules to |
- // apply for template data-binding. HTML attributes are true if they're |
- // present. However Dart only treats "true" as true. Since this is HTML we'll |
- // use something closer to the HTML rules: null (missing) and false are false, |
- // everything else is true. See: https://github.com/polymer-project/mdv/issues/59 |
- static bool _toBoolean(value) => null != value && false != value; |
- |
- static Node _createDeepCloneAndDecorateTemplates(Node node, String syntax) { |
- var clone = node.clone(false); // Shallow clone. |
- if (clone is Element && clone.isTemplate) { |
- TemplateElement.decorate(clone, node); |
- if (syntax != null) { |
- clone.attributes.putIfAbsent('syntax', () => syntax); |
- } |
- } |
+// From: https://github.com/rafaelw/ChangeSummary/blob/master/change_summary.js |
- for (var c = node.$dom_firstChild; c != null; c = c.nextNode) { |
- clone.append(_createDeepCloneAndDecorateTemplates(c, syntax)); |
- } |
- return clone; |
- } |
+const _pathIndentPart = r'[$a-z0-9_]+[$a-z0-9_\d]*'; |
+final _pathRegExp = new RegExp('^' |
+ '(?:#?' + _pathIndentPart + ')?' |
+ '(?:' |
+ '(?:\\.' + _pathIndentPart + ')' |
+ ')*' |
+ r'$', caseSensitive: false); |
- // http://dvcs.w3.org/hg/webcomponents/raw-file/tip/spec/templates/index.html#dfn-template-contents-owner |
- static Document _getTemplateContentsOwner(HtmlDocument doc) { |
- if (doc.window == null) { |
- return doc; |
- } |
- var d = doc._templateContentsOwner; |
- if (d == null) { |
- // TODO(arv): This should either be a Document or HTMLDocument depending |
- // on doc. |
- d = doc.implementation.createHtmlDocument(''); |
- while (d.$dom_lastChild != null) { |
- d.$dom_lastChild.remove(); |
- } |
- doc._templateContentsOwner = d; |
- } |
- return d; |
- } |
+final _spacesRegExp = new RegExp(r'\s'); |
- static Element _cloneAndSeperateAttributeTemplate(Element templateElement) { |
- var clone = templateElement.clone(false); |
- var attributes = templateElement.attributes; |
- for (var name in attributes.keys.toList()) { |
- switch (name) { |
- case 'template': |
- case 'repeat': |
- case 'bind': |
- case 'ref': |
- clone.attributes.remove(name); |
- break; |
- default: |
- attributes.remove(name); |
- break; |
- } |
- } |
+bool _isPathValid(String s) { |
+ s = s.replaceAll(_spacesRegExp, ''); |
- return clone; |
- } |
+ if (s == '') return true; |
+ if (s[0] == '.') return false; |
+ return _pathRegExp.hasMatch(s); |
+} |
+// Copyright (c) 2013, 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. |
- static void _liftNonNativeChildrenIntoContent(Element templateElement) { |
- var content = templateElement.content; |
- if (!templateElement._isAttributeTemplate) { |
- var child; |
- while ((child = templateElement.$dom_firstChild) != null) { |
- content.append(child); |
- } |
- return; |
- } |
+/** |
+ * A utility class for representing two-dimensional positions. |
+ */ |
+class Point { |
+ final num x; |
+ final num y; |
- // For attribute templates we copy the whole thing into the content and |
- // we move the non template attributes into the content. |
- // |
- // <tr foo template> |
- // |
- // becomes |
- // |
- // <tr template> |
- // + #document-fragment |
- // + <tr foo> |
- // |
- var newRoot = _cloneAndSeperateAttributeTemplate(templateElement); |
- var child; |
- while ((child = templateElement.$dom_firstChild) != null) { |
- newRoot.append(child); |
- } |
- content.append(newRoot); |
- } |
+ const Point([num x = 0, num y = 0]): x = x, y = y; |
- static void _bootstrapTemplatesRecursivelyFrom(Node node) { |
- void bootstrap(template) { |
- if (!TemplateElement.decorate(template)) { |
- _bootstrapTemplatesRecursivelyFrom(template.content); |
- } |
- } |
+ String toString() => '($x, $y)'; |
- // Need to do this first as the contents may get lifted if |node| is |
- // template. |
- // TODO(jmesserly): node is DocumentFragment or Element |
- var descendents = (node as dynamic).queryAll(_allTemplatesSelectors); |
- if (node is Element && (node as Element).isTemplate) bootstrap(node); |
+ bool operator ==(other) { |
+ if (other is !Point) return false; |
+ return x == other.x && y == other.y; |
+ } |
- descendents.forEach(bootstrap); |
+ Point operator +(Point other) { |
+ return new Point(x + other.x, y + other.y); |
} |
- static final String _allTemplatesSelectors = 'template, option[template], ' + |
- Element._TABLE_TAGS.keys.map((k) => "$k[template]").join(", "); |
+ Point operator -(Point other) { |
+ return new Point(x - other.x, y - other.y); |
+ } |
- static void _addBindings(Node node, model, [CustomBindingSyntax syntax]) { |
- if (node is Element) { |
- _addAttributeBindings(node, model, syntax); |
- } else if (node is Text) { |
- _parseAndBind(node, 'text', node.text, model, syntax); |
- } |
+ Point operator *(num factor) { |
+ return new Point(x * factor, y * factor); |
+ } |
- for (var c = node.$dom_firstChild; c != null; c = c.nextNode) { |
- _addBindings(c, model, syntax); |
- } |
+ /** |
+ * Returns the distance between two points. |
+ */ |
+ double distanceTo(Point other) { |
+ var dx = x - other.x; |
+ var dy = y - other.y; |
+ return sqrt(dx * dx + dy * dy); |
} |
- static void _addAttributeBindings(Element element, model, syntax) { |
- element.attributes.forEach((name, value) { |
- if (value == '' && (name == 'bind' || name == 'repeat')) { |
- value = '{{}}'; |
- } |
- _parseAndBind(element, name, value, model, syntax); |
- }); |
+ /** |
+ * Returns the squared distance between two points. |
+ * |
+ * Squared distances can be used for comparisons when the actual value is not |
+ * required. |
+ */ |
+ num squaredDistanceTo(Point other) { |
+ var dx = x - other.x; |
+ var dy = y - other.y; |
+ return dx * dx + dy * dy; |
} |
- static void _parseAndBind(Node node, String name, String text, model, |
- CustomBindingSyntax syntax) { |
+ Point ceil() => new Point(x.ceil(), y.ceil()); |
+ Point floor() => new Point(x.floor(), y.floor()); |
+ Point round() => new Point(x.round(), y.round()); |
- var tokens = _parseMustacheTokens(text); |
- if (tokens.length == 0 || (tokens.length == 1 && tokens[0].isText)) { |
- return; |
- } |
+ /** |
+ * Truncates x and y to integers and returns the result as a new point. |
+ */ |
+ Point toInt() => new Point(x.toInt(), y.toInt()); |
+} |
+// Copyright (c) 2011, 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. |
- // If this is a custom element, give the .xtag a change to bind. |
- node = _nodeOrCustom(node); |
- if (tokens.length == 1 && tokens[0].isBinding) { |
- _bindOrDelegate(node, name, model, tokens[0].value, syntax); |
- return; |
- } |
+/** |
+ * Contains the set of standard values returned by HTMLDocument.getReadyState. |
+ */ |
+abstract class ReadyState { |
+ /** |
+ * Indicates the document is still loading and parsing. |
+ */ |
+ static const String LOADING = "loading"; |
- var replacementBinding = new CompoundBinding(); |
- for (var i = 0; i < tokens.length; i++) { |
- var token = tokens[i]; |
- if (token.isBinding) { |
- _bindOrDelegate(replacementBinding, i, model, token.value, syntax); |
- } |
- } |
+ /** |
+ * Indicates the document is finished parsing but is still loading |
+ * subresources. |
+ */ |
+ static const String INTERACTIVE = "interactive"; |
- replacementBinding.combinator = (values) { |
- var newValue = new StringBuffer(); |
+ /** |
+ * Indicates the document and all subresources have been loaded. |
+ */ |
+ static const String COMPLETE = "complete"; |
+} |
+// Copyright (c) 2013, 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. |
- for (var i = 0; i < tokens.length; i++) { |
- var token = tokens[i]; |
- if (token.isText) { |
- newValue.write(token.value); |
- } else { |
- var value = values[i]; |
- if (value != null) { |
- newValue.write(value); |
- } |
- } |
- } |
- return newValue.toString(); |
- }; |
+/** |
+ * A class for representing two-dimensional rectangles. |
+ */ |
+class Rect { |
+ final num left; |
+ final num top; |
+ final num width; |
+ final num height; |
- node.bind(name, replacementBinding, 'value'); |
+ const Rect(this.left, this.top, this.width, this.height); |
+ |
+ factory Rect.fromPoints(Point a, Point b) { |
+ var left; |
+ var width; |
+ if (a.x < b.x) { |
+ left = a.x; |
+ width = b.x - left; |
+ } else { |
+ left = b.x; |
+ width = a.x - left; |
+ } |
+ var top; |
+ var height; |
+ if (a.y < b.y) { |
+ top = a.y; |
+ height = b.y - top; |
+ } else { |
+ top = b.y; |
+ height = a.y - top; |
+ } |
+ |
+ return new Rect(left, top, width, height); |
} |
- static void _bindOrDelegate(node, name, model, String path, |
- CustomBindingSyntax syntax) { |
+ num get right => left + width; |
+ num get bottom => top + height; |
- if (syntax != null) { |
- var delegateBinding = syntax.getBinding(model, path, name, node); |
- if (delegateBinding != null) { |
- model = delegateBinding; |
- path = 'value'; |
- } |
- } |
+ // NOTE! All code below should be common with Rect. |
+ // TODO: implement with mixins when available. |
- node.bind(name, model, path); |
+ String toString() { |
+ return '($left, $top, $width, $height)'; |
+ } |
+ |
+ bool operator ==(other) { |
+ if (other is !Rect) return false; |
+ return left == other.left && top == other.top && width == other.width && |
+ height == other.height; |
} |
/** |
- * Gets the [node]'s custom [Element.xtag] if present, otherwise returns |
- * the node. This is used so nodes can override [Node.bind], [Node.unbind], |
- * and [Node.unbindAll] like InputElement does. |
+ * Computes the intersection of this rectangle and the rectangle parameter. |
+ * Returns null if there is no intersection. |
*/ |
- // TODO(jmesserly): remove this when we can extend Element for real. |
- static _nodeOrCustom(node) => node is Element ? node.xtag : node; |
+ Rect intersection(Rect rect) { |
+ var x0 = max(left, rect.left); |
+ var x1 = min(left + width, rect.left + rect.width); |
- static List<_BindingToken> _parseMustacheTokens(String s) { |
- var result = []; |
- var length = s.length; |
- var index = 0, lastIndex = 0; |
- while (lastIndex < length) { |
- index = s.indexOf('{{', lastIndex); |
- if (index < 0) { |
- result.add(new _BindingToken(s.substring(lastIndex))); |
- break; |
- } else { |
- // There is a non-empty text run before the next path token. |
- if (index > 0 && lastIndex < index) { |
- result.add(new _BindingToken(s.substring(lastIndex, index))); |
- } |
- lastIndex = index + 2; |
- index = s.indexOf('}}', lastIndex); |
- if (index < 0) { |
- var text = s.substring(lastIndex - 2); |
- if (result.length > 0 && result.last.isText) { |
- result.last.value += text; |
- } else { |
- result.add(new _BindingToken(text)); |
- } |
- break; |
- } |
+ if (x0 <= x1) { |
+ var y0 = max(top, rect.top); |
+ var y1 = min(top + height, rect.top + rect.height); |
- var value = s.substring(lastIndex, index).trim(); |
- result.add(new _BindingToken(value, isBinding: true)); |
- lastIndex = index + 2; |
+ if (y0 <= y1) { |
+ return new Rect(x0, y0, x1 - x0, y1 - y0); |
} |
} |
- return result; |
+ return null; |
} |
- static void _addTemplateInstanceRecord(fragment, model) { |
- if (fragment.$dom_firstChild == null) { |
- return; |
- } |
- |
- var instanceRecord = new TemplateInstance( |
- fragment.$dom_firstChild, fragment.$dom_lastChild, model); |
- var node = instanceRecord.firstNode; |
- while (node != null) { |
- node._templateInstance = instanceRecord; |
- node = node.nextNode; |
- } |
+ /** |
+ * Returns whether a rectangle intersects this rectangle. |
+ */ |
+ bool intersects(Rect other) { |
+ return (left <= other.left + other.width && other.left <= left + width && |
+ top <= other.top + other.height && other.top <= top + height); |
} |
- static void _removeAllBindingsRecursively(Node node) { |
- _nodeOrCustom(node).unbindAll(); |
- for (var c = node.$dom_firstChild; c != null; c = c.nextNode) { |
- _removeAllBindingsRecursively(c); |
- } |
- } |
+ /** |
+ * Returns a new rectangle which completely contains this rectangle and the |
+ * input rectangle. |
+ */ |
+ Rect union(Rect rect) { |
+ var right = max(this.left + this.width, rect.left + rect.width); |
+ var bottom = max(this.top + this.height, rect.top + rect.height); |
- static void _removeChild(Node parent, Node child) { |
- child._templateInstance = null; |
- if (child is Element && (child as Element).isTemplate) { |
- Element childElement = child; |
- // Make sure we stop observing when we remove an element. |
- var templateIterator = childElement._templateIterator; |
- if (templateIterator != null) { |
- templateIterator.abandon(); |
- childElement._templateIterator = null; |
- } |
- } |
- child.remove(); |
- _removeAllBindingsRecursively(child); |
+ var left = min(this.left, rect.left); |
+ var top = min(this.top, rect.top); |
+ |
+ return new Rect(left, top, right - left, bottom - top); |
} |
-} |
-class _BindingToken { |
- final String value; |
- final bool isBinding; |
+ /** |
+ * Tests whether this rectangle entirely contains another rectangle. |
+ */ |
+ bool containsRect(Rect another) { |
+ return left <= another.left && |
+ left + width >= another.left + another.width && |
+ top <= another.top && |
+ top + height >= another.top + another.height; |
+ } |
- _BindingToken(this.value, {this.isBinding: false}); |
+ /** |
+ * Tests whether this rectangle entirely contains a point. |
+ */ |
+ bool containsPoint(Point another) { |
+ return another.x >= left && |
+ another.x <= left + width && |
+ another.y >= top && |
+ another.y <= top + height; |
+ } |
- bool get isText => !isBinding; |
-} |
+ Rect ceil() => new Rect(left.ceil(), top.ceil(), width.ceil(), height.ceil()); |
+ Rect floor() => new Rect(left.floor(), top.floor(), width.floor(), |
+ height.floor()); |
+ Rect round() => new Rect(left.round(), top.round(), width.round(), |
+ height.round()); |
-class _TemplateIterator { |
- final Element _templateElement; |
- final List<Node> terminators = []; |
- final CompoundBinding inputs; |
- List iteratedValue; |
+ /** |
+ * Truncates coordinates to integers and returns the result as a new |
+ * rectangle. |
+ */ |
+ Rect toInt() => new Rect(left.toInt(), top.toInt(), width.toInt(), |
+ height.toInt()); |
- StreamSubscription _sub; |
- StreamSubscription _valueBinding; |
+ Point get topLeft => new Point(this.left, this.top); |
+ Point get bottomRight => new Point(this.left + this.width, |
+ this.top + this.height); |
+} |
+// 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. |
- _TemplateIterator(this._templateElement) |
- : inputs = new CompoundBinding(resolveInputs) { |
+// Patch file for the dart:isolate library. |
- _valueBinding = new PathObserver(inputs, 'value').bindSync(valueChanged); |
- } |
- static Object resolveInputs(Map values) { |
- if (values.containsKey('if') && !_Bindings._toBoolean(values['if'])) { |
- return null; |
- } |
+/******************************************************** |
+ Inserted from lib/isolate/serialization.dart |
+ ********************************************************/ |
- if (values.containsKey('repeat')) { |
- return values['repeat']; |
- } |
+class _MessageTraverserVisitedMap { |
- if (values.containsKey('bind')) { |
- return [values['bind']]; |
- } |
+ operator[](var object) => null; |
+ void operator[]=(var object, var info) { } |
- return null; |
- } |
+ void reset() { } |
+ void cleanup() { } |
- void valueChanged(value) { |
- clear(); |
- if (value is! List) return; |
+} |
- iteratedValue = value; |
+/** Abstract visitor for dart objects that can be sent as isolate messages. */ |
+abstract class _MessageTraverser { |
- if (value is Observable) { |
- _sub = value.changes.listen(_handleChanges); |
- } |
+ _MessageTraverserVisitedMap _visited; |
+ _MessageTraverser() : _visited = new _MessageTraverserVisitedMap(); |
- int len = iteratedValue.length; |
- if (len > 0) { |
- _handleChanges([new ListChangeRecord(0, addedCount: len)]); |
+ /** Visitor's entry point. */ |
+ traverse(var x) { |
+ if (isPrimitive(x)) return visitPrimitive(x); |
+ _visited.reset(); |
+ var result; |
+ try { |
+ result = _dispatch(x); |
+ } finally { |
+ _visited.cleanup(); |
} |
+ return result; |
} |
- Node getTerminatorAt(int index) { |
- if (index == -1) return _templateElement; |
- var terminator = terminators[index]; |
- if (terminator is! Element) return terminator; |
- |
- var subIterator = terminator._templateIterator; |
- if (subIterator == null) return terminator; |
+ _dispatch(var x) { |
+ if (isPrimitive(x)) return visitPrimitive(x); |
+ if (x is List) return visitList(x); |
+ if (x is Map) return visitMap(x); |
+ if (x is SendPort) return visitSendPort(x); |
+ if (x is SendPortSync) return visitSendPortSync(x); |
- return subIterator.getTerminatorAt(subIterator.terminators.length - 1); |
+ // Overridable fallback. |
+ return visitObject(x); |
} |
- void insertInstanceAt(int index, Node fragment) { |
- var previousTerminator = getTerminatorAt(index - 1); |
- var terminator = fragment.$dom_lastChild; |
- if (terminator == null) terminator = previousTerminator; |
+ visitPrimitive(x); |
+ visitList(List x); |
+ visitMap(Map x); |
+ visitSendPort(SendPort x); |
+ visitSendPortSync(SendPortSync x); |
- terminators.insert(index, terminator); |
- var parent = _templateElement.parentNode; |
- parent.insertBefore(fragment, previousTerminator.nextNode); |
+ visitObject(Object x) { |
+ // TODO(floitsch): make this a real exception. (which one)? |
+ throw "Message serialization: Illegal value $x passed"; |
} |
- void removeInstanceAt(int index) { |
- var previousTerminator = getTerminatorAt(index - 1); |
- var terminator = getTerminatorAt(index); |
- terminators.removeAt(index); |
- |
- var parent = _templateElement.parentNode; |
- while (terminator != previousTerminator) { |
- var node = terminator; |
- terminator = node.previousNode; |
- _Bindings._removeChild(parent, node); |
- } |
+ static bool isPrimitive(x) { |
+ return (x == null) || (x is String) || (x is num) || (x is bool); |
} |
+} |
- void removeAllInstances() { |
- if (terminators.length == 0) return; |
- var previousTerminator = _templateElement; |
- var terminator = getTerminatorAt(terminators.length - 1); |
- terminators.length = 0; |
+/** Visitor that serializes a message as a JSON array. */ |
+abstract class _Serializer extends _MessageTraverser { |
+ int _nextFreeRefId = 0; |
- var parent = _templateElement.parentNode; |
- while (terminator != previousTerminator) { |
- var node = terminator; |
- terminator = node.previousNode; |
- _Bindings._removeChild(parent, node); |
- } |
- } |
+ visitPrimitive(x) => x; |
- void clear() { |
- unobserve(); |
- removeAllInstances(); |
- iteratedValue = null; |
+ visitList(List list) { |
+ int copyId = _visited[list]; |
+ if (copyId != null) return ['ref', copyId]; |
+ |
+ int id = _nextFreeRefId++; |
+ _visited[list] = id; |
+ var jsArray = _serializeList(list); |
+ // TODO(floitsch): we are losing the generic type. |
+ return ['list', id, jsArray]; |
} |
- getInstanceModel(model, syntax) { |
- if (syntax != null) { |
- return syntax.getInstanceModel(_templateElement, model); |
- } |
- return model; |
+ visitMap(Map map) { |
+ int copyId = _visited[map]; |
+ if (copyId != null) return ['ref', copyId]; |
+ |
+ int id = _nextFreeRefId++; |
+ _visited[map] = id; |
+ var keys = _serializeList(map.keys.toList()); |
+ var values = _serializeList(map.values.toList()); |
+ // TODO(floitsch): we are losing the generic type. |
+ return ['map', id, keys, values]; |
} |
- getInstanceFragment(syntax) { |
- if (syntax != null) { |
- return syntax.getInstanceFragment(_templateElement); |
+ _serializeList(List list) { |
+ int len = list.length; |
+ var result = new List(len); |
+ for (int i = 0; i < len; i++) { |
+ result[i] = _dispatch(list[i]); |
} |
- return _templateElement.createInstance(); |
+ return result; |
} |
+} |
- void _handleChanges(List<ListChangeRecord> splices) { |
- var syntax = TemplateElement.syntax[_templateElement.attributes['syntax']]; |
- |
- for (var splice in splices) { |
- if (splice is! ListChangeRecord) continue; |
+/** Deserializes arrays created with [_Serializer]. */ |
+abstract class _Deserializer { |
+ Map<int, dynamic> _deserialized; |
- for (int i = 0; i < splice.removedCount; i++) { |
- removeInstanceAt(splice.index); |
- } |
+ _Deserializer(); |
- for (var addIndex = splice.index; |
- addIndex < splice.index + splice.addedCount; |
- addIndex++) { |
+ static bool isPrimitive(x) { |
+ return (x == null) || (x is String) || (x is num) || (x is bool); |
+ } |
- var model = getInstanceModel(iteratedValue[addIndex], syntax); |
+ deserialize(x) { |
+ if (isPrimitive(x)) return x; |
+ // TODO(floitsch): this should be new HashMap<int, dynamic>() |
+ _deserialized = new HashMap(); |
+ return _deserializeHelper(x); |
+ } |
- var fragment = getInstanceFragment(syntax); |
+ _deserializeHelper(x) { |
+ if (isPrimitive(x)) return x; |
+ assert(x is List); |
+ switch (x[0]) { |
+ case 'ref': return _deserializeRef(x); |
+ case 'list': return _deserializeList(x); |
+ case 'map': return _deserializeMap(x); |
+ case 'sendport': return deserializeSendPort(x); |
+ default: return deserializeObject(x); |
+ } |
+ } |
- _Bindings._addBindings(fragment, model, syntax); |
- _Bindings._addTemplateInstanceRecord(fragment, model); |
+ _deserializeRef(List x) { |
+ int id = x[1]; |
+ var result = _deserialized[id]; |
+ assert(result != null); |
+ return result; |
+ } |
- insertInstanceAt(addIndex, fragment); |
- } |
+ List _deserializeList(List x) { |
+ int id = x[1]; |
+ // We rely on the fact that Dart-lists are directly mapped to Js-arrays. |
+ List dartList = x[2]; |
+ _deserialized[id] = dartList; |
+ int len = dartList.length; |
+ for (int i = 0; i < len; i++) { |
+ dartList[i] = _deserializeHelper(dartList[i]); |
} |
+ return dartList; |
} |
- void unobserve() { |
- if (_sub == null) return; |
- _sub.cancel(); |
- _sub = null; |
+ Map _deserializeMap(List x) { |
+ Map result = new Map(); |
+ int id = x[1]; |
+ _deserialized[id] = result; |
+ List keys = x[2]; |
+ List values = x[3]; |
+ int len = keys.length; |
+ assert(len == values.length); |
+ for (int i = 0; i < len; i++) { |
+ var key = _deserializeHelper(keys[i]); |
+ var value = _deserializeHelper(values[i]); |
+ result[key] = value; |
+ } |
+ return result; |
} |
- void abandon() { |
- unobserve(); |
- _valueBinding.cancel(); |
- inputs.dispose(); |
+ deserializeSendPort(List x); |
+ |
+ deserializeObject(List x) { |
+ // TODO(floitsch): Use real exception (which one?). |
+ throw "Unexpected serialized object"; |
} |
} |
+ |
// Copyright (c) 2013, 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. |
+// This code is a port of Model-Driven-Views: |
+// https://github.com/polymer-project/mdv |
+// The code mostly comes from src/template_element.js |
+ |
+typedef void _ChangeHandler(value); |
+ |
/** |
- * Helper class to implement custom events which wrap DOM events. |
+ * Model-Driven Views (MDV)'s native features enables a wide-range of use cases, |
+ * but (by design) don't attempt to implement a wide array of specialized |
+ * behaviors. |
+ * |
+ * Enabling these features in MDV is a matter of implementing and registering an |
+ * MDV Custom Syntax. A Custom Syntax is an object which contains one or more |
+ * delegation functions which implement specialized behavior. This object is |
+ * registered with MDV via [TemplateElement.syntax]: |
+ * |
+ * |
+ * HTML: |
+ * <template bind syntax="MySyntax"> |
+ * {{ What!Ever('crazy')->thing^^^I+Want(data) }} |
+ * </template> |
+ * |
+ * Dart: |
+ * class MySyntax extends CustomBindingSyntax { |
+ * getBinding(model, path, name, node) { |
+ * // The magic happens here! |
+ * } |
+ * } |
+ * |
+ * ... |
+ * |
+ * TemplateElement.syntax['MySyntax'] = new MySyntax(); |
+ * |
+ * See <https://github.com/polymer-project/mdv/blob/master/docs/syntax.md> for more |
+ * information about Custom Syntax. |
*/ |
-class _WrappedEvent implements Event { |
- final Event wrapped; |
- _WrappedEvent(this.wrapped); |
+// TODO(jmesserly): if this is just one method, a function type would make it |
+// more Dart-friendly. |
+@Experimental |
+abstract class CustomBindingSyntax { |
+ /** |
+ * This syntax method allows for a custom interpretation of the contents of |
+ * mustaches (`{{` ... `}}`). |
+ * |
+ * When a template is inserting an instance, it will invoke this method for |
+ * each mustache which is encountered. The function is invoked with four |
+ * arguments: |
+ * |
+ * - [model]: The data context for which this instance is being created. |
+ * - [path]: The text contents (trimmed of outer whitespace) of the mustache. |
+ * - [name]: The context in which the mustache occurs. Within element |
+ * attributes, this will be the name of the attribute. Within text, |
+ * this will be 'text'. |
+ * - [node]: A reference to the node to which this binding will be created. |
+ * |
+ * If the method wishes to handle binding, it is required to return an object |
+ * which has at least a `value` property that can be observed. If it does, |
+ * then MDV will call [Node.bind on the node: |
+ * |
+ * node.bind(name, retval, 'value'); |
+ * |
+ * If the 'getBinding' does not wish to override the binding, it should return |
+ * null. |
+ */ |
+ // TODO(jmesserly): I had to remove type annotations from "name" and "node" |
+ // Normally they are String and Node respectively. But sometimes it will pass |
+ // (int name, CompoundBinding node). That seems very confusing; we may want |
+ // to change this API. |
+ getBinding(model, String path, name, node) => null; |
- bool get bubbles => wrapped.bubbles; |
+ /** |
+ * This syntax method allows a syntax to provide an alterate model than the |
+ * one the template would otherwise use when producing an instance. |
+ * |
+ * When a template is about to create an instance, it will invoke this method |
+ * The function is invoked with two arguments: |
+ * |
+ * - [template]: The template element which is about to create and insert an |
+ * instance. |
+ * - [model]: The data context for which this instance is being created. |
+ * |
+ * The template element will always use the return value of `getInstanceModel` |
+ * as the model for the new instance. If the syntax does not wish to override |
+ * the value, it should simply return the `model` value it was passed. |
+ */ |
+ getInstanceModel(Element template, model) => model; |
- bool get cancelBubble => wrapped.bubbles; |
- void set cancelBubble(bool value) { |
- wrapped.cancelBubble = value; |
- } |
+ /** |
+ * This syntax method allows a syntax to provide an alterate expansion of |
+ * the [template] contents. When the template wants to create an instance, |
+ * it will call this method with the template element. |
+ * |
+ * By default this will call `template.createInstance()`. |
+ */ |
+ getInstanceFragment(Element template) => template.createInstance(); |
+} |
- bool get cancelable => wrapped.cancelable; |
+/** The callback used in the [CompoundBinding.combinator] field. */ |
+@Experimental |
+typedef Object CompoundBindingCombinator(Map objects); |
- DataTransfer get clipboardData => wrapped.clipboardData; |
+/** Information about the instantiated template. */ |
+@Experimental |
+class TemplateInstance { |
+ // TODO(rafaelw): firstNode & lastNode should be read-synchronous |
+ // in cases where script has modified the template instance boundary. |
- EventTarget get currentTarget => wrapped.currentTarget; |
+ /** The first node of this template instantiation. */ |
+ final Node firstNode; |
- bool get defaultPrevented => wrapped.defaultPrevented; |
+ /** |
+ * The last node of this template instantiation. |
+ * This could be identical to [firstNode] if the template only expanded to a |
+ * single node. |
+ */ |
+ final Node lastNode; |
- int get eventPhase => wrapped.eventPhase; |
+ /** The model used to instantiate the template. */ |
+ final model; |
- EventTarget get target => wrapped.target; |
+ TemplateInstance(this.firstNode, this.lastNode, this.model); |
+} |
- int get timeStamp => wrapped.timeStamp; |
+/** |
+ * Model-Driven Views contains a helper object which is useful for the |
+ * implementation of a Custom Syntax. |
+ * |
+ * var binding = new CompoundBinding((values) { |
+ * var combinedValue; |
+ * // compute combinedValue based on the current values which are provided |
+ * return combinedValue; |
+ * }); |
+ * binding.bind('name1', obj1, path1); |
+ * binding.bind('name2', obj2, path2); |
+ * //... |
+ * binding.bind('nameN', objN, pathN); |
+ * |
+ * CompoundBinding is an object which knows how to listen to multiple path |
+ * values (registered via [bind]) and invoke its [combinator] when one or more |
+ * of the values have changed and set its [value] property to the return value |
+ * of the function. When any value has changed, all current values are provided |
+ * to the [combinator] in the single `values` argument. |
+ * |
+ * See [CustomBindingSyntax] for more information. |
+ */ |
+// TODO(jmesserly): what is the public API surface here? I just guessed; |
+// most of it seemed non-public. |
+@Experimental |
+class CompoundBinding extends ObservableBase { |
+ CompoundBindingCombinator _combinator; |
- String get type => wrapped.type; |
+ // TODO(jmesserly): ideally these would be String keys, but sometimes we |
+ // use integers. |
+ Map<dynamic, StreamSubscription> _bindings = new Map(); |
+ Map _values = new Map(); |
+ bool _scheduled = false; |
+ bool _disposed = false; |
+ Object _value; |
- void $dom_initEvent(String eventTypeArg, bool canBubbleArg, |
- bool cancelableArg) { |
- throw new UnsupportedError( |
- 'Cannot initialize this Event.'); |
+ CompoundBinding([CompoundBindingCombinator combinator]) { |
+ // TODO(jmesserly): this is a tweak to the original code, it seemed to me |
+ // that passing the combinator to the constructor should be equivalent to |
+ // setting it via the property. |
+ // I also added a null check to the combinator setter. |
+ this.combinator = combinator; |
} |
- void preventDefault() { |
- wrapped.preventDefault(); |
+ CompoundBindingCombinator get combinator => _combinator; |
+ |
+ set combinator(CompoundBindingCombinator combinator) { |
+ _combinator = combinator; |
+ if (combinator != null) _scheduleResolve(); |
} |
- void stopImmediatePropagation() { |
- wrapped.stopImmediatePropagation(); |
+ static const _VALUE = const Symbol('value'); |
+ |
+ get value => _value; |
+ |
+ void set value(newValue) { |
+ _value = notifyPropertyChange(_VALUE, _value, newValue); |
} |
- void stopPropagation() { |
- wrapped.stopPropagation(); |
+ // TODO(jmesserly): remove these workarounds when dart2js supports mirrors! |
+ getValueWorkaround(key) { |
+ if (key == _VALUE) return value; |
+ return null; |
+ } |
+ setValueWorkaround(key, val) { |
+ if (key == _VALUE) value = val; |
+ } |
+ |
+ void bind(name, model, String path) { |
+ unbind(name); |
+ |
+ _bindings[name] = new PathObserver(model, path).bindSync((value) { |
+ _values[name] = value; |
+ _scheduleResolve(); |
+ }); |
} |
-} |
-// Copyright (c) 2013, 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. |
+ void unbind(name, {bool suppressResolve: false}) { |
+ var binding = _bindings.remove(name); |
+ if (binding == null) return; |
+ |
+ binding.cancel(); |
+ _values.remove(name); |
+ if (!suppressResolve) _scheduleResolve(); |
+ } |
+ |
+ // TODO(rafaelw): Is this the right processing model? |
+ // TODO(rafaelw): Consider having a seperate ChangeSummary for |
+ // CompoundBindings so to excess dirtyChecks. |
+ void _scheduleResolve() { |
+ if (_scheduled) return; |
+ _scheduled = true; |
+ queueChangeRecords(resolve); |
+ } |
-/** |
- * A list which just wraps another list, for either intercepting list calls or |
- * retyping the list (for example, from List<A> to List<B> where B extends A). |
- */ |
-class _WrappedList<E> extends ListBase<E> { |
- final List _list; |
+ void resolve() { |
+ if (_disposed) return; |
+ _scheduled = false; |
- _WrappedList(this._list); |
+ if (_combinator == null) { |
+ throw new StateError( |
+ 'CompoundBinding attempted to resolve without a combinator'); |
+ } |
- // Iterable APIs |
+ value = _combinator(_values); |
+ } |
- Iterator<E> get iterator => new _WrappedIterator(_list.iterator); |
+ void dispose() { |
+ for (var binding in _bindings.values) { |
+ binding.cancel(); |
+ } |
+ _bindings.clear(); |
+ _values.clear(); |
- int get length => _list.length; |
+ _disposed = true; |
+ value = null; |
+ } |
+} |
- // Collection APIs |
+abstract class _InputBinding { |
+ final InputElement element; |
+ PathObserver binding; |
+ StreamSubscription _pathSub; |
+ StreamSubscription _eventSub; |
- void add(E element) { _list.add(element); } |
+ _InputBinding(this.element, model, String path) { |
+ binding = new PathObserver(model, path); |
+ _pathSub = binding.bindSync(valueChanged); |
+ _eventSub = _getStreamForInputType(element).listen(updateBinding); |
+ } |
- bool remove(Object element) => _list.remove(element); |
+ void valueChanged(newValue); |
- void clear() { _list.clear(); } |
+ void updateBinding(e); |
- // List APIs |
+ void unbind() { |
+ binding = null; |
+ _pathSub.cancel(); |
+ _eventSub.cancel(); |
+ } |
- E operator [](int index) => _list[index]; |
- void operator []=(int index, E value) { _list[index] = value; } |
+ static Stream<Event> _getStreamForInputType(InputElement element) { |
+ switch (element.type) { |
+ case 'checkbox': |
+ return element.onClick; |
+ case 'radio': |
+ case 'select-multiple': |
+ case 'select-one': |
+ return element.onChange; |
+ default: |
+ return element.onInput; |
+ } |
+ } |
+} |
- void set length(int newLength) { _list.length = newLength; } |
+class _ValueBinding extends _InputBinding { |
+ _ValueBinding(element, model, path) : super(element, model, path); |
- void sort([int compare(E a, E b)]) { _list.sort(compare); } |
+ void valueChanged(value) { |
+ element.value = value == null ? '' : '$value'; |
+ } |
- int indexOf(E element, [int start = 0]) => _list.indexOf(element, start); |
+ void updateBinding(e) { |
+ binding.value = element.value; |
+ } |
+} |
- int lastIndexOf(E element, [int start]) => _list.lastIndexOf(element, start); |
+class _CheckedBinding extends _InputBinding { |
+ _CheckedBinding(element, model, path) : super(element, model, path); |
- void insert(int index, E element) => _list.insert(index, element); |
+ void valueChanged(value) { |
+ element.checked = _Bindings._toBoolean(value); |
+ } |
- E removeAt(int index) => _list.removeAt(index); |
+ void updateBinding(e) { |
+ binding.value = element.checked; |
- void setRange(int start, int end, Iterable<E> iterable, [int skipCount = 0]) { |
- _list.setRange(start, end, iterable, skipCount); |
+ // Only the radio button that is getting checked gets an event. We |
+ // therefore find all the associated radio buttons and update their |
+ // CheckedBinding manually. |
+ if (element is InputElement && element.type == 'radio') { |
+ for (var r in _getAssociatedRadioButtons(element)) { |
+ var checkedBinding = r._checkedBinding; |
+ if (checkedBinding != null) { |
+ // Set the value directly to avoid an infinite call stack. |
+ checkedBinding.binding.value = false; |
+ } |
+ } |
+ } |
} |
- void removeRange(int start, int end) { _list.removeRange(start, end); } |
- |
- void replaceRange(int start, int end, Iterable<E> iterable) { |
- _list.replaceRange(start, end, iterable); |
+ // |element| is assumed to be an HTMLInputElement with |type| == 'radio'. |
+ // Returns an array containing all radio buttons other than |element| that |
+ // have the same |name|, either in the form that |element| belongs to or, |
+ // if no form, in the document tree to which |element| belongs. |
+ // |
+ // This implementation is based upon the HTML spec definition of a |
+ // "radio button group": |
+ // http://www.whatwg.org/specs/web-apps/current-work/multipage/number-state.html#radio-button-group |
+ // |
+ static Iterable _getAssociatedRadioButtons(element) { |
+ if (!_isNodeInDocument(element)) return []; |
+ if (element.form != null) { |
+ return element.form.nodes.where((el) { |
+ return el != element && |
+ el is InputElement && |
+ el.type == 'radio' && |
+ el.name == element.name; |
+ }); |
+ } else { |
+ var radios = element.document.queryAll( |
+ 'input[type="radio"][name="${element.name}"]'); |
+ return radios.where((el) => el != element && el.form == null); |
+ } |
} |
- void fillRange(int start, int end, [E fillValue]) { |
- _list.fillRange(start, end, fillValue); |
+ // TODO(jmesserly): polyfill document.contains API instead of doing it here |
+ static bool _isNodeInDocument(Node node) { |
+ // On non-IE this works: |
+ // return node.document.contains(node); |
+ var document = node.document; |
+ if (node == document || node.parentNode == document) return true; |
+ return document.documentElement.contains(node); |
} |
} |
-/** |
- * Iterator wrapper for _WrappedList. |
- */ |
-class _WrappedIterator<E> implements Iterator<E> { |
- Iterator _iterator; |
+class _Bindings { |
+ // TODO(jmesserly): not sure what kind of boolean conversion rules to |
+ // apply for template data-binding. HTML attributes are true if they're |
+ // present. However Dart only treats "true" as true. Since this is HTML we'll |
+ // use something closer to the HTML rules: null (missing) and false are false, |
+ // everything else is true. See: https://github.com/polymer-project/mdv/issues/59 |
+ static bool _toBoolean(value) => null != value && false != value; |
- _WrappedIterator(this._iterator); |
+ static Node _createDeepCloneAndDecorateTemplates(Node node, String syntax) { |
+ var clone = node.clone(false); // Shallow clone. |
+ if (clone is Element && clone.isTemplate) { |
+ TemplateElement.decorate(clone, node); |
+ if (syntax != null) { |
+ clone.attributes.putIfAbsent('syntax', () => syntax); |
+ } |
+ } |
- bool moveNext() { |
- return _iterator.moveNext(); |
+ for (var c = node.$dom_firstChild; c != null; c = c.nextNode) { |
+ clone.append(_createDeepCloneAndDecorateTemplates(c, syntax)); |
+ } |
+ return clone; |
} |
- E get current => _iterator.current; |
-} |
-// 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. |
+ // http://dvcs.w3.org/hg/webcomponents/raw-file/tip/spec/templates/index.html#dfn-template-contents-owner |
+ static Document _getTemplateContentsOwner(HtmlDocument doc) { |
+ if (doc.window == null) { |
+ return doc; |
+ } |
+ var d = doc._templateContentsOwner; |
+ if (d == null) { |
+ // TODO(arv): This should either be a Document or HTMLDocument depending |
+ // on doc. |
+ d = doc.implementation.createHtmlDocument(''); |
+ while (d.$dom_lastChild != null) { |
+ d.$dom_lastChild.remove(); |
+ } |
+ doc._templateContentsOwner = d; |
+ } |
+ return d; |
+ } |
+ |
+ static Element _cloneAndSeperateAttributeTemplate(Element templateElement) { |
+ var clone = templateElement.clone(false); |
+ var attributes = templateElement.attributes; |
+ for (var name in attributes.keys.toList()) { |
+ switch (name) { |
+ case 'template': |
+ case 'repeat': |
+ case 'bind': |
+ case 'ref': |
+ clone.attributes.remove(name); |
+ break; |
+ default: |
+ attributes.remove(name); |
+ break; |
+ } |
+ } |
+ return clone; |
+ } |
-class _HttpRequestUtils { |
+ static void _liftNonNativeChildrenIntoContent(Element templateElement) { |
+ var content = templateElement.content; |
- // Helper for factory HttpRequest.get |
- static HttpRequest get(String url, |
- onComplete(HttpRequest request), |
- bool withCredentials) { |
- final request = new HttpRequest(); |
- request.open('GET', url, async: true); |
+ if (!templateElement._isAttributeTemplate) { |
+ var child; |
+ while ((child = templateElement.$dom_firstChild) != null) { |
+ content.append(child); |
+ } |
+ return; |
+ } |
- request.withCredentials = withCredentials; |
+ // For attribute templates we copy the whole thing into the content and |
+ // we move the non template attributes into the content. |
+ // |
+ // <tr foo template> |
+ // |
+ // becomes |
+ // |
+ // <tr template> |
+ // + #document-fragment |
+ // + <tr foo> |
+ // |
+ var newRoot = _cloneAndSeperateAttributeTemplate(templateElement); |
+ var child; |
+ while ((child = templateElement.$dom_firstChild) != null) { |
+ newRoot.append(child); |
+ } |
+ content.append(newRoot); |
+ } |
- request.onReadyStateChange.listen((e) { |
- if (request.readyState == HttpRequest.DONE) { |
- onComplete(request); |
+ static void _bootstrapTemplatesRecursivelyFrom(Node node) { |
+ void bootstrap(template) { |
+ if (!TemplateElement.decorate(template)) { |
+ _bootstrapTemplatesRecursivelyFrom(template.content); |
} |
- }); |
+ } |
- request.send(); |
+ // Need to do this first as the contents may get lifted if |node| is |
+ // template. |
+ // TODO(jmesserly): node is DocumentFragment or Element |
+ var descendents = (node as dynamic).queryAll(_allTemplatesSelectors); |
+ if (node is Element && (node as Element).isTemplate) bootstrap(node); |
- return request; |
+ descendents.forEach(bootstrap); |
} |
-} |
-/** |
- * A custom KeyboardEvent that attempts to eliminate cross-browser |
- * inconsistencies, and also provide both keyCode and charCode information |
- * for all key events (when such information can be determined). |
- * |
- * KeyEvent tries to provide a higher level, more polished keyboard event |
- * information on top of the "raw" [KeyboardEvent]. |
- * |
- * This class is very much a work in progress, and we'd love to get information |
- * on how we can make this class work with as many international keyboards as |
- * possible. Bugs welcome! |
- */ |
-class KeyEvent extends _WrappedEvent implements KeyboardEvent { |
- /** The parent KeyboardEvent that this KeyEvent is wrapping and "fixing". */ |
- KeyboardEvent _parent; |
+ static final String _allTemplatesSelectors = 'template, option[template], ' + |
+ Element._TABLE_TAGS.keys.map((k) => "$k[template]").join(", "); |
- /** The "fixed" value of whether the alt key is being pressed. */ |
- bool _shadowAltKey; |
+ static void _addBindings(Node node, model, [CustomBindingSyntax syntax]) { |
+ if (node is Element) { |
+ _addAttributeBindings(node, model, syntax); |
+ } else if (node is Text) { |
+ _parseAndBind(node, 'text', node.text, model, syntax); |
+ } |
- /** Caculated value of what the estimated charCode is for this event. */ |
- int _shadowCharCode; |
+ for (var c = node.$dom_firstChild; c != null; c = c.nextNode) { |
+ _addBindings(c, model, syntax); |
+ } |
+ } |
- /** Caculated value of what the estimated keyCode is for this event. */ |
- int _shadowKeyCode; |
+ static void _addAttributeBindings(Element element, model, syntax) { |
+ element.attributes.forEach((name, value) { |
+ if (value == '' && (name == 'bind' || name == 'repeat')) { |
+ value = '{{}}'; |
+ } |
+ _parseAndBind(element, name, value, model, syntax); |
+ }); |
+ } |
- /** Caculated value of what the estimated keyCode is for this event. */ |
- int get keyCode => _shadowKeyCode; |
+ static void _parseAndBind(Node node, String name, String text, model, |
+ CustomBindingSyntax syntax) { |
- /** Caculated value of what the estimated charCode is for this event. */ |
- int get charCode => this.type == 'keypress' ? _shadowCharCode : 0; |
+ var tokens = _parseMustacheTokens(text); |
+ if (tokens.length == 0 || (tokens.length == 1 && tokens[0].isText)) { |
+ return; |
+ } |
- /** Caculated value of whether the alt key is pressed is for this event. */ |
- bool get altKey => _shadowAltKey; |
+ // If this is a custom element, give the .xtag a change to bind. |
+ node = _nodeOrCustom(node); |
- /** Caculated value of what the estimated keyCode is for this event. */ |
- int get which => keyCode; |
+ if (tokens.length == 1 && tokens[0].isBinding) { |
+ _bindOrDelegate(node, name, model, tokens[0].value, syntax); |
+ return; |
+ } |
- /** Accessor to the underlying keyCode value is the parent event. */ |
- int get _realKeyCode => _parent.keyCode; |
+ var replacementBinding = new CompoundBinding(); |
+ for (var i = 0; i < tokens.length; i++) { |
+ var token = tokens[i]; |
+ if (token.isBinding) { |
+ _bindOrDelegate(replacementBinding, i, model, token.value, syntax); |
+ } |
+ } |
- /** Accessor to the underlying charCode value is the parent event. */ |
- int get _realCharCode => _parent.charCode; |
+ replacementBinding.combinator = (values) { |
+ var newValue = new StringBuffer(); |
- /** Accessor to the underlying altKey value is the parent event. */ |
- bool get _realAltKey => _parent.altKey; |
+ for (var i = 0; i < tokens.length; i++) { |
+ var token = tokens[i]; |
+ if (token.isText) { |
+ newValue.write(token.value); |
+ } else { |
+ var value = values[i]; |
+ if (value != null) { |
+ newValue.write(value); |
+ } |
+ } |
+ } |
- /** Construct a KeyEvent with [parent] as the event we're emulating. */ |
- KeyEvent(KeyboardEvent parent): super(parent) { |
- _parent = parent; |
- _shadowAltKey = _realAltKey; |
- _shadowCharCode = _realCharCode; |
- _shadowKeyCode = _realKeyCode; |
+ return newValue.toString(); |
+ }; |
+ |
+ node.bind(name, replacementBinding, 'value'); |
} |
- /** Accessor to provide a stream of KeyEvents on the desired target. */ |
- static EventStreamProvider<KeyEvent> keyDownEvent = |
- new _KeyboardEventHandler('keydown'); |
- /** Accessor to provide a stream of KeyEvents on the desired target. */ |
- static EventStreamProvider<KeyEvent> keyUpEvent = |
- new _KeyboardEventHandler('keyup'); |
- /** Accessor to provide a stream of KeyEvents on the desired target. */ |
- static EventStreamProvider<KeyEvent> keyPressEvent = |
- new _KeyboardEventHandler('keypress'); |
+ static void _bindOrDelegate(node, name, model, String path, |
+ CustomBindingSyntax syntax) { |
- /** True if the altGraphKey is pressed during this event. */ |
- bool get altGraphKey => _parent.altGraphKey; |
- /** Accessor to the clipboardData available for this event. */ |
- DataTransfer get clipboardData => _parent.clipboardData; |
- /** True if the ctrl key is pressed during this event. */ |
- bool get ctrlKey => _parent.ctrlKey; |
- int get detail => _parent.detail; |
- /** |
- * Accessor to the part of the keyboard that the key was pressed from (one of |
- * KeyLocation.STANDARD, KeyLocation.RIGHT, KeyLocation.LEFT, |
- * KeyLocation.NUMPAD, KeyLocation.MOBILE, KeyLocation.JOYSTICK). |
- */ |
- int get keyLocation => _parent.keyLocation; |
- Point get layer => _parent.layer; |
- /** True if the Meta (or Mac command) key is pressed during this event. */ |
- bool get metaKey => _parent.metaKey; |
- Point get page => _parent.page; |
- /** True if the shift key was pressed during this event. */ |
- bool get shiftKey => _parent.shiftKey; |
- Window get view => _parent.view; |
- void $dom_initUIEvent(String type, bool canBubble, bool cancelable, |
- Window view, int detail) { |
- throw new UnsupportedError("Cannot initialize a UI Event from a KeyEvent."); |
- } |
- String get _shadowKeyIdentifier => _parent.$dom_keyIdentifier; |
+ if (syntax != null) { |
+ var delegateBinding = syntax.getBinding(model, path, name, node); |
+ if (delegateBinding != null) { |
+ model = delegateBinding; |
+ path = 'value'; |
+ } |
+ } |
- int get $dom_charCode => charCode; |
- int get $dom_keyCode => keyCode; |
- String get $dom_keyIdentifier { |
- throw new UnsupportedError("keyIdentifier is unsupported."); |
- } |
- void $dom_initKeyboardEvent(String type, bool canBubble, bool cancelable, |
- Window view, String keyIdentifier, int keyLocation, bool ctrlKey, |
- bool altKey, bool shiftKey, bool metaKey, |
- bool altGraphKey) { |
- throw new UnsupportedError( |
- "Cannot initialize a KeyboardEvent from a KeyEvent."); |
+ node.bind(name, model, path); |
} |
-} |
-// Copyright (c) 2013, 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. |
- |
-class Platform { |
/** |
- * Returns true if dart:typed_data types are supported on this |
- * browser. If false, using these types will generate a runtime |
- * error. |
+ * Gets the [node]'s custom [Element.xtag] if present, otherwise returns |
+ * the node. This is used so nodes can override [Node.bind], [Node.unbind], |
+ * and [Node.unbindAll] like InputElement does. |
*/ |
- static final supportsTypedData = true; |
+ // TODO(jmesserly): remove this when we can extend Element for real. |
+ static _nodeOrCustom(node) => node is Element ? node.xtag : node; |
- /** |
- * Returns true if SIMD types in dart:typed_data types are supported |
- * on this browser. If false, using these types will generate a runtime |
- * error. |
- */ |
- static final supportsSimd = true; |
-} |
-// 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. |
+ static List<_BindingToken> _parseMustacheTokens(String s) { |
+ var result = []; |
+ var length = s.length; |
+ var index = 0, lastIndex = 0; |
+ while (lastIndex < length) { |
+ index = s.indexOf('{{', lastIndex); |
+ if (index < 0) { |
+ result.add(new _BindingToken(s.substring(lastIndex))); |
+ break; |
+ } else { |
+ // There is a non-empty text run before the next path token. |
+ if (index > 0 && lastIndex < index) { |
+ result.add(new _BindingToken(s.substring(lastIndex, index))); |
+ } |
+ lastIndex = index + 2; |
+ index = s.indexOf('}}', lastIndex); |
+ if (index < 0) { |
+ var text = s.substring(lastIndex - 2); |
+ if (result.length > 0 && result.last.isText) { |
+ result.last.value += text; |
+ } else { |
+ result.add(new _BindingToken(text)); |
+ } |
+ break; |
+ } |
+ var value = s.substring(lastIndex, index).trim(); |
+ result.add(new _BindingToken(value, isBinding: true)); |
+ lastIndex = index + 2; |
+ } |
+ } |
+ return result; |
+ } |
-_serialize(var message) { |
- return new _JsSerializer().traverse(message); |
-} |
+ static void _addTemplateInstanceRecord(fragment, model) { |
+ if (fragment.$dom_firstChild == null) { |
+ return; |
+ } |
-class _JsSerializer extends _Serializer { |
+ var instanceRecord = new TemplateInstance( |
+ fragment.$dom_firstChild, fragment.$dom_lastChild, model); |
- visitSendPortSync(SendPortSync x) { |
- if (x is _JsSendPortSync) return visitJsSendPortSync(x); |
- if (x is _LocalSendPortSync) return visitLocalSendPortSync(x); |
- if (x is _RemoteSendPortSync) return visitRemoteSendPortSync(x); |
- throw "Unknown port type $x"; |
+ var node = instanceRecord.firstNode; |
+ while (node != null) { |
+ node._templateInstance = instanceRecord; |
+ node = node.nextNode; |
+ } |
} |
- visitJsSendPortSync(_JsSendPortSync x) { |
- return [ 'sendport', 'nativejs', x._id ]; |
+ static void _removeAllBindingsRecursively(Node node) { |
+ _nodeOrCustom(node).unbindAll(); |
+ for (var c = node.$dom_firstChild; c != null; c = c.nextNode) { |
+ _removeAllBindingsRecursively(c); |
+ } |
} |
- visitLocalSendPortSync(_LocalSendPortSync x) { |
- return [ 'sendport', 'dart', |
- ReceivePortSync._isolateId, x._receivePort._portId ]; |
+ static void _removeChild(Node parent, Node child) { |
+ child._templateInstance = null; |
+ if (child is Element && (child as Element).isTemplate) { |
+ Element childElement = child; |
+ // Make sure we stop observing when we remove an element. |
+ var templateIterator = childElement._templateIterator; |
+ if (templateIterator != null) { |
+ templateIterator.abandon(); |
+ childElement._templateIterator = null; |
+ } |
+ } |
+ child.remove(); |
+ _removeAllBindingsRecursively(child); |
} |
+} |
- visitSendPort(SendPort x) { |
- throw new UnimplementedError('Asynchronous send port not yet implemented.'); |
- } |
+class _BindingToken { |
+ final String value; |
+ final bool isBinding; |
- visitRemoteSendPortSync(_RemoteSendPortSync x) { |
- return [ 'sendport', 'dart', x._isolateId, x._portId ]; |
- } |
-} |
+ _BindingToken(this.value, {this.isBinding: false}); |
-_deserialize(var message) { |
- return new _JsDeserializer().deserialize(message); |
+ bool get isText => !isBinding; |
} |
+class _TemplateIterator { |
+ final Element _templateElement; |
+ final List<Node> terminators = []; |
+ final CompoundBinding inputs; |
+ List iteratedValue; |
-class _JsDeserializer extends _Deserializer { |
+ StreamSubscription _sub; |
+ StreamSubscription _valueBinding; |
- static const _UNSPECIFIED = const Object(); |
+ _TemplateIterator(this._templateElement) |
+ : inputs = new CompoundBinding(resolveInputs) { |
- deserializeSendPort(List x) { |
- String tag = x[1]; |
- switch (tag) { |
- case 'nativejs': |
- num id = x[2]; |
- return new _JsSendPortSync(id); |
- case 'dart': |
- num isolateId = x[2]; |
- num portId = x[3]; |
- return ReceivePortSync._lookup(isolateId, portId); |
- default: |
- throw 'Illegal SendPortSync type: $tag'; |
- } |
+ _valueBinding = new PathObserver(inputs, 'value').bindSync(valueChanged); |
} |
-} |
-// The receiver is JS. |
-class _JsSendPortSync implements SendPortSync { |
+ static Object resolveInputs(Map values) { |
+ if (values.containsKey('if') && !_Bindings._toBoolean(values['if'])) { |
+ return null; |
+ } |
- final num _id; |
- _JsSendPortSync(this._id); |
+ if (values.containsKey('repeat')) { |
+ return values['repeat']; |
+ } |
- callSync(var message) { |
- var serialized = _serialize(message); |
- var result = _callPortSync(_id, serialized); |
- return _deserialize(result); |
- } |
+ if (values.containsKey('bind')) { |
+ return [values['bind']]; |
+ } |
- bool operator==(var other) { |
- return (other is _JsSendPortSync) && (_id == other._id); |
+ return null; |
} |
- int get hashCode => _id; |
-} |
+ void valueChanged(value) { |
+ clear(); |
+ if (value is! List) return; |
-// TODO(vsm): Differentiate between Dart2Js and Dartium isolates. |
-// The receiver is a different Dart isolate, compiled to JS. |
-class _RemoteSendPortSync implements SendPortSync { |
+ iteratedValue = value; |
- int _isolateId; |
- int _portId; |
- _RemoteSendPortSync(this._isolateId, this._portId); |
+ if (value is Observable) { |
+ _sub = value.changes.listen(_handleChanges); |
+ } |
- callSync(var message) { |
- var serialized = _serialize(message); |
- var result = _call(_isolateId, _portId, serialized); |
- return _deserialize(result); |
+ int len = iteratedValue.length; |
+ if (len > 0) { |
+ _handleChanges([new ListChangeRecord(0, addedCount: len)]); |
+ } |
} |
- static _call(int isolateId, int portId, var message) { |
- var target = 'dart-port-$isolateId-$portId'; |
- // TODO(vsm): Make this re-entrant. |
- // TODO(vsm): Set this up set once, on the first call. |
- var source = '$target-result'; |
- var result = null; |
- window.on[source].first.then((Event e) { |
- result = json.parse(_getPortSyncEventData(e)); |
- }); |
- _dispatchEvent(target, [source, message]); |
- return result; |
+ Node getTerminatorAt(int index) { |
+ if (index == -1) return _templateElement; |
+ var terminator = terminators[index]; |
+ if (terminator is! Element) return terminator; |
+ |
+ var subIterator = terminator._templateIterator; |
+ if (subIterator == null) return terminator; |
+ |
+ return subIterator.getTerminatorAt(subIterator.terminators.length - 1); |
} |
- bool operator==(var other) { |
- return (other is _RemoteSendPortSync) && (_isolateId == other._isolateId) |
- && (_portId == other._portId); |
+ void insertInstanceAt(int index, Node fragment) { |
+ var previousTerminator = getTerminatorAt(index - 1); |
+ var terminator = fragment.$dom_lastChild; |
+ if (terminator == null) terminator = previousTerminator; |
+ |
+ terminators.insert(index, terminator); |
+ var parent = _templateElement.parentNode; |
+ parent.insertBefore(fragment, previousTerminator.nextNode); |
} |
- int get hashCode => _isolateId >> 16 + _portId; |
-} |
+ void removeInstanceAt(int index) { |
+ var previousTerminator = getTerminatorAt(index - 1); |
+ var terminator = getTerminatorAt(index); |
+ terminators.removeAt(index); |
-// The receiver is in the same Dart isolate, compiled to JS. |
-class _LocalSendPortSync implements SendPortSync { |
+ var parent = _templateElement.parentNode; |
+ while (terminator != previousTerminator) { |
+ var node = terminator; |
+ terminator = node.previousNode; |
+ _Bindings._removeChild(parent, node); |
+ } |
+ } |
- ReceivePortSync _receivePort; |
+ void removeAllInstances() { |
+ if (terminators.length == 0) return; |
- _LocalSendPortSync._internal(this._receivePort); |
+ var previousTerminator = _templateElement; |
+ var terminator = getTerminatorAt(terminators.length - 1); |
+ terminators.length = 0; |
- callSync(var message) { |
- // TODO(vsm): Do a more efficient deep copy. |
- var copy = _deserialize(_serialize(message)); |
- var result = _receivePort._callback(copy); |
- return _deserialize(_serialize(result)); |
+ var parent = _templateElement.parentNode; |
+ while (terminator != previousTerminator) { |
+ var node = terminator; |
+ terminator = node.previousNode; |
+ _Bindings._removeChild(parent, node); |
+ } |
} |
- bool operator==(var other) { |
- return (other is _LocalSendPortSync) |
- && (_receivePort == other._receivePort); |
+ void clear() { |
+ unobserve(); |
+ removeAllInstances(); |
+ iteratedValue = null; |
} |
- int get hashCode => _receivePort.hashCode; |
-} |
+ getInstanceModel(model, syntax) { |
+ if (syntax != null) { |
+ return syntax.getInstanceModel(_templateElement, model); |
+ } |
+ return model; |
+ } |
-// TODO(vsm): Move this to dart:isolate. This will take some |
-// refactoring as there are dependences here on the DOM. Users |
-// interact with this class (or interface if we change it) directly - |
-// new ReceivePortSync. I think most of the DOM logic could be |
-// delayed until the corresponding SendPort is registered on the |
-// window. |
+ getInstanceFragment(syntax) { |
+ if (syntax != null) { |
+ return syntax.getInstanceFragment(_templateElement); |
+ } |
+ return _templateElement.createInstance(); |
+ } |
-// A Dart ReceivePortSync (tagged 'dart' when serialized) is |
-// identifiable / resolvable by the combination of its isolateid and |
-// portid. When a corresponding SendPort is used within the same |
-// isolate, the _portMap below can be used to obtain the |
-// ReceivePortSync directly. Across isolates (or from JS), an |
-// EventListener can be used to communicate with the port indirectly. |
-class ReceivePortSync { |
+ void _handleChanges(List<ListChangeRecord> splices) { |
+ var syntax = TemplateElement.syntax[_templateElement.attributes['syntax']]; |
- static Map<int, ReceivePortSync> _portMap; |
- static int _portIdCount; |
- static int _cachedIsolateId; |
+ for (var splice in splices) { |
+ if (splice is! ListChangeRecord) continue; |
- num _portId; |
- Function _callback; |
- StreamSubscription _portSubscription; |
+ for (int i = 0; i < splice.removedCount; i++) { |
+ removeInstanceAt(splice.index); |
+ } |
- ReceivePortSync() { |
- if (_portIdCount == null) { |
- _portIdCount = 0; |
- _portMap = new Map<int, ReceivePortSync>(); |
- } |
- _portId = _portIdCount++; |
- _portMap[_portId] = this; |
- } |
+ for (var addIndex = splice.index; |
+ addIndex < splice.index + splice.addedCount; |
+ addIndex++) { |
- static int get _isolateId { |
- // TODO(vsm): Make this coherent with existing isolate code. |
- if (_cachedIsolateId == null) { |
- _cachedIsolateId = _getNewIsolateId(); |
- } |
- return _cachedIsolateId; |
- } |
+ var model = getInstanceModel(iteratedValue[addIndex], syntax); |
- static String _getListenerName(isolateId, portId) => |
- 'dart-port-$isolateId-$portId'; |
- String get _listenerName => _getListenerName(_isolateId, _portId); |
+ var fragment = getInstanceFragment(syntax); |
- void receive(callback(var message)) { |
- _callback = callback; |
- if (_portSubscription == null) { |
- _portSubscription = window.on[_listenerName].listen((Event e) { |
- var data = json.parse(_getPortSyncEventData(e)); |
- var replyTo = data[0]; |
- var message = _deserialize(data[1]); |
- var result = _callback(message); |
- _dispatchEvent(replyTo, _serialize(result)); |
- }); |
- } |
- } |
+ _Bindings._addBindings(fragment, model, syntax); |
+ _Bindings._addTemplateInstanceRecord(fragment, model); |
- void close() { |
- _portMap.remove(_portId); |
- if (_portSubscription != null) _portSubscription.cancel(); |
+ insertInstanceAt(addIndex, fragment); |
+ } |
+ } |
} |
- SendPortSync toSendPort() { |
- return new _LocalSendPortSync._internal(this); |
+ void unobserve() { |
+ if (_sub == null) return; |
+ _sub.cancel(); |
+ _sub = null; |
} |
- static SendPortSync _lookup(int isolateId, int portId) { |
- if (isolateId == _isolateId) { |
- return _portMap[portId].toSendPort(); |
- } else { |
- return new _RemoteSendPortSync(isolateId, portId); |
- } |
+ void abandon() { |
+ unobserve(); |
+ _valueBinding.cancel(); |
+ inputs.dispose(); |
} |
} |
- |
-get _isolateId => ReceivePortSync._isolateId; |
- |
-void _dispatchEvent(String receiver, var message) { |
- var event = new CustomEvent(receiver, canBubble: false, cancelable:false, |
- detail: json.stringify(message)); |
- window.dispatchEvent(event); |
-} |
- |
-String _getPortSyncEventData(CustomEvent event) => event.detail; |
-// Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file |
+// Copyright (c) 2013, 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. |
-typedef void _MicrotaskCallback(); |
/** |
- * This class attempts to invoke a callback as soon as the current event stack |
- * unwinds, but before the browser repaints. |
+ * Interface used to validate that only accepted elements and attributes are |
+ * allowed while parsing HTML strings into DOM nodes. |
*/ |
-abstract class _MicrotaskScheduler { |
- bool _nextMicrotaskFrameScheduled = false; |
- final _MicrotaskCallback _callback; |
+abstract class NodeValidator { |
- _MicrotaskScheduler(this._callback); |
+ /** |
+ * Construct a default NodeValidator which only accepts whitelisted HTML5 |
+ * elements and attributes. |
+ * |
+ * If a uriPolicy is not specified then the default uriPolicy will be used. |
+ */ |
+ factory NodeValidator({UriPolicy uriPolicy}) => |
+ new _Html5NodeValidator(uriPolicy: uriPolicy); |
/** |
- * Creates the best possible microtask scheduler for the current platform. |
+ * Returns true if the tagName is an accepted type. |
*/ |
- factory _MicrotaskScheduler.best(_MicrotaskCallback callback) { |
- if (Window._supportsSetImmediate) { |
- return new _SetImmediateScheduler(callback); |
- } else if (MutationObserver.supported) { |
- return new _MutationObserverScheduler(callback); |
- } |
- return new _PostMessageScheduler(callback); |
- } |
+ bool allowsElement(Element element); |
/** |
- * Schedules a microtask callback if one has not been scheduled already. |
+ * Returns true if the attribute is allowed. |
+ * |
+ * The attributeName parameter will always be in lowercase. |
+ * |
+ * See [allowsElement] for format of tagName. |
*/ |
- void maybeSchedule() { |
- if (this._nextMicrotaskFrameScheduled) { |
- return; |
- } |
- this._nextMicrotaskFrameScheduled = true; |
- this._schedule(); |
- } |
+ bool allowsAttribute(Element element, String attributeName, String value); |
+} |
+ |
+/** |
+ * Performs sanitization of a node tree after construction to ensure that it |
+ * does not contain any disallowed elements or attributes. |
+ * |
+ * In general custom implementations of this class should not be necessary and |
+ * all validation customization should be done in custom NodeValidators, but |
+ * custom implementations of this class can be created to perform more complex |
+ * tree sanitization. |
+ */ |
+abstract class NodeTreeSanitizer { |
/** |
- * Does the actual scheduling of the callback. |
+ * Constructs a default tree sanitizer which will remove all elements and |
+ * attributes which are not allowed by the provided validator. |
*/ |
- void _schedule(); |
+ factory NodeTreeSanitizer(NodeValidator validator) => |
+ new _ValidatingTreeSanitizer(validator); |
/** |
- * Handles the microtask callback and forwards it if necessary. |
+ * Called with the root of the tree which is to be sanitized. |
+ * |
+ * This method needs to walk the entire tree and either remove elements and |
+ * attributes which are not recognized as safe or throw an exception which |
+ * will mark the entire tree as unsafe. |
*/ |
- void _onCallback() { |
- // Ignore spurious messages. |
- if (!_nextMicrotaskFrameScheduled) { |
- return; |
- } |
- _nextMicrotaskFrameScheduled = false; |
- this._callback(); |
- } |
+ void sanitizeTree(Node node); |
} |
/** |
- * Scheduler which uses window.postMessage to schedule events. |
+ * Defines the policy for what types of uris are allowed for particular |
+ * attribute values. |
+ * |
+ * This can be used to provide custom rules such as allowing all http:// URIs |
+ * for image attributes but only same-origin URIs for anchor tags. |
*/ |
-class _PostMessageScheduler extends _MicrotaskScheduler { |
- const _MICROTASK_MESSAGE = "DART-MICROTASK"; |
+abstract class UriPolicy { |
+ /** |
+ * Constructs the default UriPolicy which is to only allow Uris to the same |
+ * origin as the application was launched from. |
+ * |
+ * This will block all ftp: mailto: URIs. It will also block accessing |
+ * https://example.com if the app is running from http://example.com. |
+ */ |
+ factory UriPolicy() => new _SameOriginUriPolicy(); |
- _PostMessageScheduler(_MicrotaskCallback callback): super(callback) { |
- // Messages from other windows do not cause a security risk as |
- // all we care about is that _handleMessage is called |
- // after the current event loop is unwound and calling the function is |
- // a noop when zero requests are pending. |
- window.onMessage.listen(this._handleMessage); |
- } |
+ /** |
+ * Checks if the uri is allowed on the specified attribute. |
+ * |
+ * The uri provided may or may not be a relative path. |
+ */ |
+ bool allowsUri(String uri); |
+} |
- void _schedule() { |
- window.postMessage(_MICROTASK_MESSAGE, "*"); |
- } |
+/** |
+ * Allows URIs to the same origin as the current application was loaded from |
+ * (such as https://example.com:80). |
+ */ |
+class _SameOriginUriPolicy implements UriPolicy { |
+ final AnchorElement _hiddenAnchor = new AnchorElement(); |
- void _handleMessage(e) { |
- this._onCallback(); |
+ bool allowsUri(String uri) { |
+ _hiddenAnchor.href = uri; |
+ return _hiddenAnchor.href.startsWith(window.location.origin); |
} |
} |
+ |
/** |
- * Scheduler which uses a MutationObserver to schedule events. |
+ * Standard tree sanitizer which validates a node tree against the provided |
+ * validator and removes any nodes or attributes which are not allowed. |
*/ |
-class _MutationObserverScheduler extends _MicrotaskScheduler { |
- MutationObserver _observer; |
- Element _dummy; |
- |
- _MutationObserverScheduler(_MicrotaskCallback callback): super(callback) { |
- // Mutation events get fired as soon as the current event stack is unwound |
- // so we just make a dummy event and listen for that. |
- _observer = new MutationObserver(this._handleMutation); |
- _dummy = new DivElement(); |
- _observer.observe(_dummy, attributes: true); |
+class _ValidatingTreeSanitizer implements NodeTreeSanitizer { |
+ final NodeValidator validator; |
+ _ValidatingTreeSanitizer(this.validator) {} |
+ |
+ void sanitizeTree(Node node) { |
+ void walk(Node node) { |
+ sanitizeNode(node); |
+ |
+ var child = node.$dom_lastChild; |
+ while (child != null) { |
+ // Child may be removed during the walk. |
+ var nextChild = child.previousNode; |
+ walk(child); |
+ child = nextChild; |
+ } |
+ } |
+ walk(node); |
} |
- void _schedule() { |
- // Toggle it to trigger the mutation event. |
- _dummy.hidden = !_dummy.hidden; |
- } |
+ void sanitizeNode(Node node) { |
+ switch (node.nodeType) { |
+ case Node.ELEMENT_NODE: |
+ Element element = node; |
+ var attrs = element.attributes; |
+ if (!validator.allowsElement(element)) { |
+ element.remove(); |
+ break; |
+ } |
- _handleMutation(List<MutationRecord> mutations, MutationObserver observer) { |
- this._onCallback(); |
+ var isAttr = attrs['is']; |
+ if (isAttr != null) { |
+ if (!validator.allowsAttribute(element, 'is', isAttr)) { |
+ element.remove(); |
+ break; |
+ } |
+ } |
+ |
+ // TODO(blois): Need to be able to get all attributes, irrespective of |
+ // XMLNS. |
+ var keys = attrs.keys.toList(); |
+ for (var i = attrs.length - 1; i >= 0; --i) { |
+ var name = keys[i]; |
+ if (!validator.allowsAttribute(element, name, attrs[name])) { |
+ attrs.remove(name); |
+ } |
+ } |
+ break; |
+ case Node.COMMENT_NODE: |
+ case Node.DOCUMENT_FRAGMENT_NODE: |
+ case Node.TEXT_NODE: |
+ case Node.CDATA_SECTION_NODE: |
+ break; |
+ default: |
+ node.remove(); |
+ } |
} |
} |
+// Copyright (c) 2013, 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. |
+ |
/** |
- * Scheduler which uses window.setImmediate to schedule events. |
+ * Helper class to implement custom events which wrap DOM events. |
*/ |
-class _SetImmediateScheduler extends _MicrotaskScheduler { |
- _SetImmediateScheduler(_MicrotaskCallback callback): super(callback); |
+class _WrappedEvent implements Event { |
+ final Event wrapped; |
+ _WrappedEvent(this.wrapped); |
- void _schedule() { |
- window._setImmediate(_handleImmediate); |
- } |
+ bool get bubbles => wrapped.bubbles; |
- void _handleImmediate() { |
- this._onCallback(); |
+ bool get cancelBubble => wrapped.bubbles; |
+ void set cancelBubble(bool value) { |
+ wrapped.cancelBubble = value; |
} |
-} |
-List<TimeoutHandler> _pendingMicrotasks; |
-_MicrotaskScheduler _microtaskScheduler = null; |
+ bool get cancelable => wrapped.cancelable; |
-void _maybeScheduleMicrotaskFrame() { |
- if (_microtaskScheduler == null) { |
- _microtaskScheduler = |
- new _MicrotaskScheduler.best(_completeMicrotasks); |
+ DataTransfer get clipboardData => wrapped.clipboardData; |
+ |
+ EventTarget get currentTarget => wrapped.currentTarget; |
+ |
+ bool get defaultPrevented => wrapped.defaultPrevented; |
+ |
+ int get eventPhase => wrapped.eventPhase; |
+ |
+ EventTarget get target => wrapped.target; |
+ |
+ int get timeStamp => wrapped.timeStamp; |
+ |
+ String get type => wrapped.type; |
+ |
+ void $dom_initEvent(String eventTypeArg, bool canBubbleArg, |
+ bool cancelableArg) { |
+ throw new UnsupportedError( |
+ 'Cannot initialize this Event.'); |
} |
- _microtaskScheduler.maybeSchedule(); |
-} |
-/** |
- * Registers a [callback] which is called after the current execution stack |
- * unwinds. |
- */ |
-void _addMicrotaskCallback(TimeoutHandler callback) { |
- if (_pendingMicrotasks == null) { |
- _pendingMicrotasks = <TimeoutHandler>[]; |
- _maybeScheduleMicrotaskFrame(); |
+ void preventDefault() { |
+ wrapped.preventDefault(); |
} |
- _pendingMicrotasks.add(callback); |
-} |
+ void stopImmediatePropagation() { |
+ wrapped.stopImmediatePropagation(); |
+ } |
-/** |
- * Complete all pending microtasks. |
- */ |
-void _completeMicrotasks() { |
- var callbacks = _pendingMicrotasks; |
- _pendingMicrotasks = null; |
- for (var callback in callbacks) { |
- callback(); |
+ void stopPropagation() { |
+ wrapped.stopPropagation(); |
} |
} |
-// Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file |
+// Copyright (c) 2013, 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. |
-// Patch file for the dart:isolate library. |
- |
- |
-/******************************************************** |
- Inserted from lib/isolate/serialization.dart |
- ********************************************************/ |
-class _MessageTraverserVisitedMap { |
+/** |
+ * A list which just wraps another list, for either intercepting list calls or |
+ * retyping the list (for example, from List<A> to List<B> where B extends A). |
+ */ |
+class _WrappedList<E> extends ListBase<E> { |
+ final List _list; |
- operator[](var object) => null; |
- void operator[]=(var object, var info) { } |
+ _WrappedList(this._list); |
- void reset() { } |
- void cleanup() { } |
+ // Iterable APIs |
-} |
+ Iterator<E> get iterator => new _WrappedIterator(_list.iterator); |
-/** Abstract visitor for dart objects that can be sent as isolate messages. */ |
-abstract class _MessageTraverser { |
+ int get length => _list.length; |
- _MessageTraverserVisitedMap _visited; |
- _MessageTraverser() : _visited = new _MessageTraverserVisitedMap(); |
+ // Collection APIs |
- /** Visitor's entry point. */ |
- traverse(var x) { |
- if (isPrimitive(x)) return visitPrimitive(x); |
- _visited.reset(); |
- var result; |
- try { |
- result = _dispatch(x); |
- } finally { |
- _visited.cleanup(); |
- } |
- return result; |
- } |
+ void add(E element) { _list.add(element); } |
- _dispatch(var x) { |
- if (isPrimitive(x)) return visitPrimitive(x); |
- if (x is List) return visitList(x); |
- if (x is Map) return visitMap(x); |
- if (x is SendPort) return visitSendPort(x); |
- if (x is SendPortSync) return visitSendPortSync(x); |
+ bool remove(Object element) => _list.remove(element); |
- // Overridable fallback. |
- return visitObject(x); |
- } |
+ void clear() { _list.clear(); } |
- visitPrimitive(x); |
- visitList(List x); |
- visitMap(Map x); |
- visitSendPort(SendPort x); |
- visitSendPortSync(SendPortSync x); |
+ // List APIs |
- visitObject(Object x) { |
- // TODO(floitsch): make this a real exception. (which one)? |
- throw "Message serialization: Illegal value $x passed"; |
- } |
+ E operator [](int index) => _list[index]; |
- static bool isPrimitive(x) { |
- return (x == null) || (x is String) || (x is num) || (x is bool); |
- } |
-} |
+ void operator []=(int index, E value) { _list[index] = value; } |
+ void set length(int newLength) { _list.length = newLength; } |
-/** Visitor that serializes a message as a JSON array. */ |
-abstract class _Serializer extends _MessageTraverser { |
- int _nextFreeRefId = 0; |
+ void sort([int compare(E a, E b)]) { _list.sort(compare); } |
- visitPrimitive(x) => x; |
+ int indexOf(E element, [int start = 0]) => _list.indexOf(element, start); |
- visitList(List list) { |
- int copyId = _visited[list]; |
- if (copyId != null) return ['ref', copyId]; |
+ int lastIndexOf(E element, [int start]) => _list.lastIndexOf(element, start); |
- int id = _nextFreeRefId++; |
- _visited[list] = id; |
- var jsArray = _serializeList(list); |
- // TODO(floitsch): we are losing the generic type. |
- return ['list', id, jsArray]; |
+ void insert(int index, E element) => _list.insert(index, element); |
+ |
+ E removeAt(int index) => _list.removeAt(index); |
+ |
+ void setRange(int start, int end, Iterable<E> iterable, [int skipCount = 0]) { |
+ _list.setRange(start, end, iterable, skipCount); |
} |
- visitMap(Map map) { |
- int copyId = _visited[map]; |
- if (copyId != null) return ['ref', copyId]; |
+ void removeRange(int start, int end) { _list.removeRange(start, end); } |
- int id = _nextFreeRefId++; |
- _visited[map] = id; |
- var keys = _serializeList(map.keys.toList()); |
- var values = _serializeList(map.values.toList()); |
- // TODO(floitsch): we are losing the generic type. |
- return ['map', id, keys, values]; |
+ void replaceRange(int start, int end, Iterable<E> iterable) { |
+ _list.replaceRange(start, end, iterable); |
} |
- _serializeList(List list) { |
- int len = list.length; |
- var result = new List(len); |
- for (int i = 0; i < len; i++) { |
- result[i] = _dispatch(list[i]); |
- } |
- return result; |
+ void fillRange(int start, int end, [E fillValue]) { |
+ _list.fillRange(start, end, fillValue); |
} |
} |
-/** Deserializes arrays created with [_Serializer]. */ |
-abstract class _Deserializer { |
- Map<int, dynamic> _deserialized; |
+/** |
+ * Iterator wrapper for _WrappedList. |
+ */ |
+class _WrappedIterator<E> implements Iterator<E> { |
+ Iterator _iterator; |
- _Deserializer(); |
+ _WrappedIterator(this._iterator); |
- static bool isPrimitive(x) { |
- return (x == null) || (x is String) || (x is num) || (x is bool); |
+ bool moveNext() { |
+ return _iterator.moveNext(); |
} |
- deserialize(x) { |
- if (isPrimitive(x)) return x; |
- // TODO(floitsch): this should be new HashMap<int, dynamic>() |
- _deserialized = new HashMap(); |
- return _deserializeHelper(x); |
- } |
+ E get current => _iterator.current; |
+} |
+// 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. |
- _deserializeHelper(x) { |
- if (isPrimitive(x)) return x; |
- assert(x is List); |
- switch (x[0]) { |
- case 'ref': return _deserializeRef(x); |
- case 'list': return _deserializeList(x); |
- case 'map': return _deserializeMap(x); |
- case 'sendport': return deserializeSendPort(x); |
- default: return deserializeObject(x); |
- } |
- } |
- _deserializeRef(List x) { |
- int id = x[1]; |
- var result = _deserialized[id]; |
- assert(result != null); |
- return result; |
- } |
+class _HttpRequestUtils { |
- List _deserializeList(List x) { |
- int id = x[1]; |
- // We rely on the fact that Dart-lists are directly mapped to Js-arrays. |
- List dartList = x[2]; |
- _deserialized[id] = dartList; |
- int len = dartList.length; |
- for (int i = 0; i < len; i++) { |
- dartList[i] = _deserializeHelper(dartList[i]); |
- } |
- return dartList; |
- } |
+ // Helper for factory HttpRequest.get |
+ static HttpRequest get(String url, |
+ onComplete(HttpRequest request), |
+ bool withCredentials) { |
+ final request = new HttpRequest(); |
+ request.open('GET', url, async: true); |
- Map _deserializeMap(List x) { |
- Map result = new Map(); |
- int id = x[1]; |
- _deserialized[id] = result; |
- List keys = x[2]; |
- List values = x[3]; |
- int len = keys.length; |
- assert(len == values.length); |
- for (int i = 0; i < len; i++) { |
- var key = _deserializeHelper(keys[i]); |
- var value = _deserializeHelper(values[i]); |
- result[key] = value; |
- } |
- return result; |
- } |
+ request.withCredentials = withCredentials; |
- deserializeSendPort(List x); |
+ request.onReadyStateChange.listen((e) { |
+ if (request.readyState == HttpRequest.DONE) { |
+ onComplete(request); |
+ } |
+ }); |
- deserializeObject(List x) { |
- // TODO(floitsch): Use real exception (which one?). |
- throw "Unexpected serialized object"; |
+ request.send(); |
+ |
+ return request; |
} |
} |
- |
// Copyright (c) 2011, 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. |
@@ -31000,6 +31895,130 @@ class _VariableSizeListIterator<T> implements Iterator<T> { |
T get current => _current; |
} |
+/** |
+ * A custom KeyboardEvent that attempts to eliminate cross-browser |
+ * inconsistencies, and also provide both keyCode and charCode information |
+ * for all key events (when such information can be determined). |
+ * |
+ * KeyEvent tries to provide a higher level, more polished keyboard event |
+ * information on top of the "raw" [KeyboardEvent]. |
+ * |
+ * This class is very much a work in progress, and we'd love to get information |
+ * on how we can make this class work with as many international keyboards as |
+ * possible. Bugs welcome! |
+ */ |
+ |
+class KeyEvent extends _WrappedEvent implements KeyboardEvent { |
+ /** The parent KeyboardEvent that this KeyEvent is wrapping and "fixing". */ |
+ KeyboardEvent _parent; |
+ |
+ /** The "fixed" value of whether the alt key is being pressed. */ |
+ bool _shadowAltKey; |
+ |
+ /** Caculated value of what the estimated charCode is for this event. */ |
+ int _shadowCharCode; |
+ |
+ /** Caculated value of what the estimated keyCode is for this event. */ |
+ int _shadowKeyCode; |
+ |
+ /** Caculated value of what the estimated keyCode is for this event. */ |
+ int get keyCode => _shadowKeyCode; |
+ |
+ /** Caculated value of what the estimated charCode is for this event. */ |
+ int get charCode => this.type == 'keypress' ? _shadowCharCode : 0; |
+ |
+ /** Caculated value of whether the alt key is pressed is for this event. */ |
+ bool get altKey => _shadowAltKey; |
+ |
+ /** Caculated value of what the estimated keyCode is for this event. */ |
+ int get which => keyCode; |
+ |
+ /** Accessor to the underlying keyCode value is the parent event. */ |
+ int get _realKeyCode => _parent.keyCode; |
+ |
+ /** Accessor to the underlying charCode value is the parent event. */ |
+ int get _realCharCode => _parent.charCode; |
+ |
+ /** Accessor to the underlying altKey value is the parent event. */ |
+ bool get _realAltKey => _parent.altKey; |
+ |
+ /** Construct a KeyEvent with [parent] as the event we're emulating. */ |
+ KeyEvent(KeyboardEvent parent): super(parent) { |
+ _parent = parent; |
+ _shadowAltKey = _realAltKey; |
+ _shadowCharCode = _realCharCode; |
+ _shadowKeyCode = _realKeyCode; |
+ } |
+ |
+ /** Accessor to provide a stream of KeyEvents on the desired target. */ |
+ static EventStreamProvider<KeyEvent> keyDownEvent = |
+ new _KeyboardEventHandler('keydown'); |
+ /** Accessor to provide a stream of KeyEvents on the desired target. */ |
+ static EventStreamProvider<KeyEvent> keyUpEvent = |
+ new _KeyboardEventHandler('keyup'); |
+ /** Accessor to provide a stream of KeyEvents on the desired target. */ |
+ static EventStreamProvider<KeyEvent> keyPressEvent = |
+ new _KeyboardEventHandler('keypress'); |
+ |
+ /** True if the altGraphKey is pressed during this event. */ |
+ bool get altGraphKey => _parent.altGraphKey; |
+ /** Accessor to the clipboardData available for this event. */ |
+ DataTransfer get clipboardData => _parent.clipboardData; |
+ /** True if the ctrl key is pressed during this event. */ |
+ bool get ctrlKey => _parent.ctrlKey; |
+ int get detail => _parent.detail; |
+ /** |
+ * Accessor to the part of the keyboard that the key was pressed from (one of |
+ * KeyLocation.STANDARD, KeyLocation.RIGHT, KeyLocation.LEFT, |
+ * KeyLocation.NUMPAD, KeyLocation.MOBILE, KeyLocation.JOYSTICK). |
+ */ |
+ int get keyLocation => _parent.keyLocation; |
+ Point get layer => _parent.layer; |
+ /** True if the Meta (or Mac command) key is pressed during this event. */ |
+ bool get metaKey => _parent.metaKey; |
+ Point get page => _parent.page; |
+ /** True if the shift key was pressed during this event. */ |
+ bool get shiftKey => _parent.shiftKey; |
+ Window get view => _parent.view; |
+ void $dom_initUIEvent(String type, bool canBubble, bool cancelable, |
+ Window view, int detail) { |
+ throw new UnsupportedError("Cannot initialize a UI Event from a KeyEvent."); |
+ } |
+ String get _shadowKeyIdentifier => _parent.$dom_keyIdentifier; |
+ |
+ int get $dom_charCode => charCode; |
+ int get $dom_keyCode => keyCode; |
+ String get $dom_keyIdentifier { |
+ throw new UnsupportedError("keyIdentifier is unsupported."); |
+ } |
+ void $dom_initKeyboardEvent(String type, bool canBubble, bool cancelable, |
+ Window view, String keyIdentifier, int keyLocation, bool ctrlKey, |
+ bool altKey, bool shiftKey, bool metaKey, |
+ bool altGraphKey) { |
+ throw new UnsupportedError( |
+ "Cannot initialize a KeyboardEvent from a KeyEvent."); |
+ } |
+} |
+// Copyright (c) 2013, 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. |
+ |
+ |
+class Platform { |
+ /** |
+ * Returns true if dart:typed_data types are supported on this |
+ * browser. If false, using these types will generate a runtime |
+ * error. |
+ */ |
+ static final supportsTypedData = true; |
+ |
+ /** |
+ * Returns true if SIMD types in dart:typed_data types are supported |
+ * on this browser. If false, using these types will generate a runtime |
+ * error. |
+ */ |
+ static final supportsSimd = true; |
+} |
// Copyright (c) 2011, 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. |