| <!-- |
| // 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> |