Chromium Code Reviews| 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. |