blob: a76874a5f1215af1bbefca5594b9edc2b2c1c2e8 [file] [log] [blame]
<!--
// Copyright 2014 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.
-->
<import src="sky-binder.sky" as="binder" />
<script>
var attributeConverters = {
boolean: function(value) {
if (typeof value == 'string')
return value == 'true';
return !!value;
},
number: function(value) {
return Number(value);
},
string: function(value) {
if (value === null)
return '';
return String(value);
},
};
function parseAttributeSpec(spec) {
var attributes = new Map();
var attributeTokens = (spec || '').split(',');
for (var i = 0; i < attributeTokens.length; ++i) {
var parts = attributeTokens[i].split(':');
var name = parts[0].trim();
var type = (parts[1] || '').trim();
var converter = attributeConverters[type] || attributeConverters.string;
attributes.set(name, converter);
}
return attributes;
}
function collectEventHandlers(definition) {
var eventHandlers = [];
var attributes = definition.getAttributes();
for (var i = 0; i < attributes.length; i++) {
var attr = attributes[i];
var name = attr.name;
var value = attr.value;
if (name.startsWith('on-')) {
eventHandlers.push(name.substring(3));
}
}
return eventHandlers;
}
function eventHandlerCallback(event) {
var element = event.currentTarget;
var registration = registrations.get(element.localName);
var method = registration.getEventHandler(event.type);
var handler = element[method];
if (handler instanceof Function)
return handler.call(element, event);
}
class ElementRegistration {
constructor(definition) {
this.definition = definition;
this.tagName = definition.getAttribute('name');
this.attributes = parseAttributeSpec(definition.getAttribute('attributes'));
this.eventHandlers = collectEventHandlers(definition);
this.template = definition.querySelector('template');
Object.preventExtensions(this);
}
getEventHandler(eventName) {
return this.definition.getAttribute('on-' + eventName);
}
getAttributeNames() {
// TODO(esprehn): We can replace this method with
// Array.from(registration.attributes) once we turn that on.
var names = []
this.attributes.forEach(function(converter, name) {
names.push(name);
});
return names;
}
synthesizeAttributes(prototype) {
this.attributes.forEach(function(converter, name) {
Object.defineProperty(prototype, name, {
get: function() {
return converter(this.getAttribute(name));
},
set: function(newValue) {
this.setAttribute(name, converter(newValue));
},
enumerable: true,
configurable: true,
});
});
}
}
var registrations = new Map();
class SkyElement extends HTMLElement {
static register() {
var definition = document.currentScript.parentNode;
if (definition.localName !== 'sky-element') {
throw new Error('register() calls must be inside a <sky-element>.');
}
var registration = new ElementRegistration(definition);
if (!registration.tagName) {
throw new Error('<sky-element> must have a name.');
}
if (registrations.has(registration.tagName)) {
throw new Error('Duplicate registration for tag name: ' +
registration.tagName);
}
registration.synthesizeAttributes(this.prototype);
// TODO(esprehn): Combine the two element registries here and in sky binder.
binder.registerElement(registration.tagName, {
attributeNames: registration.getAttributeNames(),
});
registrations.set(registration.tagName, registration);
return document.registerElement(registration.tagName, {
prototype: this.prototype,
});
}
created() {
// override
}
attached() {
// override
}
detached() {
// override
}
attributeChanged(attrName, oldValue, newValue) {
// override
}
shadowRootReady() {
// override
}
createdCallback() {
this.isAttached = false;
this.created();
Object.preventExtensions(this);
// Invoke attributeChanged callback when element is first created too.
var attributes = this.getAttributes();
for (var i = 0; i < attributes.length; ++i) {
var attribute = attributes[i];
this.attributeChangedCallback(attribute.name, null, attribute.value);
}
var registration = registrations.get(this.localName);
for (var i = 0; i < registration.eventHandlers.length; ++i) {
var eventName = registration.eventHandlers[i];
this.addEventListener(eventName, eventHandlerCallback);
}
}
attachedCallback() {
if (!this.shadowRoot) {
var registration = registrations.get(this.localName);
if (registration.template) {
var shadow = this.ensureShadowRoot();
var instance = binder.createInstance(registration.template, this);
shadow.appendChild(instance.fragment);
this.shadowRootReady();
}
}
this.attached();
this.isAttached = true;
}
detachedCallback() {
this.detached();
this.isAttached = false;
}
attributeChangedCallback(name, oldValue, newValue) {
this.attributeChanged(name, oldValue, newValue);
var registration = registrations.get(this.localName);
var converter = registration.attributes.get(name);
if (converter) {
this.notifyPropertyChanged(name, converter(oldValue),
converter(newValue));
}
}
notifyPropertyChanged(name, oldValue, newValue) {
var notifier = Object.getNotifier(this);
notifier.notify({
type: 'update',
name: name,
oldValue: oldValue,
});
var handler = this[name + 'Changed'];
if (typeof handler == 'function')
handler.call(this, oldValue, newValue);
}
};
module.exports = SkyElement;
</script>