Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(9)

Unified Diff: sdk/lib/html/dartium/html_dartium.dart

Side-by-side diff isn't available for this file because of its large size.
Issue 16374007: First rev of Safe DOM (Closed) Base URL: https://dart.googlecode.com/svn/branches/bleeding_edge/dart
Patch Set: Created 7 years, 6 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
Download patch
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.

Powered by Google App Engine
This is Rietveld 408576698