| Index: sdk/lib/html/dart2js/html_dart2js.dart
|
| diff --git a/sdk/lib/html/dart2js/html_dart2js.dart b/sdk/lib/html/dart2js/html_dart2js.dart
|
| index ea38b93dfe008a3c2fc6e7a3dad8c14f72107311..78e30a268bb434754b5049b7123df660f2561854 100644
|
| --- a/sdk/lib/html/dart2js/html_dart2js.dart
|
| +++ b/sdk/lib/html/dart2js/html_dart2js.dart
|
| @@ -6885,21 +6885,18 @@ class Document extends Node native "Document"
|
| class DocumentFragment extends Node native "DocumentFragment" {
|
| 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}) {
|
| +
|
| + 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);
|
| }
|
|
|
| // Native field is used only by Dart code so does not lead to instantiation
|
| @@ -6933,17 +6930,16 @@ class DocumentFragment extends Node native "DocumentFragment" {
|
| 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));
|
| }
|
|
|
| /**
|
| @@ -7462,20 +7458,30 @@ abstract class Element extends Node implements ElementTraversal native "Element"
|
| /**
|
| * Creates an HTML element from a valid fragment of HTML.
|
| *
|
| - * The [html] fragment must represent valid HTML with a single element root,
|
| - * which will be parsed and returned.
|
| + * var element = new Element.html('<div class="foo">content</div>');
|
| + *
|
| + * The HTML fragment should contain only one single root element, any
|
| + * leading or trailing text nodes will be removed.
|
| *
|
| - * Important: the contents of [html] should not contain any user-supplied
|
| - * data. Without strict data validation it is impossible to prevent script
|
| - * injection exploits.
|
| + * The HTML fragment is parsed as if it occurred within the context of a
|
| + * `<body>` tag, this means that special elements such as `<caption>` which
|
| + * must be parsed within the scope of a `<table>` element will be dropped. Use
|
| + * [createFragment] to parse contextual HTML fragments.
|
| *
|
| - * It is instead recommended that elements be constructed via [Element.tag]
|
| - * and text be added via [text].
|
| + * Unless a validator is provided this will perform the default validation
|
| + * and remove all scriptable elements and attributes.
|
| + *
|
| + * See also:
|
| + *
|
| + * * [NodeValidator]
|
| *
|
| - * 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.
|
| @@ -8146,6 +8152,55 @@ abstract class Element extends Node implements ElementTraversal native "Element"
|
| 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);
|
| + } else if (validator != null) {
|
| + throw new ArgumentError(
|
| + 'validator can only be passed if treeSanitizer is null');
|
| + }
|
| +
|
| + 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 => $dom_innerHtml;
|
| +
|
|
|
| @DomName('Element.abortEvent')
|
| @DocsEditable
|
| @@ -8377,7 +8432,7 @@ abstract class Element extends Node implements ElementTraversal native "Element"
|
| @JSName('innerHTML')
|
| @DomName('Element.innerHTML')
|
| @DocsEditable
|
| - String innerHtml;
|
| + String $dom_innerHtml;
|
|
|
| @DomName('Element.isContentEditable')
|
| @DocsEditable
|
| @@ -8956,118 +9011,46 @@ abstract class Element extends Node implements ElementTraversal native "Element"
|
| }
|
|
|
|
|
| -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';
|
| - }
|
| + static HtmlDocument _parseDocument;
|
|
|
| - 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];
|
| + if (_parseDocument == null) {
|
| + _parseDocument = document.implementation.createHtmlDocument('');
|
| + }
|
| + var contextElement;
|
| + if (context == null || context is BodyElement) {
|
| + contextElement = _parseDocument.body;
|
| } else {
|
| - _singleNode(temp.children);
|
| + contextElement = _parseDocument.$dom_createElement(context.tagName);
|
| + _parseDocument.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) {
|
| + var range = _parseDocument.$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());
|
| - }
|
| + if (contextElement != _parseDocument.body) {
|
| + contextElement.remove();
|
| + }
|
| +
|
| + treeSanitizer.sanitizeTree(fragment);
|
| + return fragment;
|
| + } else {
|
| + contextElement.$dom_innerHtml = html;
|
|
|
| - 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');
|
| + treeSanitizer.sanitizeTree(contextElement);
|
| +
|
| + var fragment = new DocumentFragment();
|
| + while (contextElement.$dom_firstChild != null) {
|
| + fragment.append(contextElement.$dom_firstChild);
|
| + }
|
| + return fragment;
|
| + }
|
| }
|
|
|
| @DomName('Document.createElement')
|
| @@ -20615,6 +20598,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
|
| @@ -25690,6 +25683,455 @@ 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})
|
| + :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.
|
| @@ -25771,404 +26213,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.
|
| @@ -26838,1983 +27098,2741 @@ abstract class KeyName {
|
| /** The Volume Up key */
|
| static const String VOLUMN_UP = "VolumeUp";
|
|
|
| - /** The Windows Logo key */
|
| - static const String WIN = "Win";
|
| + /** The Windows Logo key */
|
| + static const String WIN = "Win";
|
| +
|
| + /** The Zoom key */
|
| + static const String ZOOM = "Zoom";
|
| +
|
| + /**
|
| + * The Backspace (Back) key. Note: This key value shall be also used for the
|
| + * key labeled 'delete' MacOS keyboards when not modified by the 'Fn' key
|
| + */
|
| + static const String BACKSPACE = "Backspace";
|
| +
|
| + /** The Horizontal Tabulation (Tab) key */
|
| + static const String TAB = "Tab";
|
| +
|
| + /** The Cancel key */
|
| + static const String CANCEL = "Cancel";
|
| +
|
| + /** The Escape (Esc) key */
|
| + static const String ESC = "Esc";
|
| +
|
| + /** The Space (Spacebar) key: */
|
| + static const String SPACEBAR = "Spacebar";
|
| +
|
| + /**
|
| + * The Delete (Del) Key. Note: This key value shall be also used for the key
|
| + * labeled 'delete' MacOS keyboards when modified by the 'Fn' key
|
| + */
|
| + static const String DEL = "Del";
|
| +
|
| + /** The Combining Grave Accent (Greek Varia, Dead Grave) key */
|
| + static const String DEAD_GRAVE = "DeadGrave";
|
| +
|
| + /**
|
| + * The Combining Acute Accent (Stress Mark, Greek Oxia, Tonos, Dead Eacute)
|
| + * key
|
| + */
|
| + static const String DEAD_EACUTE = "DeadEacute";
|
| +
|
| + /** The Combining Circumflex Accent (Hat, Dead Circumflex) key */
|
| + static const String DEAD_CIRCUMFLEX = "DeadCircumflex";
|
| +
|
| + /** The Combining Tilde (Dead Tilde) key */
|
| + static const String DEAD_TILDE = "DeadTilde";
|
| +
|
| + /** The Combining Macron (Long, Dead Macron) key */
|
| + static const String DEAD_MACRON = "DeadMacron";
|
| +
|
| + /** The Combining Breve (Short, Dead Breve) key */
|
| + static const String DEAD_BREVE = "DeadBreve";
|
| +
|
| + /** The Combining Dot Above (Derivative, Dead Above Dot) key */
|
| + static const String DEAD_ABOVE_DOT = "DeadAboveDot";
|
| +
|
| + /**
|
| + * The Combining Diaeresis (Double Dot Abode, Umlaut, Greek Dialytika,
|
| + * Double Derivative, Dead Diaeresis) key
|
| + */
|
| + static const String DEAD_UMLAUT = "DeadUmlaut";
|
| +
|
| + /** The Combining Ring Above (Dead Above Ring) key */
|
| + static const String DEAD_ABOVE_RING = "DeadAboveRing";
|
| +
|
| + /** The Combining Double Acute Accent (Dead Doubleacute) key */
|
| + static const String DEAD_DOUBLEACUTE = "DeadDoubleacute";
|
| +
|
| + /** The Combining Caron (Hacek, V Above, Dead Caron) key */
|
| + static const String DEAD_CARON = "DeadCaron";
|
| +
|
| + /** 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 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.
|
| +
|
|
|
| - /** The Zoom key */
|
| - static const String ZOOM = "Zoom";
|
| +/**
|
| + * 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 Backspace (Back) key. Note: This key value shall be also used for the
|
| - * key labeled 'delete' MacOS keyboards when not modified by the 'Fn' key
|
| + * The set of keys that have been pressed down without seeing their
|
| + * corresponding keyup event.
|
| */
|
| - static const String BACKSPACE = "Backspace";
|
| + final List<KeyboardEvent> _keyDownList = <KeyboardEvent>[];
|
|
|
| - /** The Horizontal Tabulation (Tab) key */
|
| - static const String TAB = "Tab";
|
| + /** The type of KeyEvent we are tracking (keyup, keydown, keypress). */
|
| + final String _type;
|
|
|
| - /** The Cancel key */
|
| - static const String CANCEL = "Cancel";
|
| + /** The element we are watching for events to happen on. */
|
| + final EventTarget _target;
|
|
|
| - /** The Escape (Esc) key */
|
| - static const String ESC = "Esc";
|
| + // 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];
|
|
|
| - /** The Space (Spacebar) key: */
|
| - static const String SPACEBAR = "Spacebar";
|
| + /** Controller to produce KeyEvents for the stream. */
|
| + final StreamController _controller = new StreamController(sync: true);
|
| +
|
| + static const _EVENT_TYPE = 'KeyEvent';
|
|
|
| /**
|
| - * The Delete (Del) Key. Note: This key value shall be also used for the key
|
| - * labeled 'delete' MacOS keyboards when modified by the 'Fn' key
|
| + * 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 String DEL = "Del";
|
| + 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
|
| + };
|
|
|
| - /** The Combining Grave Accent (Greek Varia, Dead Grave) key */
|
| - static const String DEAD_GRAVE = "DeadGrave";
|
| + /** 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 Acute Accent (Stress Mark, Greek Oxia, Tonos, Dead Eacute)
|
| - * 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_EACUTE = "DeadEacute";
|
| + 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 Circumflex Accent (Hat, Dead Circumflex) key */
|
| - static const String DEAD_CIRCUMFLEX = "DeadCircumflex";
|
| + /**
|
| + * General constructor, performs basic initialization for our improved
|
| + * KeyboardEvent controller.
|
| + */
|
| + _KeyboardEventHandler(this._type) :
|
| + _target = null, super(_EVENT_TYPE) {
|
| + }
|
|
|
| - /** The Combining Tilde (Dead Tilde) key */
|
| - static const String DEAD_TILDE = "DeadTilde";
|
| + /**
|
| + * 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);
|
| + }
|
|
|
| - /** The Combining Macron (Long, Dead Macron) key */
|
| - static const String DEAD_MACRON = "DeadMacron";
|
| + /**
|
| + * Notify all callback listeners that a KeyEvent of the relevant type has
|
| + * occurred.
|
| + */
|
| + bool _dispatch(KeyEvent event) {
|
| + if (event.type == _type)
|
| + _controller.add(event);
|
| + }
|
|
|
| - /** The Combining Breve (Short, Dead Breve) key */
|
| - static const String DEAD_BREVE = "DeadBreve";
|
| + /** Determine if caps lock is one of the currently depressed keys. */
|
| + bool get _capsLockOn =>
|
| + _keyDownList.any((var element) => element.keyCode == KeyCode.CAPS_LOCK);
|
|
|
| - /** The Combining Dot Above (Derivative, Dead Above Dot) key */
|
| - static const String DEAD_ABOVE_DOT = "DeadAboveDot";
|
| + /**
|
| + * 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;
|
| + }
|
|
|
| /**
|
| - * The Combining Diaeresis (Double Dot Abode, Umlaut, Greek Dialytika,
|
| - * Double Derivative, Dead Diaeresis) key
|
| + * 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.
|
| */
|
| - static const String DEAD_UMLAUT = "DeadUmlaut";
|
| + 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;
|
| + }
|
|
|
| - /** The Combining Ring Above (Dead Above Ring) key */
|
| - static const String DEAD_ABOVE_RING = "DeadAboveRing";
|
| + /**
|
| + * Returns true if the key fires a keypress event in the current browser.
|
| + */
|
| + bool _firesKeyPressEvent(KeyEvent event) {
|
| + if (!Device.isIE && !Device.isWebKit) {
|
| + return true;
|
| + }
|
|
|
| - /** The Combining Double Acute Accent (Dead Doubleacute) key */
|
| - static const String DEAD_DOUBLEACUTE = "DeadDoubleacute";
|
| + if (Device.userAgent.contains('Mac') && event.altKey) {
|
| + return KeyCode.isCharacterKey(event.keyCode);
|
| + }
|
| +
|
| + // Alt but not AltGr which is represented as Alt+Ctrl.
|
| + if (event.altKey && !event.ctrlKey) {
|
| + return false;
|
| + }
|
|
|
| - /** The Combining Caron (Hacek, V Above, Dead Caron) key */
|
| - static const String DEAD_CARON = "DeadCaron";
|
| + // 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;
|
| + }
|
|
|
| - /** The Combining Cedilla (Dead Cedilla) key */
|
| - static const String DEAD_CEDILLA = "DeadCedilla";
|
| + // 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;
|
| + }
|
|
|
| - /** The Combining Ogonek (Nasal Hook, Dead Ogonek) key */
|
| - static const String DEAD_OGONEK = "DeadOgonek";
|
| + switch (event.keyCode) {
|
| + case KeyCode.ENTER:
|
| + // IE9 does not fire keypress on ENTER.
|
| + return !Device.isIE;
|
| + case KeyCode.ESC:
|
| + return !Device.isWebKit;
|
| + }
|
|
|
| - /**
|
| - * The Combining Greek Ypogegrammeni (Greek Non-Spacing Iota Below, Iota
|
| - * Subscript, Dead Iota) key
|
| - */
|
| - static const String DEAD_IOTA = "DeadIota";
|
| + return KeyCode.isCharacterKey(event.keyCode);
|
| + }
|
|
|
| /**
|
| - * The Combining Katakana-Hiragana Voiced Sound Mark (Dead Voiced Sound) key
|
| + * Normalize the keycodes to the IE KeyCodes (this is what Chrome, IE, and
|
| + * Opera all use).
|
| */
|
| - static const String DEAD_VOICED_SOUND = "DeadVoicedSound";
|
| + 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;
|
| + }
|
|
|
| - /**
|
| - * The Combining Katakana-Hiragana Semi-Voiced Sound Mark (Dead Semivoiced
|
| - * Sound) key
|
| - */
|
| - static const String DEC_SEMIVOICED_SOUND= "DeadSemivoicedSound";
|
| + /** 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();
|
| + }
|
|
|
| - /**
|
| - * 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) 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 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);
|
| + }
|
| + _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);
|
|
|
| -// 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.
|
| + // 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);
|
| + }
|
|
|
| -// 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.
|
| + /** 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);
|
| + }
|
| +}
|
|
|
|
|
| /**
|
| - * A data-bound path starting from a view-model or model object, for example
|
| - * `foo.bar.baz`.
|
| + * 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.
|
| *
|
| - * 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.
|
| + * Example usage:
|
| *
|
| - * This class is used to implement [Node.bind] and similar functionality.
|
| + * 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!
|
| */
|
| -// 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;
|
| +class KeyboardEventStream {
|
|
|
| - Object _lastValue;
|
| - bool _scheduled = false;
|
| + /** Named constructor to produce a stream for onKeyPress events. */
|
| + static Stream<KeyEvent> onKeyPress(EventTarget target) =>
|
| + new _KeyboardEventHandler('keypress').forTarget(target);
|
|
|
| - /**
|
| - * 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) {
|
| + /** Named constructor to produce a stream for onKeyUp events. */
|
| + static Stream<KeyEvent> onKeyUp(EventTarget target) =>
|
| + new _KeyboardEventHandler('keyup').forTarget(target);
|
|
|
| - // TODO(jmesserly): if the path is empty, or the object is! Observable, we
|
| - // can optimize the PathObserver to be more lightweight.
|
| + /** Named constructor to produce a stream for onKeyDown events. */
|
| + static Stream<KeyEvent> onKeyDown(EventTarget target) =>
|
| + new _KeyboardEventHandler('keydown').forTarget(target);
|
| +}
|
| +// 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.
|
|
|
| - _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));
|
| - }
|
| +typedef void _MicrotaskCallback();
|
|
|
| - // 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;
|
| - }
|
| - }
|
| - }
|
| +/**
|
| + * This class attempts to invoke a callback as soon as the current event stack
|
| + * unwinds, but before the browser repaints.
|
| + */
|
| +abstract class _MicrotaskScheduler {
|
| + bool _nextMicrotaskFrameScheduled = false;
|
| + final _MicrotaskCallback _callback;
|
| +
|
| + _MicrotaskScheduler(this._callback);
|
|
|
| - // 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.
|
| + * Creates the best possible microtask scheduler for the current platform.
|
| */
|
| - StreamSubscription bindSync(void callback(value)) {
|
| - var result = values.listen(callback);
|
| - callback(value);
|
| - return result;
|
| + factory _MicrotaskScheduler.best(_MicrotaskCallback callback) {
|
| + if (Window._supportsSetImmediate) {
|
| + return new _SetImmediateScheduler(callback);
|
| + } else if (MutationObserver.supported) {
|
| + return new _MutationObserverScheduler(callback);
|
| + }
|
| + return new _PostMessageScheduler(callback);
|
| }
|
|
|
| - // 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.
|
| + * Schedules a microtask callback if one has not been scheduled already.
|
| */
|
| - 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;
|
| + void maybeSchedule() {
|
| + if (this._nextMicrotaskFrameScheduled) {
|
| + return;
|
| }
|
| + this._nextMicrotaskFrameScheduled = true;
|
| + this._schedule();
|
| }
|
|
|
| - void _observe() {
|
| - if (_observer != null) {
|
| - _lastValue = value;
|
| - _observer.observe();
|
| - }
|
| - }
|
| + /**
|
| + * Does the actual scheduling of the callback.
|
| + */
|
| + void _schedule();
|
|
|
| - void _unobserve() {
|
| - if (_observer != null) _observer.unobserve();
|
| + /**
|
| + * Handles the microtask callback and forwards it if necessary.
|
| + */
|
| + void _onCallback() {
|
| + // Ignore spurious messages.
|
| + if (!_nextMicrotaskFrameScheduled) {
|
| + return;
|
| + }
|
| + _nextMicrotaskFrameScheduled = false;
|
| + this._callback();
|
| }
|
| +}
|
|
|
| - void _notifyChange() {
|
| - if (_scheduled) return;
|
| - _scheduled = true;
|
| +/**
|
| + * Scheduler which uses window.postMessage to schedule events.
|
| + */
|
| +class _PostMessageScheduler extends _MicrotaskScheduler {
|
| + const _MICROTASK_MESSAGE = "DART-MICROTASK";
|
|
|
| - // 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);
|
| + _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);
|
| }
|
|
|
| - /** 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;
|
| + void _schedule() {
|
| + window.postMessage(_MICROTASK_MESSAGE, "*");
|
| }
|
|
|
| - /** 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;
|
| - }
|
| + void _handleMessage(e) {
|
| + this._onCallback();
|
| }
|
| }
|
|
|
| -// 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;
|
| - }
|
| - }
|
| -
|
| - // TODO(jmesserly): what about length?
|
| - if (object is Map) return object[property];
|
| -
|
| - if (object is Observable) return object.getValueWorkaround(property);
|
| -
|
| - return null;
|
| -}
|
| +/**
|
| + * Scheduler which uses a MutationObserver to schedule events.
|
| + */
|
| +class _MutationObserverScheduler extends _MicrotaskScheduler {
|
| + MutationObserver _observer;
|
| + Element _dummy;
|
|
|
| -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;
|
| + _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);
|
| }
|
| - 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 _schedule() {
|
| + // Toggle it to trigger the mutation event.
|
| + _dummy.hidden = !_dummy.hidden;
|
| + }
|
|
|
| - void set value(Object newValue) {
|
| - _value = newValue;
|
| - if (_next != null) {
|
| - if (_sub != null) _next.unobserve();
|
| - _next.ensureValue(_value);
|
| - if (_sub != null) _next.observe();
|
| - }
|
| + _handleMutation(List<MutationRecord> mutations, MutationObserver observer) {
|
| + this._onCallback();
|
| }
|
| +}
|
|
|
| - void ensureValue(object) {
|
| - // If we're observing, values should be up to date already.
|
| - if (_sub != null) return;
|
| +/**
|
| + * Scheduler which uses window.setImmediate to schedule events.
|
| + */
|
| +class _SetImmediateScheduler extends _MicrotaskScheduler {
|
| + _SetImmediateScheduler(_MicrotaskCallback callback): super(callback);
|
|
|
| - _object = object;
|
| - value = _getObjectProperty(object, _property);
|
| + void _schedule() {
|
| + window._setImmediate(_handleImmediate);
|
| }
|
|
|
| - void observe() {
|
| - if (_object is Observable) {
|
| - assert(_sub == null);
|
| - _sub = (_object as Observable).changes.listen(_onChange);
|
| - }
|
| - if (_next != null) _next.observe();
|
| + void _handleImmediate() {
|
| + this._onCallback();
|
| }
|
| +}
|
|
|
| - void unobserve() {
|
| - if (_sub == null) return;
|
| +List<TimeoutHandler> _pendingMicrotasks;
|
| +_MicrotaskScheduler _microtaskScheduler = null;
|
|
|
| - _sub.cancel();
|
| - _sub = null;
|
| - if (_next != null) _next.unobserve();
|
| +void _maybeScheduleMicrotaskFrame() {
|
| + if (_microtaskScheduler == null) {
|
| + _microtaskScheduler =
|
| + new _MicrotaskScheduler.best(_completeMicrotasks);
|
| }
|
| + _microtaskScheduler.maybeSchedule();
|
| +}
|
|
|
| - 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;
|
| - }
|
| - }
|
| +/**
|
| + * Registers a [callback] which is called after the current execution stack
|
| + * unwinds.
|
| + */
|
| +void _addMicrotaskCallback(TimeoutHandler callback) {
|
| + if (_pendingMicrotasks == null) {
|
| + _pendingMicrotasks = <TimeoutHandler>[];
|
| + _maybeScheduleMicrotaskFrame();
|
| }
|
| + _pendingMicrotasks.add(callback);
|
| }
|
|
|
| -// 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);
|
| -
|
| -final _spacesRegExp = new RegExp(r'\s');
|
| -
|
| -bool _isPathValid(String s) {
|
| - s = s.replaceAll(_spacesRegExp, '');
|
|
|
| - if (s == '') return true;
|
| - if (s[0] == '.') return false;
|
| - return _pathRegExp.hasMatch(s);
|
| +/**
|
| + * 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.
|
|
|
|
|
| +
|
| /**
|
| - * A utility class for representing two-dimensional positions.
|
| + * 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 Point {
|
| - final num x;
|
| - final num y;
|
| -
|
| - const Point([num x = 0, num y = 0]): x = x, y = y;
|
| +class NodeValidatorBuilder implements NodeValidator {
|
|
|
| - String toString() => '($x, $y)';
|
| + final List<NodeValidator> _validators = <NodeValidator>[];
|
|
|
| - bool operator ==(other) {
|
| - if (other is !Point) return false;
|
| - return x == other.x && y == other.y;
|
| + NodeValidatorBuilder() {
|
| }
|
|
|
| - Point operator +(Point other) {
|
| - return new Point(x + other.x, y + other.y);
|
| + /**
|
| + * Creates a new NodeValidatorBuilder which accepts common constructs.
|
| + *
|
| + * By default this will accept HTML5 elements and attributes with the default
|
| + * [UriPolicy] and templating elements.
|
| + */
|
| + factory NodeValidatorBuilder.common() {
|
| + return new NodeValidatorBuilder()
|
| + ..allowHtml5()
|
| + ..allowTemplating();
|
| }
|
|
|
| - Point operator -(Point other) {
|
| - return new Point(x - other.x, y - other.y);
|
| + /**
|
| + * 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();
|
| + }
|
| + add(new _SimpleNodeValidator.allowNavigation(uriPolicy));
|
| }
|
|
|
| - Point operator *(num factor) {
|
| - return new Point(x * factor, y * factor);
|
| + /**
|
| + * 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].
|
| + */
|
| + void allowImages([UriPolicy uriPolicy]) {
|
| + if (uriPolicy == null) {
|
| + uriPolicy = new UriPolicy();
|
| + }
|
| + add(new _SimpleNodeValidator.allowImages(uriPolicy));
|
| }
|
|
|
| /**
|
| - * Returns the distance between two points.
|
| + * 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
|
| */
|
| - double distanceTo(Point other) {
|
| - var dx = x - other.x;
|
| - var dy = y - other.y;
|
| - return sqrt(dx * dx + dy * dy);
|
| + void allowTextElements() {
|
| + add(new _SimpleNodeValidator.allowTextElements());
|
| }
|
|
|
| /**
|
| - * Returns the squared distance between two points.
|
| + * Allow common safe HTML5 elements and attributes.
|
| *
|
| - * Squared distances can be used for comparisons when the actual value is not
|
| - * required.
|
| + * 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.
|
| */
|
| - num squaredDistanceTo(Point other) {
|
| - var dx = x - other.x;
|
| - var dy = y - other.y;
|
| - return dx * dx + dy * dy;
|
| + void allowHtml5({UriPolicy uriPolicy}) {
|
| + add(new _Html5NodeValidator(uriPolicy: uriPolicy));
|
| }
|
|
|
| - Point ceil() => new Point(x.ceil(), y.ceil());
|
| - Point floor() => new Point(x.floor(), y.floor());
|
| - Point round() => new Point(x.round(), y.round());
|
| + /**
|
| + * Allow SVG elements and attributes except for known bad ones.
|
| + */
|
| + void allowSvg() {
|
| + add(new _SvgNodeValidator());
|
| + }
|
|
|
| /**
|
| - * Truncates x and y to integers and returns the result as a new point.
|
| + * 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.
|
| */
|
| - 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.
|
| + 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));
|
| + }
|
|
|
| -/**
|
| - * Contains the set of standard values returned by HTMLDocument.getReadyState.
|
| - */
|
| -abstract class ReadyState {
|
| /**
|
| - * Indicates the document is still loading and parsing.
|
| + * 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.
|
| */
|
| - static const String LOADING = "loading";
|
| + void allowTagExtension(String tagName, String baseName,
|
| + {UriPolicy uriPolicy,
|
| + Iterable<String> attributes,
|
| + Iterable<String> uriAttributes}) {
|
| +
|
| + 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));
|
| + }
|
| +
|
| + void allowElement(String tagName, {UriPolicy uriPolicy,
|
| + Iterable<String> attributes,
|
| + Iterable<String> uriAttributes}) {
|
| +
|
| + allowCustomElement(tagName, uriPolicy: uriPolicy,
|
| + attributes: attributes,
|
| + uriAttributes: uriAttributes);
|
| + }
|
|
|
| /**
|
| - * Indicates the document is finished parsing but is still loading
|
| - * subresources.
|
| + * Allow templating elements (such as <template> and template-related
|
| + * attributes.
|
| + *
|
| + * This still requires other validators to allow regular attributes to be
|
| + * bound (such as [allowHtml5]).
|
| */
|
| - static const String INTERACTIVE = "interactive";
|
| + void allowTemplating() {
|
| + add(new _TemplatingNodeValidator());
|
| + }
|
|
|
| /**
|
| - * Indicates the document and all subresources have been loaded.
|
| + * Add an additional validator to the current list of validators.
|
| + *
|
| + * Elements and attributes will be accepted if they are accepted by any
|
| + * validators.
|
| */
|
| - static const String COMPLETE = "complete";
|
| + 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',
|
| + ]);
|
| + }
|
| +
|
| + /**
|
| + * 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'`.
|
| + */
|
| + _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;
|
| + }
|
| +}
|
| +
|
| +class _CustomElementNodeValidator extends _SimpleNodeValidator {
|
| + final bool allowTypeExtension;
|
| + final bool allowCustomTag;
|
| +
|
| + _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.
|
|
|
|
|
| +// 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 class for representing two-dimensional rectangles.
|
| + * 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.
|
| */
|
| -class Rect {
|
| - final num left;
|
| - final num top;
|
| - final num width;
|
| - final num height;
|
| +// TODO(jmesserly): find a better home for this type.
|
| +@Experimental
|
| +class PathObserver {
|
| + /** The object being observed. */
|
| + final object;
|
|
|
| - const Rect(this.left, this.top, this.width, this.height);
|
| + /** The path string. */
|
| + final String path;
|
|
|
| - 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;
|
| - }
|
| + /** True if the path is valid, otherwise false. */
|
| + final bool _isValid;
|
|
|
| - return new Rect(left, top, width, height);
|
| - }
|
| + // TODO(jmesserly): same issue here as ObservableMixin: is there an easier
|
| + // way to get a broadcast stream?
|
| + StreamController _values;
|
| + Stream _valueStream;
|
|
|
| - num get right => left + width;
|
| - num get bottom => top + height;
|
| + _PropertyObserver _observer, _lastObserver;
|
|
|
| - // NOTE! All code below should be common with Rect.
|
| - // TODO: implement with mixins when available.
|
| + Object _lastValue;
|
| + bool _scheduled = false;
|
|
|
| - String toString() {
|
| - return '($left, $top, $width, $height)';
|
| + /**
|
| + * 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) {
|
| +
|
| + // 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));
|
| + }
|
| +
|
| + // 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;
|
| + }
|
| + }
|
| }
|
|
|
| - bool operator ==(other) {
|
| - if (other is !Rect) return false;
|
| - return left == other.left && top == other.top && width == other.width &&
|
| - height == other.height;
|
| + // 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.
|
| /**
|
| - * Computes the intersection of this rectangle and the rectangle parameter.
|
| - * Returns null if there is no intersection.
|
| + * Gets the stream of values that were observed at this path.
|
| + * This returns a single-subscription stream.
|
| */
|
| - Rect intersection(Rect rect) {
|
| - var x0 = max(left, rect.left);
|
| - var x1 = min(left + width, rect.left + rect.width);
|
| + Stream get values => _values.stream;
|
|
|
| - if (x0 <= x1) {
|
| - var y0 = max(top, rect.top);
|
| - var y1 = min(top + height, rect.top + rect.height);
|
| + /** Force synchronous delivery of [values]. */
|
| + void _deliverValues() {
|
| + _scheduled = false;
|
|
|
| - if (y0 <= y1) {
|
| - return new Rect(x0, y0, x1 - x0, y1 - y0);
|
| - }
|
| + var newValue = value;
|
| + if (!identical(_lastValue, newValue)) {
|
| + _values.add(newValue);
|
| + _lastValue = newValue;
|
| }
|
| - return null;
|
| }
|
|
|
| + void _observe() {
|
| + if (_observer != null) {
|
| + _lastValue = value;
|
| + _observer.observe();
|
| + }
|
| + }
|
|
|
| - /**
|
| - * 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);
|
| + void _unobserve() {
|
| + if (_observer != null) _observer.unobserve();
|
| }
|
|
|
| - /**
|
| - * 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);
|
| + void _notifyChange() {
|
| + if (_scheduled) return;
|
| + _scheduled = true;
|
|
|
| - var left = min(this.left, rect.left);
|
| - var top = min(this.top, rect.top);
|
| + // 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);
|
| + }
|
|
|
| - return new Rect(left, top, right - left, bottom - top);
|
| + /** 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;
|
| }
|
|
|
| - /**
|
| - * 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;
|
| + /** 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;
|
| + }
|
| }
|
| +}
|
|
|
| - /**
|
| - * 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;
|
| +// 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;
|
| + }
|
| }
|
|
|
| - 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());
|
| + // TODO(jmesserly): what about length?
|
| + if (object is Map) return object[property];
|
|
|
| - /**
|
| - * Truncates coordinates to integers and returns the result as a new
|
| - * rectangle.
|
| - */
|
| - Rect toInt() => new Rect(left.toInt(), top.toInt(), width.toInt(),
|
| - height.toInt());
|
| + if (object is Observable) return object.getValueWorkaround(property);
|
|
|
| - Point get topLeft => new Point(this.left, this.top);
|
| - Point get bottomRight => new Point(this.left + this.width,
|
| - this.top + this.height);
|
| + return null;
|
| }
|
| -// 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.
|
|
|
| +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;
|
| +}
|
|
|
| -// 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);
|
| +class _PropertyObserver {
|
| + final PathObserver _path;
|
| + final _property;
|
| + final _PropertyObserver _next;
|
|
|
| -/**
|
| - * 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');
|
| - *
|
| - * 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;
|
| + // TODO(jmesserly): would be nice not to store both of these.
|
| + Object _object;
|
| + Object _value;
|
| + StreamSubscription _sub;
|
|
|
| - /**
|
| - * 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;
|
| + _PropertyObserver(this._path, this._property, this._next);
|
|
|
| - /**
|
| - * 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();
|
| -}
|
| + 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();
|
| + }
|
| + }
|
|
|
| -/** The callback used in the [CompoundBinding.combinator] field. */
|
| -@Experimental
|
| -typedef Object CompoundBindingCombinator(Map objects);
|
| + void ensureValue(object) {
|
| + // If we're observing, values should be up to date already.
|
| + if (_sub != null) return;
|
|
|
| -/** 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.
|
| + _object = object;
|
| + value = _getObjectProperty(object, _property);
|
| + }
|
|
|
| - /** The first node of this template instantiation. */
|
| - final Node firstNode;
|
| + void observe() {
|
| + if (_object is Observable) {
|
| + assert(_sub == null);
|
| + _sub = (_object as Observable).changes.listen(_onChange);
|
| + }
|
| + if (_next != null) _next.observe();
|
| + }
|
|
|
| - /**
|
| - * 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;
|
| + void unobserve() {
|
| + if (_sub == null) return;
|
|
|
| - /** The model used to instantiate the template. */
|
| - final model;
|
| + _sub.cancel();
|
| + _sub = null;
|
| + if (_next != null) _next.unobserve();
|
| + }
|
|
|
| - TemplateInstance(this.firstNode, this.lastNode, this.model);
|
| + 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;
|
| + }
|
| + }
|
| + }
|
| }
|
|
|
| -/**
|
| - * 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;
|
| +// From: https://github.com/rafaelw/ChangeSummary/blob/master/change_summary.js
|
|
|
| - // 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;
|
| +const _pathIndentPart = r'[$a-z0-9_]+[$a-z0-9_\d]*';
|
| +final _pathRegExp = new RegExp('^'
|
| + '(?:#?' + _pathIndentPart + ')?'
|
| + '(?:'
|
| + '(?:\\.' + _pathIndentPart + ')'
|
| + ')*'
|
| + r'$', caseSensitive: false);
|
|
|
| - 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;
|
| - }
|
| +final _spacesRegExp = new RegExp(r'\s');
|
|
|
| - CompoundBindingCombinator get combinator => _combinator;
|
| +bool _isPathValid(String s) {
|
| + s = s.replaceAll(_spacesRegExp, '');
|
|
|
| - set combinator(CompoundBindingCombinator combinator) {
|
| - _combinator = combinator;
|
| - if (combinator != null) _scheduleResolve();
|
| - }
|
| + 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 const _VALUE = const Symbol('value');
|
|
|
| - get value => _value;
|
| +/**
|
| + * A utility class for representing two-dimensional positions.
|
| + */
|
| +class Point {
|
| + final num x;
|
| + final num y;
|
|
|
| - void set value(newValue) {
|
| - _value = notifyPropertyChange(_VALUE, _value, newValue);
|
| - }
|
| + const Point([num x = 0, num y = 0]): x = x, y = y;
|
|
|
| - // 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;
|
| + String toString() => '($x, $y)';
|
| +
|
| + bool operator ==(other) {
|
| + if (other is !Point) return false;
|
| + return x == other.x && y == other.y;
|
| }
|
|
|
| - void bind(name, model, String path) {
|
| - unbind(name);
|
| + Point operator +(Point other) {
|
| + return new Point(x + other.x, y + other.y);
|
| + }
|
|
|
| - _bindings[name] = new PathObserver(model, path).bindSync((value) {
|
| - _values[name] = value;
|
| - _scheduleResolve();
|
| - });
|
| + Point operator -(Point other) {
|
| + return new Point(x - other.x, y - other.y);
|
| }
|
|
|
| - void unbind(name, {bool suppressResolve: false}) {
|
| - var binding = _bindings.remove(name);
|
| - if (binding == null) return;
|
| + Point operator *(num factor) {
|
| + return new Point(x * factor, y * factor);
|
| + }
|
|
|
| - binding.cancel();
|
| - _values.remove(name);
|
| - if (!suppressResolve) _scheduleResolve();
|
| + /**
|
| + * 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);
|
| }
|
|
|
| - // 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);
|
| + /**
|
| + * 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;
|
| }
|
|
|
| - void resolve() {
|
| - if (_disposed) return;
|
| - _scheduled = false;
|
| + Point ceil() => new Point(x.ceil(), y.ceil());
|
| + Point floor() => new Point(x.floor(), y.floor());
|
| + Point round() => new Point(x.round(), y.round());
|
|
|
| - if (_combinator == null) {
|
| - throw new StateError(
|
| - 'CompoundBinding attempted to resolve without a combinator');
|
| - }
|
| + /**
|
| + * 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.
|
|
|
| - value = _combinator(_values);
|
| - }
|
|
|
| - void dispose() {
|
| - for (var binding in _bindings.values) {
|
| - binding.cancel();
|
| - }
|
| - _bindings.clear();
|
| - _values.clear();
|
| +/**
|
| + * 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";
|
|
|
| - _disposed = true;
|
| - value = null;
|
| - }
|
| + /**
|
| + * Indicates the document is finished parsing but is still loading
|
| + * subresources.
|
| + */
|
| + static const String INTERACTIVE = "interactive";
|
| +
|
| + /**
|
| + * 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.
|
|
|
| -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);
|
| - }
|
| +/**
|
| + * A class for representing two-dimensional rectangles.
|
| + */
|
| +class Rect {
|
| + final num left;
|
| + final num top;
|
| + final num width;
|
| + final num height;
|
|
|
| - void valueChanged(newValue);
|
| + const Rect(this.left, this.top, this.width, this.height);
|
|
|
| - void updateBinding(e);
|
| + 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 unbind() {
|
| - binding = null;
|
| - _pathSub.cancel();
|
| - _eventSub.cancel();
|
| + return new Rect(left, top, width, height);
|
| }
|
|
|
| + num get right => left + width;
|
| + num get bottom => top + height;
|
|
|
| - 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;
|
| - }
|
| - }
|
| -}
|
| -
|
| -class _ValueBinding extends _InputBinding {
|
| - _ValueBinding(element, model, path) : super(element, model, path);
|
| + // NOTE! All code below should be common with Rect.
|
| + // TODO: implement with mixins when available.
|
|
|
| - void valueChanged(value) {
|
| - element.value = value == null ? '' : '$value';
|
| + String toString() {
|
| + return '($left, $top, $width, $height)';
|
| }
|
|
|
| - void updateBinding(e) {
|
| - binding.value = element.value;
|
| + bool operator ==(other) {
|
| + if (other is !Rect) return false;
|
| + return left == other.left && top == other.top && width == other.width &&
|
| + height == other.height;
|
| }
|
| -}
|
| -
|
| -class _CheckedBinding extends _InputBinding {
|
| - _CheckedBinding(element, model, path) : super(element, model, path);
|
|
|
| - void valueChanged(value) {
|
| - element.checked = _Bindings._toBoolean(value);
|
| - }
|
| + /**
|
| + * Computes the intersection of this rectangle and the rectangle parameter.
|
| + * Returns null if there is no intersection.
|
| + */
|
| + Rect intersection(Rect rect) {
|
| + var x0 = max(left, rect.left);
|
| + var x1 = min(left + width, rect.left + rect.width);
|
|
|
| - void updateBinding(e) {
|
| - binding.value = element.checked;
|
| + if (x0 <= x1) {
|
| + var y0 = max(top, rect.top);
|
| + var y1 = min(top + height, rect.top + rect.height);
|
|
|
| - // 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;
|
| - }
|
| + if (y0 <= y1) {
|
| + return new Rect(x0, y0, x1 - x0, y1 - y0);
|
| }
|
| }
|
| + return null;
|
| }
|
|
|
| - // |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);
|
| + /**
|
| + * 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);
|
| }
|
| -}
|
|
|
| -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;
|
| + /**
|
| + * 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 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);
|
| - }
|
| - }
|
| + var left = min(this.left, rect.left);
|
| + var top = min(this.top, rect.top);
|
|
|
| - for (var c = node.$dom_firstChild; c != null; c = c.nextNode) {
|
| - clone.append(_createDeepCloneAndDecorateTemplates(c, syntax));
|
| - }
|
| - return clone;
|
| + return new Rect(left, top, right - left, bottom - top);
|
| }
|
|
|
| - // 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;
|
| + /**
|
| + * 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;
|
| }
|
|
|
| - 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;
|
| + /**
|
| + * 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;
|
| }
|
|
|
| - static void _liftNonNativeChildrenIntoContent(Element templateElement) {
|
| - var content = templateElement.content;
|
| -
|
| - if (!templateElement._isAttributeTemplate) {
|
| - var child;
|
| - while ((child = templateElement.$dom_firstChild) != null) {
|
| - content.append(child);
|
| - }
|
| - return;
|
| - }
|
| + 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());
|
|
|
| - // 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);
|
| - }
|
| + /**
|
| + * Truncates coordinates to integers and returns the result as a new
|
| + * rectangle.
|
| + */
|
| + Rect toInt() => new Rect(left.toInt(), top.toInt(), width.toInt(),
|
| + height.toInt());
|
|
|
| - static void _bootstrapTemplatesRecursivelyFrom(Node node) {
|
| - void bootstrap(template) {
|
| - if (!TemplateElement.decorate(template)) {
|
| - _bootstrapTemplatesRecursivelyFrom(template.content);
|
| - }
|
| - }
|
| + 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.
|
|
|
| - // 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);
|
| +// Patch file for the dart:isolate library.
|
|
|
| - descendents.forEach(bootstrap);
|
| - }
|
|
|
| - static final String _allTemplatesSelectors = 'template, option[template], ' +
|
| - Element._TABLE_TAGS.keys.map((k) => "$k[template]").join(", ");
|
| +/********************************************************
|
| + Inserted from lib/isolate/serialization.dart
|
| + ********************************************************/
|
|
|
| - 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);
|
| - }
|
| +class _MessageTraverserVisitedMap {
|
|
|
| - for (var c = node.$dom_firstChild; c != null; c = c.nextNode) {
|
| - _addBindings(c, model, syntax);
|
| - }
|
| - }
|
| + operator[](var object) => null;
|
| + void operator[]=(var object, var info) { }
|
|
|
| - 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);
|
| - });
|
| - }
|
| + void reset() { }
|
| + void cleanup() { }
|
|
|
| - static void _parseAndBind(Node node, String name, String text, model,
|
| - CustomBindingSyntax syntax) {
|
| +}
|
|
|
| - var tokens = _parseMustacheTokens(text);
|
| - if (tokens.length == 0 || (tokens.length == 1 && tokens[0].isText)) {
|
| - return;
|
| - }
|
| +/** Abstract visitor for dart objects that can be sent as isolate messages. */
|
| +abstract class _MessageTraverser {
|
|
|
| - // If this is a custom element, give the .xtag a change to bind.
|
| - node = _nodeOrCustom(node);
|
| + _MessageTraverserVisitedMap _visited;
|
| + _MessageTraverser() : _visited = new _MessageTraverserVisitedMap();
|
|
|
| - if (tokens.length == 1 && tokens[0].isBinding) {
|
| - _bindOrDelegate(node, name, model, tokens[0].value, syntax);
|
| - return;
|
| + /** 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;
|
| + }
|
|
|
| - 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);
|
| - }
|
| - }
|
| + _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);
|
|
|
| - replacementBinding.combinator = (values) {
|
| - var newValue = new StringBuffer();
|
| + // Overridable fallback.
|
| + return visitObject(x);
|
| + }
|
|
|
| - 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);
|
| - }
|
| - }
|
| - }
|
| + visitPrimitive(x);
|
| + visitList(List x);
|
| + visitMap(Map x);
|
| + visitSendPort(SendPort x);
|
| + visitSendPortSync(SendPortSync x);
|
|
|
| - return newValue.toString();
|
| - };
|
| + visitObject(Object x) {
|
| + // TODO(floitsch): make this a real exception. (which one)?
|
| + throw "Message serialization: Illegal value $x passed";
|
| + }
|
|
|
| - node.bind(name, replacementBinding, 'value');
|
| + static bool isPrimitive(x) {
|
| + return (x == null) || (x is String) || (x is num) || (x is bool);
|
| }
|
| +}
|
|
|
| - static void _bindOrDelegate(node, name, model, String path,
|
| - CustomBindingSyntax syntax) {
|
|
|
| - if (syntax != null) {
|
| - var delegateBinding = syntax.getBinding(model, path, name, node);
|
| - if (delegateBinding != null) {
|
| - model = delegateBinding;
|
| - path = 'value';
|
| - }
|
| - }
|
| +/** Visitor that serializes a message as a JSON array. */
|
| +abstract class _Serializer extends _MessageTraverser {
|
| + int _nextFreeRefId = 0;
|
|
|
| - node.bind(name, model, path);
|
| + visitPrimitive(x) => x;
|
| +
|
| + 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];
|
| }
|
|
|
| - /**
|
| - * 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.
|
| - */
|
| - // TODO(jmesserly): remove this when we can extend Element for real.
|
| - static _nodeOrCustom(node) => node is Element ? node.xtag : node;
|
| + visitMap(Map map) {
|
| + int copyId = _visited[map];
|
| + if (copyId != null) return ['ref', copyId];
|
|
|
| - 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;
|
| - }
|
| + 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];
|
| + }
|
|
|
| - var value = s.substring(lastIndex, index).trim();
|
| - result.add(new _BindingToken(value, isBinding: true));
|
| - lastIndex = index + 2;
|
| - }
|
| + _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;
|
| }
|
| +}
|
|
|
| - static void _addTemplateInstanceRecord(fragment, model) {
|
| - if (fragment.$dom_firstChild == null) {
|
| - return;
|
| - }
|
| +/** Deserializes arrays created with [_Serializer]. */
|
| +abstract class _Deserializer {
|
| + Map<int, dynamic> _deserialized;
|
|
|
| - var instanceRecord = new TemplateInstance(
|
| - fragment.$dom_firstChild, fragment.$dom_lastChild, model);
|
| + _Deserializer();
|
|
|
| - var node = instanceRecord.firstNode;
|
| - while (node != null) {
|
| - node._templateInstance = instanceRecord;
|
| - node = node.nextNode;
|
| + static bool isPrimitive(x) {
|
| + return (x == null) || (x is String) || (x is num) || (x is bool);
|
| + }
|
| +
|
| + deserialize(x) {
|
| + if (isPrimitive(x)) return x;
|
| + // TODO(floitsch): this should be new HashMap<int, dynamic>()
|
| + _deserialized = new HashMap();
|
| + return _deserializeHelper(x);
|
| + }
|
| +
|
| + _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);
|
| }
|
| }
|
|
|
| - static void _removeAllBindingsRecursively(Node node) {
|
| - _nodeOrCustom(node).unbindAll();
|
| - for (var c = node.$dom_firstChild; c != null; c = c.nextNode) {
|
| - _removeAllBindingsRecursively(c);
|
| + _deserializeRef(List x) {
|
| + int id = x[1];
|
| + var result = _deserialized[id];
|
| + assert(result != null);
|
| + return result;
|
| + }
|
| +
|
| + 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;
|
| }
|
|
|
| - 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;
|
| - }
|
| + 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;
|
| }
|
| - child.remove();
|
| - _removeAllBindingsRecursively(child);
|
| + return result;
|
| }
|
| -}
|
| -
|
| -class _BindingToken {
|
| - final String value;
|
| - final bool isBinding;
|
|
|
| - _BindingToken(this.value, {this.isBinding: false});
|
| + deserializeSendPort(List x);
|
|
|
| - bool get isText => !isBinding;
|
| + deserializeObject(List x) {
|
| + // TODO(floitsch): Use real exception (which one?).
|
| + throw "Unexpected serialized object";
|
| + }
|
| }
|
|
|
| -class _TemplateIterator {
|
| - final Element _templateElement;
|
| - final List<Node> terminators = [];
|
| - final CompoundBinding inputs;
|
| - List iteratedValue;
|
| +// 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.
|
|
|
| - StreamSubscription _sub;
|
| - StreamSubscription _valueBinding;
|
|
|
| - _TemplateIterator(this._templateElement)
|
| - : inputs = new CompoundBinding(resolveInputs) {
|
| +// This code is a port of Model-Driven-Views:
|
| +// https://github.com/polymer-project/mdv
|
| +// The code mostly comes from src/template_element.js
|
|
|
| - _valueBinding = new PathObserver(inputs, 'value').bindSync(valueChanged);
|
| - }
|
| +typedef void _ChangeHandler(value);
|
|
|
| - static Object resolveInputs(Map values) {
|
| - if (values.containsKey('if') && !_Bindings._toBoolean(values['if'])) {
|
| - return null;
|
| - }
|
| +/**
|
| + * 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');
|
| + *
|
| + * 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;
|
|
|
| - if (values.containsKey('repeat')) {
|
| - return values['repeat'];
|
| - }
|
| + /**
|
| + * 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;
|
|
|
| - if (values.containsKey('bind')) {
|
| - return [values['bind']];
|
| - }
|
| + /**
|
| + * 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();
|
| +}
|
|
|
| - return null;
|
| - }
|
| +/** The callback used in the [CompoundBinding.combinator] field. */
|
| +@Experimental
|
| +typedef Object CompoundBindingCombinator(Map objects);
|
|
|
| - void valueChanged(value) {
|
| - clear();
|
| - if (value is! List) return;
|
| +/** 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.
|
|
|
| - iteratedValue = value;
|
| + /** The first node of this template instantiation. */
|
| + final Node firstNode;
|
|
|
| - if (value is Observable) {
|
| - _sub = value.changes.listen(_handleChanges);
|
| - }
|
| + /**
|
| + * 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 len = iteratedValue.length;
|
| - if (len > 0) {
|
| - _handleChanges([new ListChangeRecord(0, addedCount: len)]);
|
| - }
|
| - }
|
| + /** The model used to instantiate the template. */
|
| + final model;
|
|
|
| - Node getTerminatorAt(int index) {
|
| - if (index == -1) return _templateElement;
|
| - var terminator = terminators[index];
|
| - if (terminator is! Element) return terminator;
|
| + TemplateInstance(this.firstNode, this.lastNode, this.model);
|
| +}
|
|
|
| - var subIterator = terminator._templateIterator;
|
| - if (subIterator == null) return terminator;
|
| +/**
|
| + * 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;
|
|
|
| - return subIterator.getTerminatorAt(subIterator.terminators.length - 1);
|
| + // 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;
|
| +
|
| + 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 insertInstanceAt(int index, Node fragment) {
|
| - var previousTerminator = getTerminatorAt(index - 1);
|
| - var terminator = fragment.$dom_lastChild;
|
| - if (terminator == null) terminator = previousTerminator;
|
| + CompoundBindingCombinator get combinator => _combinator;
|
|
|
| - terminators.insert(index, terminator);
|
| - var parent = _templateElement.parentNode;
|
| - parent.insertBefore(fragment, previousTerminator.nextNode);
|
| + set combinator(CompoundBindingCombinator combinator) {
|
| + _combinator = combinator;
|
| + if (combinator != null) _scheduleResolve();
|
| }
|
|
|
| - void removeInstanceAt(int index) {
|
| - var previousTerminator = getTerminatorAt(index - 1);
|
| - var terminator = getTerminatorAt(index);
|
| - terminators.removeAt(index);
|
| + static const _VALUE = const Symbol('value');
|
|
|
| - var parent = _templateElement.parentNode;
|
| - while (terminator != previousTerminator) {
|
| - var node = terminator;
|
| - terminator = node.previousNode;
|
| - _Bindings._removeChild(parent, node);
|
| - }
|
| + get value => _value;
|
| +
|
| + void set value(newValue) {
|
| + _value = notifyPropertyChange(_VALUE, _value, newValue);
|
| + }
|
| +
|
| + // 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 removeAllInstances() {
|
| - if (terminators.length == 0) return;
|
| + void bind(name, model, String path) {
|
| + unbind(name);
|
| +
|
| + _bindings[name] = new PathObserver(model, path).bindSync((value) {
|
| + _values[name] = value;
|
| + _scheduleResolve();
|
| + });
|
| + }
|
|
|
| - var previousTerminator = _templateElement;
|
| - var terminator = getTerminatorAt(terminators.length - 1);
|
| - terminators.length = 0;
|
| + void unbind(name, {bool suppressResolve: false}) {
|
| + var binding = _bindings.remove(name);
|
| + if (binding == null) return;
|
|
|
| - var parent = _templateElement.parentNode;
|
| - while (terminator != previousTerminator) {
|
| - var node = terminator;
|
| - terminator = node.previousNode;
|
| - _Bindings._removeChild(parent, node);
|
| - }
|
| + binding.cancel();
|
| + _values.remove(name);
|
| + if (!suppressResolve) _scheduleResolve();
|
| }
|
|
|
| - void clear() {
|
| - unobserve();
|
| - removeAllInstances();
|
| - iteratedValue = null;
|
| + // 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);
|
| }
|
|
|
| - getInstanceModel(model, syntax) {
|
| - if (syntax != null) {
|
| - return syntax.getInstanceModel(_templateElement, model);
|
| + void resolve() {
|
| + if (_disposed) return;
|
| + _scheduled = false;
|
| +
|
| + if (_combinator == null) {
|
| + throw new StateError(
|
| + 'CompoundBinding attempted to resolve without a combinator');
|
| }
|
| - return model;
|
| +
|
| + value = _combinator(_values);
|
| }
|
|
|
| - getInstanceFragment(syntax) {
|
| - if (syntax != null) {
|
| - return syntax.getInstanceFragment(_templateElement);
|
| + void dispose() {
|
| + for (var binding in _bindings.values) {
|
| + binding.cancel();
|
| }
|
| - return _templateElement.createInstance();
|
| - }
|
| + _bindings.clear();
|
| + _values.clear();
|
|
|
| - void _handleChanges(List<ListChangeRecord> splices) {
|
| - var syntax = TemplateElement.syntax[_templateElement.attributes['syntax']];
|
| + _disposed = true;
|
| + value = null;
|
| + }
|
| +}
|
|
|
| - for (var splice in splices) {
|
| - if (splice is! ListChangeRecord) continue;
|
| +abstract class _InputBinding {
|
| + final InputElement element;
|
| + PathObserver binding;
|
| + StreamSubscription _pathSub;
|
| + StreamSubscription _eventSub;
|
|
|
| - for (int i = 0; i < splice.removedCount; i++) {
|
| - removeInstanceAt(splice.index);
|
| - }
|
| + _InputBinding(this.element, model, String path) {
|
| + binding = new PathObserver(model, path);
|
| + _pathSub = binding.bindSync(valueChanged);
|
| + _eventSub = _getStreamForInputType(element).listen(updateBinding);
|
| + }
|
|
|
| - for (var addIndex = splice.index;
|
| - addIndex < splice.index + splice.addedCount;
|
| - addIndex++) {
|
| + void valueChanged(newValue);
|
|
|
| - var model = getInstanceModel(iteratedValue[addIndex], syntax);
|
| + void updateBinding(e);
|
|
|
| - var fragment = getInstanceFragment(syntax);
|
| + void unbind() {
|
| + binding = null;
|
| + _pathSub.cancel();
|
| + _eventSub.cancel();
|
| + }
|
|
|
| - _Bindings._addBindings(fragment, model, syntax);
|
| - _Bindings._addTemplateInstanceRecord(fragment, model);
|
|
|
| - insertInstanceAt(addIndex, fragment);
|
| - }
|
| + 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 unobserve() {
|
| - if (_sub == null) return;
|
| - _sub.cancel();
|
| - _sub = null;
|
| +class _ValueBinding extends _InputBinding {
|
| + _ValueBinding(element, model, path) : super(element, model, path);
|
| +
|
| + void valueChanged(value) {
|
| + element.value = value == null ? '' : '$value';
|
| }
|
|
|
| - void abandon() {
|
| - unobserve();
|
| - _valueBinding.cancel();
|
| - inputs.dispose();
|
| + void updateBinding(e) {
|
| + binding.value = element.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.
|
|
|
| +class _CheckedBinding extends _InputBinding {
|
| + _CheckedBinding(element, model, path) : super(element, model, path);
|
|
|
| -class _HttpRequestUtils {
|
| -
|
| - // 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);
|
| + void valueChanged(value) {
|
| + element.checked = _Bindings._toBoolean(value);
|
| + }
|
|
|
| - request.withCredentials = withCredentials;
|
| + void updateBinding(e) {
|
| + binding.value = element.checked;
|
|
|
| - request.onReadyStateChange.listen((e) {
|
| - if (request.readyState == HttpRequest.DONE) {
|
| - onComplete(request);
|
| + // 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;
|
| + }
|
| }
|
| - });
|
| -
|
| - request.send();
|
| -
|
| - return request;
|
| + }
|
| }
|
| -}
|
| -// 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.
|
|
|
| + // |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);
|
| + }
|
| + }
|
|
|
| -_serialize(var message) {
|
| - return new _JsSerializer().traverse(message);
|
| + // 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 _JsSerializer extends _Serializer {
|
| +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;
|
|
|
| - 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";
|
| - }
|
| + 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);
|
| + }
|
| + }
|
|
|
| - visitJsSendPortSync(_JsSendPortSync x) {
|
| - return [ 'sendport', 'nativejs', x._id ];
|
| + for (var c = node.$dom_firstChild; c != null; c = c.nextNode) {
|
| + clone.append(_createDeepCloneAndDecorateTemplates(c, syntax));
|
| + }
|
| + return clone;
|
| }
|
|
|
| - visitLocalSendPortSync(_LocalSendPortSync x) {
|
| - return [ 'sendport', 'dart',
|
| - ReceivePortSync._isolateId, x._receivePort._portId ];
|
| + // 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;
|
| }
|
|
|
| - visitSendPort(SendPort x) {
|
| - throw new UnimplementedError('Asynchronous send port not yet implemented.');
|
| - }
|
| + 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;
|
| + }
|
| + }
|
|
|
| - visitRemoteSendPortSync(_RemoteSendPortSync x) {
|
| - return [ 'sendport', 'dart', x._isolateId, x._portId ];
|
| + return clone;
|
| }
|
| -}
|
| -
|
| -_deserialize(var message) {
|
| - return new _JsDeserializer().deserialize(message);
|
| -}
|
| -
|
|
|
| -class _JsDeserializer extends _Deserializer {
|
| + static void _liftNonNativeChildrenIntoContent(Element templateElement) {
|
| + var content = templateElement.content;
|
|
|
| - static const _UNSPECIFIED = const Object();
|
| + if (!templateElement._isAttributeTemplate) {
|
| + var child;
|
| + while ((child = templateElement.$dom_firstChild) != null) {
|
| + content.append(child);
|
| + }
|
| + return;
|
| + }
|
|
|
| - 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';
|
| + // 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);
|
| }
|
| -}
|
| -
|
| -// The receiver is JS.
|
| -class _JsSendPortSync implements SendPortSync {
|
|
|
| - final num _id;
|
| - _JsSendPortSync(this._id);
|
| + static void _bootstrapTemplatesRecursivelyFrom(Node node) {
|
| + void bootstrap(template) {
|
| + if (!TemplateElement.decorate(template)) {
|
| + _bootstrapTemplatesRecursivelyFrom(template.content);
|
| + }
|
| + }
|
|
|
| - callSync(var message) {
|
| - var serialized = _serialize(message);
|
| - var result = _callPortSync(_id, serialized);
|
| - return _deserialize(result);
|
| - }
|
| + // 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==(var other) {
|
| - return (other is _JsSendPortSync) && (_id == other._id);
|
| + descendents.forEach(bootstrap);
|
| }
|
|
|
| - int get hashCode => _id;
|
| -}
|
| -
|
| -// TODO(vsm): Differentiate between Dart2Js and Dartium isolates.
|
| -// The receiver is a different Dart isolate, compiled to JS.
|
| -class _RemoteSendPortSync implements SendPortSync {
|
| + static final String _allTemplatesSelectors = 'template, option[template], ' +
|
| + Element._TABLE_TAGS.keys.map((k) => "$k[template]").join(", ");
|
|
|
| - int _isolateId;
|
| - int _portId;
|
| - _RemoteSendPortSync(this._isolateId, this._portId);
|
| + 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);
|
| + }
|
|
|
| - callSync(var message) {
|
| - var serialized = _serialize(message);
|
| - var result = _call(_isolateId, _portId, serialized);
|
| - return _deserialize(result);
|
| + for (var c = node.$dom_firstChild; c != null; c = c.nextNode) {
|
| + _addBindings(c, model, syntax);
|
| + }
|
| }
|
|
|
| - 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));
|
| + 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);
|
| });
|
| - _dispatchEvent(target, [source, message]);
|
| - return result;
|
| }
|
|
|
| - bool operator==(var other) {
|
| - return (other is _RemoteSendPortSync) && (_isolateId == other._isolateId)
|
| - && (_portId == other._portId);
|
| - }
|
| + static void _parseAndBind(Node node, String name, String text, model,
|
| + CustomBindingSyntax syntax) {
|
|
|
| - int get hashCode => _isolateId >> 16 + _portId;
|
| -}
|
| + var tokens = _parseMustacheTokens(text);
|
| + if (tokens.length == 0 || (tokens.length == 1 && tokens[0].isText)) {
|
| + return;
|
| + }
|
|
|
| -// The receiver is in the same Dart isolate, compiled to JS.
|
| -class _LocalSendPortSync implements SendPortSync {
|
| + // If this is a custom element, give the .xtag a change to bind.
|
| + node = _nodeOrCustom(node);
|
|
|
| - ReceivePortSync _receivePort;
|
| + if (tokens.length == 1 && tokens[0].isBinding) {
|
| + _bindOrDelegate(node, name, model, tokens[0].value, syntax);
|
| + return;
|
| + }
|
|
|
| - _LocalSendPortSync._internal(this._receivePort);
|
| + 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);
|
| + }
|
| + }
|
|
|
| - 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));
|
| - }
|
| + replacementBinding.combinator = (values) {
|
| + var newValue = new StringBuffer();
|
|
|
| - bool operator==(var other) {
|
| - return (other is _LocalSendPortSync)
|
| - && (_receivePort == other._receivePort);
|
| + 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();
|
| + };
|
| +
|
| + node.bind(name, replacementBinding, 'value');
|
| }
|
|
|
| - int get hashCode => _receivePort.hashCode;
|
| -}
|
| + static void _bindOrDelegate(node, name, model, String path,
|
| + CustomBindingSyntax syntax) {
|
|
|
| -// 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.
|
| + if (syntax != null) {
|
| + var delegateBinding = syntax.getBinding(model, path, name, node);
|
| + if (delegateBinding != null) {
|
| + model = delegateBinding;
|
| + path = 'value';
|
| + }
|
| + }
|
|
|
| -// 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 {
|
| + node.bind(name, model, path);
|
| + }
|
|
|
| - static Map<int, ReceivePortSync> _portMap;
|
| - static int _portIdCount;
|
| - static int _cachedIsolateId;
|
| + /**
|
| + * 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.
|
| + */
|
| + // TODO(jmesserly): remove this when we can extend Element for real.
|
| + static _nodeOrCustom(node) => node is Element ? node.xtag : node;
|
|
|
| - num _portId;
|
| - Function _callback;
|
| - StreamSubscription _portSubscription;
|
| + 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;
|
| + }
|
|
|
| - ReceivePortSync() {
|
| - if (_portIdCount == null) {
|
| - _portIdCount = 0;
|
| - _portMap = new Map<int, ReceivePortSync>();
|
| + var value = s.substring(lastIndex, index).trim();
|
| + result.add(new _BindingToken(value, isBinding: true));
|
| + lastIndex = index + 2;
|
| + }
|
| }
|
| - _portId = _portIdCount++;
|
| - _portMap[_portId] = this;
|
| + return result;
|
| }
|
|
|
| - static int get _isolateId {
|
| - // TODO(vsm): Make this coherent with existing isolate code.
|
| - if (_cachedIsolateId == null) {
|
| - _cachedIsolateId = _getNewIsolateId();
|
| + static void _addTemplateInstanceRecord(fragment, model) {
|
| + if (fragment.$dom_firstChild == null) {
|
| + return;
|
| }
|
| - return _cachedIsolateId;
|
| - }
|
|
|
| - static String _getListenerName(isolateId, portId) =>
|
| - 'dart-port-$isolateId-$portId';
|
| - String get _listenerName => _getListenerName(_isolateId, _portId);
|
| + var instanceRecord = new TemplateInstance(
|
| + fragment.$dom_firstChild, fragment.$dom_lastChild, model);
|
|
|
| - 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));
|
| - });
|
| + var node = instanceRecord.firstNode;
|
| + while (node != null) {
|
| + node._templateInstance = instanceRecord;
|
| + node = node.nextNode;
|
| }
|
| }
|
|
|
| - void close() {
|
| - _portMap.remove(_portId);
|
| - if (_portSubscription != null) _portSubscription.cancel();
|
| - }
|
| -
|
| - SendPortSync toSendPort() {
|
| - return new _LocalSendPortSync._internal(this);
|
| + static void _removeAllBindingsRecursively(Node node) {
|
| + _nodeOrCustom(node).unbindAll();
|
| + for (var c = node.$dom_firstChild; c != null; c = c.nextNode) {
|
| + _removeAllBindingsRecursively(c);
|
| + }
|
| }
|
|
|
| - static SendPortSync _lookup(int isolateId, int portId) {
|
| - if (isolateId == _isolateId) {
|
| - return _portMap[portId].toSendPort();
|
| - } else {
|
| - return new _RemoteSendPortSync(isolateId, 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);
|
| }
|
| }
|
|
|
| -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);
|
| -}
|
| +class _BindingToken {
|
| + final String value;
|
| + final bool isBinding;
|
|
|
| -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.
|
| + _BindingToken(this.value, {this.isBinding: false});
|
|
|
| + bool get isText => !isBinding;
|
| +}
|
|
|
| -typedef void _MicrotaskCallback();
|
| +class _TemplateIterator {
|
| + final Element _templateElement;
|
| + final List<Node> terminators = [];
|
| + final CompoundBinding inputs;
|
| + List iteratedValue;
|
|
|
| -/**
|
| - * This class attempts to invoke a callback as soon as the current event stack
|
| - * unwinds, but before the browser repaints.
|
| - */
|
| -abstract class _MicrotaskScheduler {
|
| - bool _nextMicrotaskFrameScheduled = false;
|
| - final _MicrotaskCallback _callback;
|
| + StreamSubscription _sub;
|
| + StreamSubscription _valueBinding;
|
|
|
| - _MicrotaskScheduler(this._callback);
|
| + _TemplateIterator(this._templateElement)
|
| + : inputs = new CompoundBinding(resolveInputs) {
|
|
|
| - /**
|
| - * 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);
|
| + _valueBinding = new PathObserver(inputs, 'value').bindSync(valueChanged);
|
| }
|
|
|
| - /**
|
| - * Schedules a microtask callback if one has not been scheduled already.
|
| - */
|
| - void maybeSchedule() {
|
| - if (this._nextMicrotaskFrameScheduled) {
|
| - return;
|
| + static Object resolveInputs(Map values) {
|
| + if (values.containsKey('if') && !_Bindings._toBoolean(values['if'])) {
|
| + return null;
|
| }
|
| - this._nextMicrotaskFrameScheduled = true;
|
| - this._schedule();
|
| - }
|
| -
|
| - /**
|
| - * 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;
|
| + if (values.containsKey('repeat')) {
|
| + return values['repeat'];
|
| }
|
| - _nextMicrotaskFrameScheduled = false;
|
| - this._callback();
|
| - }
|
| -}
|
|
|
| -/**
|
| - * Scheduler which uses window.postMessage to schedule events.
|
| - */
|
| -class _PostMessageScheduler extends _MicrotaskScheduler {
|
| - const _MICROTASK_MESSAGE = "DART-MICROTASK";
|
| + if (values.containsKey('bind')) {
|
| + return [values['bind']];
|
| + }
|
|
|
| - _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);
|
| + return null;
|
| }
|
|
|
| - void _schedule() {
|
| - window.postMessage(_MICROTASK_MESSAGE, "*");
|
| - }
|
| + void valueChanged(value) {
|
| + clear();
|
| + if (value is! List) return;
|
|
|
| - void _handleMessage(e) {
|
| - this._onCallback();
|
| - }
|
| -}
|
| + iteratedValue = value;
|
|
|
| -/**
|
| - * Scheduler which uses a MutationObserver to schedule events.
|
| - */
|
| -class _MutationObserverScheduler extends _MicrotaskScheduler {
|
| - MutationObserver _observer;
|
| - Element _dummy;
|
| + if (value is Observable) {
|
| + _sub = value.changes.listen(_handleChanges);
|
| + }
|
|
|
| - _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);
|
| + int len = iteratedValue.length;
|
| + if (len > 0) {
|
| + _handleChanges([new ListChangeRecord(0, addedCount: len)]);
|
| + }
|
| }
|
|
|
| - void _schedule() {
|
| - // Toggle it to trigger the mutation event.
|
| - _dummy.hidden = !_dummy.hidden;
|
| - }
|
| + Node getTerminatorAt(int index) {
|
| + if (index == -1) return _templateElement;
|
| + var terminator = terminators[index];
|
| + if (terminator is! Element) return terminator;
|
|
|
| - _handleMutation(List<MutationRecord> mutations, MutationObserver observer) {
|
| - this._onCallback();
|
| + var subIterator = terminator._templateIterator;
|
| + if (subIterator == null) return terminator;
|
| +
|
| + return subIterator.getTerminatorAt(subIterator.terminators.length - 1);
|
| }
|
| -}
|
|
|
| -/**
|
| - * Scheduler which uses window.setImmediate to schedule events.
|
| - */
|
| -class _SetImmediateScheduler extends _MicrotaskScheduler {
|
| - _SetImmediateScheduler(_MicrotaskCallback callback): super(callback);
|
| + void insertInstanceAt(int index, Node fragment) {
|
| + var previousTerminator = getTerminatorAt(index - 1);
|
| + var terminator = fragment.$dom_lastChild;
|
| + if (terminator == null) terminator = previousTerminator;
|
|
|
| - void _schedule() {
|
| - window._setImmediate(_handleImmediate);
|
| + terminators.insert(index, terminator);
|
| + var parent = _templateElement.parentNode;
|
| + parent.insertBefore(fragment, previousTerminator.nextNode);
|
| }
|
|
|
| - void _handleImmediate() {
|
| - this._onCallback();
|
| + 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);
|
| + }
|
| }
|
| -}
|
|
|
| -List<TimeoutHandler> _pendingMicrotasks;
|
| -_MicrotaskScheduler _microtaskScheduler = null;
|
| + void removeAllInstances() {
|
| + if (terminators.length == 0) return;
|
|
|
| -void _maybeScheduleMicrotaskFrame() {
|
| - if (_microtaskScheduler == null) {
|
| - _microtaskScheduler =
|
| - new _MicrotaskScheduler.best(_completeMicrotasks);
|
| - }
|
| - _microtaskScheduler.maybeSchedule();
|
| -}
|
| + var previousTerminator = _templateElement;
|
| + var terminator = getTerminatorAt(terminators.length - 1);
|
| + terminators.length = 0;
|
|
|
| -/**
|
| - * Registers a [callback] which is called after the current execution stack
|
| - * unwinds.
|
| - */
|
| -void _addMicrotaskCallback(TimeoutHandler callback) {
|
| - if (_pendingMicrotasks == null) {
|
| - _pendingMicrotasks = <TimeoutHandler>[];
|
| - _maybeScheduleMicrotaskFrame();
|
| + var parent = _templateElement.parentNode;
|
| + while (terminator != previousTerminator) {
|
| + var node = terminator;
|
| + terminator = node.previousNode;
|
| + _Bindings._removeChild(parent, node);
|
| + }
|
| }
|
| - _pendingMicrotasks.add(callback);
|
| -}
|
|
|
| -
|
| -/**
|
| - * Complete all pending microtasks.
|
| - */
|
| -void _completeMicrotasks() {
|
| - var callbacks = _pendingMicrotasks;
|
| - _pendingMicrotasks = null;
|
| - for (var callback in callbacks) {
|
| - callback();
|
| + void clear() {
|
| + unobserve();
|
| + removeAllInstances();
|
| + iteratedValue = null;
|
| }
|
| -}
|
| -// 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.
|
|
|
| -// Patch file for the dart:isolate library.
|
| + getInstanceModel(model, syntax) {
|
| + if (syntax != null) {
|
| + return syntax.getInstanceModel(_templateElement, model);
|
| + }
|
| + return model;
|
| + }
|
|
|
| + getInstanceFragment(syntax) {
|
| + if (syntax != null) {
|
| + return syntax.getInstanceFragment(_templateElement);
|
| + }
|
| + return _templateElement.createInstance();
|
| + }
|
|
|
| -/********************************************************
|
| - Inserted from lib/isolate/serialization.dart
|
| - ********************************************************/
|
| + void _handleChanges(List<ListChangeRecord> splices) {
|
| + var syntax = TemplateElement.syntax[_templateElement.attributes['syntax']];
|
|
|
| -class _MessageTraverserVisitedMap {
|
| + for (var splice in splices) {
|
| + if (splice is! ListChangeRecord) continue;
|
|
|
| - operator[](var object) => null;
|
| - void operator[]=(var object, var info) { }
|
| + for (int i = 0; i < splice.removedCount; i++) {
|
| + removeInstanceAt(splice.index);
|
| + }
|
|
|
| - void reset() { }
|
| - void cleanup() { }
|
| + for (var addIndex = splice.index;
|
| + addIndex < splice.index + splice.addedCount;
|
| + addIndex++) {
|
|
|
| -}
|
| + var model = getInstanceModel(iteratedValue[addIndex], syntax);
|
|
|
| -/** Abstract visitor for dart objects that can be sent as isolate messages. */
|
| -abstract class _MessageTraverser {
|
| + var fragment = getInstanceFragment(syntax);
|
|
|
| - _MessageTraverserVisitedMap _visited;
|
| - _MessageTraverser() : _visited = new _MessageTraverserVisitedMap();
|
| + _Bindings._addBindings(fragment, model, syntax);
|
| + _Bindings._addTemplateInstanceRecord(fragment, model);
|
|
|
| - /** Visitor's entry point. */
|
| - traverse(var x) {
|
| - if (isPrimitive(x)) return visitPrimitive(x);
|
| - _visited.reset();
|
| - var result;
|
| - try {
|
| - result = _dispatch(x);
|
| - } finally {
|
| - _visited.cleanup();
|
| + insertInstanceAt(addIndex, fragment);
|
| + }
|
| }
|
| - return result;
|
| - }
|
| -
|
| - _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);
|
| -
|
| - // Overridable fallback.
|
| - return visitObject(x);
|
| }
|
|
|
| - visitPrimitive(x);
|
| - visitList(List x);
|
| - visitMap(Map x);
|
| - visitSendPort(SendPort x);
|
| - visitSendPortSync(SendPortSync x);
|
| -
|
| - visitObject(Object x) {
|
| - // TODO(floitsch): make this a real exception. (which one)?
|
| - throw "Message serialization: Illegal value $x passed";
|
| + void unobserve() {
|
| + if (_sub == null) return;
|
| + _sub.cancel();
|
| + _sub = null;
|
| }
|
|
|
| - static bool isPrimitive(x) {
|
| - return (x == null) || (x is String) || (x is num) || (x is bool);
|
| + void abandon() {
|
| + unobserve();
|
| + _valueBinding.cancel();
|
| + inputs.dispose();
|
| }
|
| }
|
| +// 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.
|
|
|
|
|
| -/** Visitor that serializes a message as a JSON array. */
|
| -abstract class _Serializer extends _MessageTraverser {
|
| - int _nextFreeRefId = 0;
|
|
|
| - visitPrimitive(x) => x;
|
| +/**
|
| + * Interface used to validate that only accepted elements and attributes are
|
| + * allowed while parsing HTML strings into DOM nodes.
|
| + */
|
| +abstract class NodeValidator {
|
|
|
| - visitList(List list) {
|
| - int copyId = _visited[list];
|
| - if (copyId != null) return ['ref', copyId];
|
| + /**
|
| + * 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);
|
|
|
| - int id = _nextFreeRefId++;
|
| - _visited[list] = id;
|
| - var jsArray = _serializeList(list);
|
| - // TODO(floitsch): we are losing the generic type.
|
| - return ['list', id, jsArray];
|
| - }
|
| + /**
|
| + * Returns true if the tagName is an accepted type.
|
| + */
|
| + bool allowsElement(Element element);
|
|
|
| - visitMap(Map map) {
|
| - int copyId = _visited[map];
|
| - if (copyId != null) return ['ref', copyId];
|
| + /**
|
| + * Returns true if the attribute is allowed.
|
| + *
|
| + * The attributeName parameter will always be in lowercase.
|
| + *
|
| + * See [allowsElement] for format of tagName.
|
| + */
|
| + bool allowsAttribute(Element element, String attributeName, String value);
|
| +}
|
|
|
| - 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];
|
| - }
|
| +/**
|
| + * 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 {
|
|
|
| - _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;
|
| - }
|
| -}
|
| + /**
|
| + * Constructs a default tree sanitizer which will remove all elements and
|
| + * attributes which are not allowed by the provided validator.
|
| + */
|
| + factory NodeTreeSanitizer(NodeValidator validator) =>
|
| + new _ValidatingTreeSanitizer(validator);
|
|
|
| -/** Deserializes arrays created with [_Serializer]. */
|
| -abstract class _Deserializer {
|
| - Map<int, dynamic> _deserialized;
|
| + /**
|
| + * 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 sanitizeTree(Node node);
|
| +}
|
|
|
| - _Deserializer();
|
| +/**
|
| + * 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.
|
| + */
|
| +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();
|
|
|
| - static bool isPrimitive(x) {
|
| - return (x == null) || (x is String) || (x is num) || (x is bool);
|
| - }
|
| + /**
|
| + * 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);
|
| +}
|
|
|
| - deserialize(x) {
|
| - if (isPrimitive(x)) return x;
|
| - // TODO(floitsch): this should be new HashMap<int, dynamic>()
|
| - _deserialized = new HashMap();
|
| - return _deserializeHelper(x);
|
| - }
|
| +/**
|
| + * 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();
|
| + final Location _loc = window.location;
|
|
|
| - _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);
|
| - }
|
| + bool allowsUri(String uri) {
|
| + _hiddenAnchor.href = uri;
|
| + return _hiddenAnchor.hostname == _loc.hostname &&
|
| + _hiddenAnchor.port == _loc.port &&
|
| + _hiddenAnchor.protocol == _loc.protocol;
|
| }
|
| +}
|
|
|
| - _deserializeRef(List x) {
|
| - int id = x[1];
|
| - var result = _deserialized[id];
|
| - assert(result != null);
|
| - return result;
|
| - }
|
|
|
| - 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]);
|
| +/**
|
| + * Standard tree sanitizer which validates a node tree against the provided
|
| + * validator and removes any nodes or attributes which are not allowed.
|
| + */
|
| +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;
|
| + }
|
| }
|
| - return dartList;
|
| + walk(node);
|
| }
|
|
|
| - 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 sanitizeNode(Node node) {
|
| + switch (node.nodeType) {
|
| + case Node.ELEMENT_NODE:
|
| + Element element = node;
|
| + var attrs = element.attributes;
|
| + if (!validator.allowsElement(element)) {
|
| + element.remove();
|
| + break;
|
| + }
|
|
|
| - deserializeSendPort(List x);
|
| + var isAttr = attrs['is'];
|
| + if (isAttr != null) {
|
| + if (!validator.allowsAttribute(element, 'is', isAttr)) {
|
| + element.remove();
|
| + break;
|
| + }
|
| + }
|
|
|
| - deserializeObject(List x) {
|
| - // TODO(floitsch): Use real exception (which one?).
|
| - throw "Unexpected serialized object";
|
| + // 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);
|
| + }
|
| + }
|
| +
|
| + if (element is TemplateElement) {
|
| + TemplateElement template = element;
|
| + sanitizeTree(template.content);
|
| + }
|
| + 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.
|
| @@ -28948,6 +29966,33 @@ class _WrappedIterator<E> implements Iterator<E> {
|
| // BSD-style license that can be found in the LICENSE file.
|
|
|
|
|
| +class _HttpRequestUtils {
|
| +
|
| + // 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);
|
| +
|
| + request.withCredentials = withCredentials;
|
| +
|
| + request.onReadyStateChange.listen((e) {
|
| + if (request.readyState == HttpRequest.DONE) {
|
| + onComplete(request);
|
| + }
|
| + });
|
| +
|
| + request.send();
|
| +
|
| + return request;
|
| + }
|
| +}
|
| +// 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.
|
| +
|
| +
|
| // Conversions for Window. These check if the window is the local
|
| // window, and if it's not, wraps or unwraps it with a secure wrapper.
|
| // We need to test for EventTarget here as well as it's a base type.
|
|
|