From e6a9267870c2be3bc0436166cd47d54825cda573 Mon Sep 17 00:00:00 2001 From: Eric Bidelman Date: Fri, 13 Apr 2012 13:08:58 -0700 Subject: Adding gdbar component --- js/polyfills/components-polyfill.js | 226 ++++++++++++++++++++++++++++++++++++ x-gdbar.html | 91 +++++++++++++++ 2 files changed, 317 insertions(+) create mode 100644 js/polyfills/components-polyfill.js create mode 100644 x-gdbar.html diff --git a/js/polyfills/components-polyfill.js b/js/polyfills/components-polyfill.js new file mode 100644 index 0000000..31d0d8c --- /dev/null +++ b/js/polyfills/components-polyfill.js @@ -0,0 +1,226 @@ +(function(scope) { + +var scope = scope || {}; + +var SCRIPT_SHIM = ['(function(){\n', 1, '\n}).call(this.element);']; + +if (!window.WebKitShadowRoot) { + console.error('Shadow DOM support is required.'); + return; +} + + +scope.HTMLElementElement = function(name, tagName, declaration) { + this.name = name; + this.extends = tagName; + this.lifecycle = this.lifecycle.bind(declaration); +} + +scope.HTMLElementElement.prototype = { + __proto__: HTMLElement.prototype, + lifecycle: function(dict) { + this.created = dict.created || nil; + this.inserted = dict.inserted || nil; + this.attributeChanged = dict.attributeChanged || nil; + + // TODO: Implement remove lifecycle methods. + //this.removed = dict.removed || nil; + } +}; + + +scope.Declaration = function(name, tagName) { + this.elementPrototype = Object.create(this.prototypeFromTagName(tagName)); + this.element = new scope.HTMLElementElement(name, tagName, this); + this.element.generatedConstructor = this.generateConstructor(); + // Hard-bind the following methods to "this": + this.morph = this.morph.bind(this); +} + +scope.Declaration.prototype = { + + generateConstructor: function() { + var tagName = this.element.extends; + var created = this.created; + var extended = function() { + var element = document.createElement(tagName); + extended.prototype.__proto__ = element.__proto__; + element.__proto__ = extended.prototype; + created.call(element); + } + extended.prototype = this.elementPrototype; + return extended; + }, + + evalScript: function(script) { + //FIXME: Add support for external js loading. + SCRIPT_SHIM[1] = script.textContent; + eval(SCRIPT_SHIM.join('')); + }, + + addTemplate: function(template) { + this.template = template; + }, + + morph: function(element) { + // FIXME: We shouldn't be updating __proto__ like this on each morph. + this.element.generatedConstructor.prototype.__proto__ = document.createElement(this.element.extends); + element.__proto__ = this.element.generatedConstructor.prototype; + var shadowRoot = this.createShadowRoot(element); + + // Fire created event. + this.created && this.created.call(element, shadowRoot); + this.inserted && this.inserted.call(element, shadowRoot); + + // Setup mutation observer for attribute changes. + if (this.attributeChanged) { + var observer = new WebKitMutationObserver(function(mutations) { + mutations.forEach(function(m) { + this.attributeChanged(m.attributeName, m.oldValue, + m.target.getAttribute(m.attributeName)); + }.bind(this)); + }.bind(this)); + + // TOOD: spec isn't clear if it's changes to the custom attribute + // or any attribute in the subtree. + observer.observe(shadowRoot.host, { + attributes: true, + attributeOldValue: true + }); + } + }, + + createShadowRoot: function(element) { + if (!this.template) { + return; + } + + var shadowRoot = new WebKitShadowRoot(element); + [].forEach.call(this.template.childNodes, function(node) { + shadowRoot.appendChild(node.cloneNode(true)); + }); + + return shadowRoot; + }, + + prototypeFromTagName: function(tagName) { + return Object.getPrototypeOf(document.createElement(tagName)); + } +} + + +scope.DeclarationFactory = function() { + // Hard-bind the following methods to "this": + this.createDeclaration = this.createDeclaration.bind(this); +} + +scope.DeclarationFactory.prototype = { + // Called whenever each Declaration instance is created. + oncreate: null, + + createDeclaration: function(element) { + var name = element.getAttribute('name'); + if (!name) { + // FIXME: Make errors more friendly. + console.error('name attribute is required.') + return; + } + var tagName = element.getAttribute('extends'); + if (!tagName) { + // FIXME: Make it work with any element. + // FIXME: Make errors more friendly. + console.error('extends attribute is required.'); + return; + } + var constructorName = element.getAttribute('constructor'); + var declaration = new scope.Declaration(name, tagName, constructorName); + if (constructorName) { + window[constructorName] = declaration.element.generatedConstructor; + } + + [].forEach.call(element.querySelectorAll('script'), declaration.evalScript, + declaration); + var template = element.querySelector('template'); + template && declaration.addTemplate(template); + this.oncreate && this.oncreate(declaration); + } +} + + +scope.Parser = function() { + this.parse = this.parse.bind(this); +} + +scope.Parser.prototype = { + // Called for each element that's parsed. + onparse: null, + + parse: function(string) { + var doc = document.implementation.createHTMLDocument(); + doc.body.innerHTML = string; + [].forEach.call(doc.querySelectorAll('element'), function(element) { + this.onparse && this.onparse(element); + }, this); + } +} + + +scope.Loader = function() { + this.start = this.start.bind(this); +} + +scope.Loader.prototype = { + // Called for each loaded declaration. + onload: null, + onerror: null, + + start: function() { + [].forEach.call(document.querySelectorAll('link[rel=components]'), function(link) { + this.load(link.href); + }, this); + }, + + load: function(url) { + var request = new XMLHttpRequest(); + var loader = this; + + request.open('GET', url); + request.addEventListener('readystatechange', function(e) { + if (request.readyState === 4) { + if (request.status >= 200 && request.status < 300 || request.status === 304) { + loader.onload && loader.onload(request.response); + } else { + loader.onerror && loader.onerror(request.status, request); + } + } + }); + request.send(); + } +} + +scope.run = function() { + var loader = new scope.Loader(); + document.addEventListener('DOMContentLoaded', loader.start); + var parser = new scope.Parser(); + loader.onload = parser.parse; + loader.onerror = function(status, resp) { + console.error("Unable to load component: Status " + status + " - " + + resp.statusText); + }; + + var factory = new scope.DeclarationFactory(); + parser.onparse = factory.createDeclaration; + factory.oncreate = function(declaration) { + [].forEach.call(document.querySelectorAll( + declaration.element.extends + '[is=' + declaration.element.name + + ']'), declaration.morph); + } +} + +if (!scope.runManually) { + scope.run(); +} + +function nil() {} + +})(window.__exported_components_polyfill_scope__); diff --git a/x-gdbar.html b/x-gdbar.html new file mode 100644 index 0000000..81db557 --- /dev/null +++ b/x-gdbar.html @@ -0,0 +1,91 @@ + + + + -- cgit v1.2.3