| <!-- |
| // Copyright 2015 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| --> |
| <script> |
| import "dart:mirrors"; |
| import "dart:sky"; |
| |
| typedef dynamic _Converter(String value); |
| |
| final Map<String, _Converter> _kAttributeConverters = { |
| 'boolean': (String value) { |
| return value == 'true'; |
| }, |
| 'number': (String value) { |
| return double.parse(value); |
| }, |
| 'string': (String value) { |
| return value == null ? '' : value; |
| }, |
| }; |
| |
| class _Registration { |
| final Element template; |
| final Map<String, _Converter> attributes = new Map(); |
| |
| _Registration(this.template); |
| |
| void parseAttributeSpec(definition) { |
| String spec = definition.getAttribute('attributes'); |
| if (spec == null) |
| return; |
| |
| for (String token in spec.split(',')) { |
| List<String> parts = token.split(':'); |
| |
| if (parts.length != 2) { |
| window.console.error( |
| 'Invalid attribute spec "${spec}", attributes must' |
| ' be {name}:{type}, where type is one of boolean, number or' |
| ' string.'); |
| continue; |
| } |
| |
| var name = parts[0].trim(); |
| var type = parts[1].trim(); |
| |
| defineAttribute(name, type); |
| } |
| } |
| |
| void defineAttribute(String name, String type) { |
| _Converter converter = _kAttributeConverters[type]; |
| |
| if (converter == null) { |
| window.console.error( |
| 'Invalid attribute type "${type}", type must be one of boolean,' |
| ' number or string.'); |
| return; |
| } |
| |
| attributes[name] = converter; |
| } |
| } |
| |
| final Map<String, _Registration> _registery = new Map<String, _Registration>(); |
| |
| class Tagname { |
| final String name; |
| const Tagname(this.name); |
| } |
| |
| String _getTagName(Type type) { |
| return reflectClass(type).metadata.firstWhere( |
| (i) => i.reflectee is Tagname).reflectee.name; |
| } |
| |
| abstract class SkyElement extends Element { |
| // Override these functions to receive lifecycle notifications. |
| void created() {} |
| void attached() {} |
| void detached() {} |
| void attributeChanged(String attrName, String oldValue, String newValue) {} |
| void shadowRootReady() {} |
| |
| String get tagName => _getTagName(runtimeType); |
| _Registration _registration; |
| |
| SkyElement() { |
| _registration = _registery[tagName]; |
| // Invoke attributeChanged callback when element is first created too. |
| // TODO(abarth): Is this necessary? We shouldn't have any attribute yet... |
| for (Attr attribute in getAttributes()) |
| attributeChangedCallback(attribute.name, null, attribute.value); |
| } |
| |
| attachedCallback() { |
| if (shadowRoot == null) { |
| if (_registration.template != null) { |
| ShadowRoot shadow = ensureShadowRoot(); |
| Node content = _registration.template.content; |
| shadow.appendChild(document.importNode(content, deep: true)); |
| shadowRootReady(); |
| } |
| } |
| attached(); |
| } |
| |
| detachedCallback() { |
| detached(); |
| } |
| |
| attributeChangedCallback(name, oldValue, newValue) { |
| attributeChanged(name, oldValue, newValue); |
| |
| _Converter converter = _registration.attributes[name]; |
| if (converter == null) |
| return; |
| Symbol callback = new Symbol('${name}Changed'); |
| InstanceMirror mirror = reflect(this); |
| if (mirror.type.instanceMembers.containsKey(callback)) |
| mirror.invoke(callback, [converter(oldValue), converter(newValue)]); |
| } |
| |
| noSuchMethod(Invocation invocation) { |
| String name = MirrorSystem.getName(invocation.memberName); |
| if (name.endsWith('=')) |
| name = name.substring(0, name.length - 1); |
| _Converter converter = _registration.attributes[name]; |
| if (converter != null) { |
| if (invocation.isGetter) { |
| return converter(getAttribute(name)); |
| } else if (invocation.isSetter) { |
| setAttribute(name, invocation.positionalArguments[0].toString()); |
| return; |
| } |
| } |
| return super.noSuchMethod(invocation); |
| } |
| } |
| |
| void register(Element script, Type type) { |
| Element definition = script.parentNode; |
| |
| if (definition.tagName != 'sky-element') |
| throw new UnsupportedError('register() calls must be inside a <sky-element>.'); |
| |
| ClassMirror mirror = reflectClass(type); |
| if (!mirror.isSubclassOf(reflectClass(SkyElement))) |
| throw new UnsupportedError('@Tagname can only be used on descendants of SkyElement'); |
| |
| String tagName = _getTagName(type); |
| Element template = definition.querySelector('template'); |
| |
| document.registerElement(tagName, type); |
| _registery[tagName] = new _Registration(template) |
| ..parseAttributeSpec(definition); |
| } |
| </script> |