aboutsummaryrefslogtreecommitdiff
path: root/js/polyfills
diff options
context:
space:
mode:
Diffstat (limited to 'js/polyfills')
-rw-r--r--js/polyfills/components-polyfill.js226
1 files changed, 226 insertions, 0 deletions
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 @@
1(function(scope) {
2
3var scope = scope || {};
4
5var SCRIPT_SHIM = ['(function(){\n', 1, '\n}).call(this.element);'];
6
7if (!window.WebKitShadowRoot) {
8 console.error('Shadow DOM support is required.');
9 return;
10}
11
12
13scope.HTMLElementElement = function(name, tagName, declaration) {
14 this.name = name;
15 this.extends = tagName;
16 this.lifecycle = this.lifecycle.bind(declaration);
17}
18
19scope.HTMLElementElement.prototype = {
20 __proto__: HTMLElement.prototype,
21 lifecycle: function(dict) {
22 this.created = dict.created || nil;
23 this.inserted = dict.inserted || nil;
24 this.attributeChanged = dict.attributeChanged || nil;
25
26 // TODO: Implement remove lifecycle methods.
27 //this.removed = dict.removed || nil;
28 }
29};
30
31
32scope.Declaration = function(name, tagName) {
33 this.elementPrototype = Object.create(this.prototypeFromTagName(tagName));
34 this.element = new scope.HTMLElementElement(name, tagName, this);
35 this.element.generatedConstructor = this.generateConstructor();
36 // Hard-bind the following methods to "this":
37 this.morph = this.morph.bind(this);
38}
39
40scope.Declaration.prototype = {
41
42 generateConstructor: function() {
43 var tagName = this.element.extends;
44 var created = this.created;
45 var extended = function() {
46 var element = document.createElement(tagName);
47 extended.prototype.__proto__ = element.__proto__;
48 element.__proto__ = extended.prototype;
49 created.call(element);
50 }
51 extended.prototype = this.elementPrototype;
52 return extended;
53 },
54
55 evalScript: function(script) {
56 //FIXME: Add support for external js loading.
57 SCRIPT_SHIM[1] = script.textContent;
58 eval(SCRIPT_SHIM.join(''));
59 },
60
61 addTemplate: function(template) {
62 this.template = template;
63 },
64
65 morph: function(element) {
66 // FIXME: We shouldn't be updating __proto__ like this on each morph.
67 this.element.generatedConstructor.prototype.__proto__ = document.createElement(this.element.extends);
68 element.__proto__ = this.element.generatedConstructor.prototype;
69 var shadowRoot = this.createShadowRoot(element);
70
71 // Fire created event.
72 this.created && this.created.call(element, shadowRoot);
73 this.inserted && this.inserted.call(element, shadowRoot);
74
75 // Setup mutation observer for attribute changes.
76 if (this.attributeChanged) {
77 var observer = new WebKitMutationObserver(function(mutations) {
78 mutations.forEach(function(m) {
79 this.attributeChanged(m.attributeName, m.oldValue,
80 m.target.getAttribute(m.attributeName));
81 }.bind(this));
82 }.bind(this));
83
84 // TOOD: spec isn't clear if it's changes to the custom attribute
85 // or any attribute in the subtree.
86 observer.observe(shadowRoot.host, {
87 attributes: true,
88 attributeOldValue: true
89 });
90 }
91 },
92
93 createShadowRoot: function(element) {
94 if (!this.template) {
95 return;
96 }
97
98 var shadowRoot = new WebKitShadowRoot(element);
99 [].forEach.call(this.template.childNodes, function(node) {
100 shadowRoot.appendChild(node.cloneNode(true));
101 });
102
103 return shadowRoot;
104 },
105
106 prototypeFromTagName: function(tagName) {
107 return Object.getPrototypeOf(document.createElement(tagName));
108 }
109}
110
111
112scope.DeclarationFactory = function() {
113 // Hard-bind the following methods to "this":
114 this.createDeclaration = this.createDeclaration.bind(this);
115}
116
117scope.DeclarationFactory.prototype = {
118 // Called whenever each Declaration instance is created.
119 oncreate: null,
120
121 createDeclaration: function(element) {
122 var name = element.getAttribute('name');
123 if (!name) {
124 // FIXME: Make errors more friendly.
125 console.error('name attribute is required.')
126 return;
127 }
128 var tagName = element.getAttribute('extends');
129 if (!tagName) {
130 // FIXME: Make it work with any element.
131 // FIXME: Make errors more friendly.
132 console.error('extends attribute is required.');
133 return;
134 }
135 var constructorName = element.getAttribute('constructor');
136 var declaration = new scope.Declaration(name, tagName, constructorName);
137 if (constructorName) {
138 window[constructorName] = declaration.element.generatedConstructor;
139 }
140
141 [].forEach.call(element.querySelectorAll('script'), declaration.evalScript,
142 declaration);
143 var template = element.querySelector('template');
144 template && declaration.addTemplate(template);
145 this.oncreate && this.oncreate(declaration);
146 }
147}
148
149
150scope.Parser = function() {
151 this.parse = this.parse.bind(this);
152}
153
154scope.Parser.prototype = {
155 // Called for each element that's parsed.
156 onparse: null,
157
158 parse: function(string) {
159 var doc = document.implementation.createHTMLDocument();
160 doc.body.innerHTML = string;
161 [].forEach.call(doc.querySelectorAll('element'), function(element) {
162 this.onparse && this.onparse(element);
163 }, this);
164 }
165}
166
167
168scope.Loader = function() {
169 this.start = this.start.bind(this);
170}
171
172scope.Loader.prototype = {
173 // Called for each loaded declaration.
174 onload: null,
175 onerror: null,
176
177 start: function() {
178 [].forEach.call(document.querySelectorAll('link[rel=components]'), function(link) {
179 this.load(link.href);
180 }, this);
181 },
182
183 load: function(url) {
184 var request = new XMLHttpRequest();
185 var loader = this;
186
187 request.open('GET', url);
188 request.addEventListener('readystatechange', function(e) {
189 if (request.readyState === 4) {
190 if (request.status >= 200 && request.status < 300 || request.status === 304) {
191 loader.onload && loader.onload(request.response);
192 } else {
193 loader.onerror && loader.onerror(request.status, request);
194 }
195 }
196 });
197 request.send();
198 }
199}
200
201scope.run = function() {
202 var loader = new scope.Loader();
203 document.addEventListener('DOMContentLoaded', loader.start);
204 var parser = new scope.Parser();
205 loader.onload = parser.parse;
206 loader.onerror = function(status, resp) {
207 console.error("Unable to load component: Status " + status + " - " +
208 resp.statusText);
209 };
210
211 var factory = new scope.DeclarationFactory();
212 parser.onparse = factory.createDeclaration;
213 factory.oncreate = function(declaration) {
214 [].forEach.call(document.querySelectorAll(
215 declaration.element.extends + '[is=' + declaration.element.name +
216 ']'), declaration.morph);
217 }
218}
219
220if (!scope.runManually) {
221 scope.run();
222}
223
224function nil() {}
225
226})(window.__exported_components_polyfill_scope__);