blob: 0ae6641dbb983bbbff3fc7b0e761b3965c5eeec9 [file] [log] [blame]
// 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.
part of polymer;
/// *Warning* this class is experimental and subject to change.
///
/// The data associated with a polymer-element declaration, if it is backed
/// by a Dart class instead of a JavaScript prototype.
class PolymerDeclaration {
/// The one syntax to rule them all.
static final BindingDelegate _polymerSyntax = new PolymerExpressions();
/// The polymer-element for this declaration.
final HtmlElement element;
/// The Dart type corresponding to this custom element declaration.
final Type type;
/// If we extend another custom element, this points to the super declaration.
final PolymerDeclaration superDeclaration;
/// The name of the custom element.
final String name;
/// Map of publish properties. Can be a field or a property getter, but if
/// this map contains a getter, is because it also has a corresponding setter.
///
/// Note: technically these are always single properties, so we could use a
/// Symbol instead of a PropertyPath. However there are lookups between this
/// map and [_observe] so it is easier to just track paths.
Map<PropertyPath, smoke.Declaration> _publish;
/// The names of published properties for this polymer-element.
Iterable<String> get publishedProperties =>
_publish != null ? _publish.keys.map((p) => '$p') : const [];
/// Same as [_publish] but with lower case names.
Map<String, smoke.Declaration> _publishLC;
Map<PropertyPath, List<Symbol>> _observe;
Map<String, Object> _instanceAttributes;
/// A set of properties that should be automatically reflected to attributes.
/// Typically this is used for CSS styling. If none, this variable will be
/// left as null.
Set<String> _reflect;
List<Element> _sheets;
List<Element> get sheets => _sheets;
List<Element> _styles;
List<Element> get styles => _styles;
// The default syntax for polymer-elements.
PolymerExpressions syntax = _polymerSyntax;
DocumentFragment get templateContent {
final template = fetchTemplate();
return template != null ? templateBind(template).content : null;
}
/// Maps event names and their associated method in the element class.
final Map<String, String> _eventDelegates = {};
/// Expected events per element node.
// TODO(sigmund): investigate whether we need more than 1 set of local events
// per element (why does the js implementation stores 1 per template node?)
Expando<Set<String>> _templateDelegates;
String get extendee => superDeclaration != null ?
superDeclaration.name : null;
/// The root URI for assets.
Uri _rootUri;
// Dart note: since polymer-element is handled in JS now, we have a simplified
// flow for registering. We don't need to wait for the supertype or the code
// to be noticed.
PolymerDeclaration(this.element, this.name, this.type, this.superDeclaration);
void register() {
// more declarative features
desugar();
// register our custom element
registerType(name);
// NOTE: skip in Dart because we don't have mutable global scope.
// reference constructor in a global named by 'constructor' attribute
// publishConstructor();
}
/// Implement various declarative features.
// Dart note: this merges "buildPrototype" "desugarBeforeChaining" and
// "desugarAfterChaining", because we don't have prototypes.
void desugar() {
// back reference declaration element
_declarations[name] = this;
// transcribe `attributes` declarations onto own prototype's `publish`
publishAttributes(superDeclaration);
publishProperties();
inferObservers();
// desugar compound observer syntax, e.g. @ObserveProperty('a b c')
explodeObservers();
// install mdv delegate on template
installBindingDelegate(fetchTemplate());
// install external stylesheets as if they are inline
installSheets();
// adjust any paths in dom from imports
resolveElementPaths(element);
// compile list of attributes to copy to instances
accumulateInstanceAttributes();
// parse on-* delegates declared on `this` element
parseHostEvents();
// install a helper method this.resolvePath to aid in
// setting resource urls. e.g.
// this.$.image.src = this.resolvePath('images/foo.png')
initResolvePath();
// under ShadowDOMPolyfill, transforms to approximate missing CSS features
_shimShadowDomStyling(templateContent, name, extendee);
// TODO(jmesserly): this feels unnatrual in Dart. Since we have convenient
// lazy static initialization, can we get by without it?
if (smoke.hasStaticMethod(type, #registerCallback)) {
smoke.invoke(type, #registerCallback, [this]);
}
}
void registerType(String name) {
var baseTag;
var decl = this;
while (decl != null) {
baseTag = decl.element.attributes['extends'];
decl = decl.superDeclaration;
}
document.registerElement(name, type, extendsTag: baseTag);
}
// from declaration/mdv.js
Element fetchTemplate() => element.querySelector('template');
void installBindingDelegate(Element template) {
if (template != null) {
templateBind(template).bindingDelegate = this.syntax;
}
}
// from declaration/path.js
void resolveElementPaths(Node node) {
if (_Platform == null) return;
_Platform['urlResolver'].callMethod('resolveDom', [node]);
}
// Dart note: renamed from "addResolvePathApi".
void initResolvePath() {
// let assetpath attribute modify the resolve path
var assetPath = element.attributes['assetpath'];
if (assetPath == null) assetPath = '';
var base = Uri.parse(element.ownerDocument.baseUri);
_rootUri = base.resolve(assetPath);
}
String resolvePath(String urlPath, [baseUrlOrString]) {
Uri base;
if (baseUrlOrString == null) {
// Dart note: this enforces the same invariant as JS, where you need to
// call addResolvePathApi first.
if (_rootUri == null) {
throw new StateError('call initResolvePath before calling resolvePath');
}
base = _rootUri;
} else if (baseUrlOrString is Uri) {
base = baseUrlOrString;
} else {
base = Uri.parse(baseUrlOrString);
}
return base.resolve(urlPath).toString();
}
void publishAttributes(PolymerDeclaration superDecl) {
// get properties to publish
if (superDecl != null) {
// Dart note: even though we walk the type hierarchy in
// _getPublishedProperties, this will additionally include any names
// published via the `attributes` attribute.
if (superDecl._publish != null) {
_publish = new Map.from(superDecl._publish);
}
if (superDecl._reflect != null) {
_reflect = new Set.from(superDecl._reflect);
}
}
_getPublishedProperties(type);
// merge names from 'attributes' attribute
var attrs = element.attributes['attributes'];
if (attrs != null) {
// names='a b c' or names='a,b,c'
// record each name for publishing
for (var attr in attrs.split(_ATTRIBUTES_REGEX)) {
// remove excess ws
attr = attr.trim();
// do not override explicit entries
if (attr == '') continue;
var property = smoke.nameToSymbol(attr);
var path = new PropertyPath([property]);
if (_publish != null && _publish.containsKey(path)) {
continue;
}
var decl = smoke.getDeclaration(type, property);
if (decl == null || decl.isMethod || decl.isFinal) {
window.console.warn('property for attribute $attr of polymer-element '
'name=$name not found.');
continue;
}
if (_publish == null) _publish = {};
_publish[path] = decl;
}
}
// NOTE: the following is not possible in Dart; fields must be declared.
// install 'attributes' as properties on the prototype,
// but don't override
}
void _getPublishedProperties(Type type) {
var options = const smoke.QueryOptions(includeInherited: true,
includeUpTo: HtmlElement, withAnnotations: const [PublishedProperty]);
for (var decl in smoke.query(type, options)) {
if (decl.isFinal) continue;
if (_publish == null) _publish = {};
_publish[new PropertyPath([decl.name])] = decl;
// Should we reflect the property value to the attribute automatically?
if (decl.annotations
.where((a) => a is PublishedProperty)
.any((a) => a.reflect)) {
if (_reflect == null) _reflect = new Set();
_reflect.add(smoke.symbolToName(decl.name));
}
}
}
void accumulateInstanceAttributes() {
// inherit instance attributes
_instanceAttributes = new Map<String, Object>();
if (superDeclaration != null) {
_instanceAttributes.addAll(superDeclaration._instanceAttributes);
}
// merge attributes from element
element.attributes.forEach((name, value) {
if (isInstanceAttribute(name)) {
_instanceAttributes[name] = value;
}
});
}
static bool isInstanceAttribute(name) {
// do not clone these attributes onto instances
final blackList = const {
'name': 1,
'extends': 1,
'constructor': 1,
'noscript': 1,
'assetpath': 1,
'cache-csstext': 1,
// add ATTRIBUTES_ATTRIBUTE to the blacklist
'attributes': 1,
};
return !blackList.containsKey(name) && !name.startsWith('on-');
}
/// Extracts events from the element tag attributes.
void parseHostEvents() {
addAttributeDelegates(_eventDelegates);
}
void addAttributeDelegates(Map<String, String> delegates) {
element.attributes.forEach((name, value) {
if (_hasEventPrefix(name)) {
var start = value.indexOf('{{');
var end = value.lastIndexOf('}}');
if (start >= 0 && end >= 0) {
delegates[_removeEventPrefix(name)] =
value.substring(start + 2, end).trim();
}
}
});
}
String urlToPath(String url) {
if (url == null) return '';
return (url.split('/')..removeLast()..add('')).join('/');
}
// Dart note: loadStyles, convertSheetsToStyles, copySheetAttribute and
// findLoadableStyles are not ported because they're handled by Polymer JS
// before we get into [register].
/// Install external stylesheets loaded in <element> elements into the
/// element's template.
void installSheets() {
cacheSheets();
cacheStyles();
installLocalSheets();
installGlobalStyles();
}
void cacheSheets() {
_sheets = findNodes(_SHEET_SELECTOR);
for (var s in sheets) s.remove();
}
void cacheStyles() {
_styles = findNodes('$_STYLE_SELECTOR[$_SCOPE_ATTR]');
for (var s in styles) s.remove();
}
/// Takes external stylesheets loaded in an `<element>` element and moves
/// their content into a style element inside the `<element>`'s template.
/// The sheet is then removed from the `<element>`. This is done only so
/// that if the element is loaded in the main document, the sheet does
/// not become active.
/// Note, ignores sheets with the attribute 'polymer-scope'.
void installLocalSheets() {
var sheets = this.sheets.where(
(s) => !s.attributes.containsKey(_SCOPE_ATTR));
var content = templateContent;
if (content != null) {
var cssText = new StringBuffer();
for (var sheet in sheets) {
cssText..write(_cssTextFromSheet(sheet))..write('\n');
}
if (cssText.length > 0) {
var style = element.ownerDocument.createElement('style')
..text = '$cssText';
content.insertBefore(style, content.firstChild);
}
}
}
List<Element> findNodes(String selector, [bool matcher(Element e)]) {
var nodes = element.querySelectorAll(selector).toList();
var content = templateContent;
if (content != null) {
nodes = nodes..addAll(content.querySelectorAll(selector));
}
if (matcher != null) return nodes.where(matcher).toList();
return nodes;
}
/// Promotes external stylesheets and style elements with the attribute
/// polymer-scope='global' into global scope.
/// This is particularly useful for defining @keyframe rules which
/// currently do not function in scoped or shadow style elements.
/// (See wkb.ug/72462)
// TODO(sorvell): remove when wkb.ug/72462 is addressed.
void installGlobalStyles() {
var style = styleForScope(_STYLE_GLOBAL_SCOPE);
Polymer.applyStyleToScope(style, document.head);
}
String cssTextForScope(String scopeDescriptor) {
var cssText = new StringBuffer();
// handle stylesheets
var selector = '[$_SCOPE_ATTR=$scopeDescriptor]';
matcher(s) => s.matches(selector);
for (var sheet in sheets.where(matcher)) {
cssText..write(_cssTextFromSheet(sheet))..write('\n\n');
}
// handle cached style elements
for (var style in styles.where(matcher)) {
cssText..write(style.text)..write('\n\n');
}
return cssText.toString();
}
StyleElement styleForScope(String scopeDescriptor) {
var cssText = cssTextForScope(scopeDescriptor);
return cssTextToScopeStyle(cssText, scopeDescriptor);
}
StyleElement cssTextToScopeStyle(String cssText, String scopeDescriptor) {
if (cssText == '') return null;
return new StyleElement()
..text = cssText
..attributes[_STYLE_SCOPE_ATTRIBUTE] = '$name-$scopeDescriptor';
}
/// Fetch a list of all *Changed methods so we can observe the associated
/// properties.
void inferObservers() {
for (var decl in smoke.query(type, _changedMethodQueryOptions)) {
// TODO(jmesserly): now that we have a better system, should we
// deprecate *Changed methods?
if (_observe == null) _observe = new HashMap();
var name = smoke.symbolToName(decl.name);
name = name.substring(0, name.length - 7);
_observe[new PropertyPath(name)] = [decl.name];
}
}
/// Fetch a list of all methods annotated with [ObserveProperty] so we can
/// observe the associated properties.
void explodeObservers() {
var options = const smoke.QueryOptions(includeFields: false,
includeProperties: false, includeMethods: true, includeInherited: true,
includeUpTo: HtmlElement, withAnnotations: const [ObserveProperty]);
for (var decl in smoke.query(type, options)) {
for (var meta in decl.annotations) {
if (meta is! ObserveProperty) continue;
if (_observe == null) _observe = new HashMap();
for (String name in meta.names) {
_observe.putIfAbsent(new PropertyPath(name), () => []).add(decl.name);
}
}
}
}
void publishProperties() {
// Dart note: _publish was already populated by publishAttributes
if (_publish != null) _publishLC = _lowerCaseMap(_publish);
}
Map<String, dynamic> _lowerCaseMap(Map<PropertyPath, dynamic> properties) {
final map = new Map<String, dynamic>();
properties.forEach((PropertyPath path, value) {
map['$path'.toLowerCase()] = value;
});
return map;
}
}
/// maps tag names to prototypes
final Map _typesByName = new Map<String, Type>();
Type _getRegisteredType(String name) => _typesByName[name];
/// track document.register'ed tag names and their declarations
final Map _declarations = new Map<String, PolymerDeclaration>();
bool _isRegistered(String name) => _declarations.containsKey(name);
PolymerDeclaration _getDeclaration(String name) => _declarations[name];
/// Using Polymer's platform/src/ShadowCSS.js passing the style tag's content.
void _shimShadowDomStyling(DocumentFragment template, String name,
String extendee) {
if (template == null || _ShadowCss == null) return;
_ShadowCss.callMethod('shimStyling', [template, name, extendee]);
}
final bool _hasShadowDomPolyfill = js.context.hasProperty('ShadowDOMPolyfill');
final JsObject _ShadowCss = _Platform != null ? _Platform['ShadowCSS'] : null;
const _STYLE_SELECTOR = 'style';
const _SHEET_SELECTOR = 'link[rel=stylesheet]';
const _STYLE_GLOBAL_SCOPE = 'global';
const _SCOPE_ATTR = 'polymer-scope';
const _STYLE_SCOPE_ATTRIBUTE = 'element';
const _STYLE_CONTROLLER_SCOPE = 'controller';
String _cssTextFromSheet(LinkElement sheet) {
if (sheet == null) return '';
// In deploy mode we should never do a sync XHR; link rel=stylesheet will
// be inlined into a <style> tag by ImportInliner.
if (loader.deployMode) return '';
// TODO(jmesserly): sometimes the href property is wrong after deployment.
var href = sheet.href;
if (href == '') href = sheet.attributes["href"];
// TODO(jmesserly): it seems like polymer-js is always polyfilling
// HTMLImports, because their code depends on "__resource" to work, so I
// don't see how it can work with native HTML Imports. We use a sync-XHR
// under the assumption that the file is likely to have been already
// downloaded and cached by HTML Imports.
try {
return (new HttpRequest()
..open('GET', href, async: false)
..send())
.responseText;
} on DomException catch (e, t) {
_sheetLog.fine('failed to XHR stylesheet text href="$href" error: '
'$e, trace: $t');
return '';
}
}
final Logger _sheetLog = new Logger('polymer.stylesheet');
final smoke.QueryOptions _changedMethodQueryOptions = new smoke.QueryOptions(
includeFields: false, includeProperties: false, includeMethods: true,
includeInherited: true, includeUpTo: HtmlElement,
matches: _isObserverMethod);
bool _isObserverMethod(Symbol symbol) {
String name = smoke.symbolToName(symbol);
if (name == null) return false;
return name.endsWith('Changed') && name != 'attributeChanged';
}
final _ATTRIBUTES_REGEX = new RegExp(r'\s|,');
final JsObject _Platform = js.context['Platform'];