diff options
Diffstat (limited to 'js/polyfills')
-rw-r--r-- | js/polyfills/components-polyfill.js | 226 |
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 | |||
3 | var scope = scope || {}; | ||
4 | |||
5 | var SCRIPT_SHIM = ['(function(){\n', 1, '\n}).call(this.element);']; | ||
6 | |||
7 | if (!window.WebKitShadowRoot) { | ||
8 | console.error('Shadow DOM support is required.'); | ||
9 | return; | ||
10 | } | ||
11 | |||
12 | |||
13 | scope.HTMLElementElement = function(name, tagName, declaration) { | ||
14 | this.name = name; | ||
15 | this.extends = tagName; | ||
16 | this.lifecycle = this.lifecycle.bind(declaration); | ||
17 | } | ||
18 | |||
19 | scope.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 | |||
32 | scope.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 | |||
40 | scope.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 | |||
112 | scope.DeclarationFactory = function() { | ||
113 | // Hard-bind the following methods to "this": | ||
114 | this.createDeclaration = this.createDeclaration.bind(this); | ||
115 | } | ||
116 | |||
117 | scope.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 | |||
150 | scope.Parser = function() { | ||
151 | this.parse = this.parse.bind(this); | ||
152 | } | ||
153 | |||
154 | scope.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 | |||
168 | scope.Loader = function() { | ||
169 | this.start = this.start.bind(this); | ||
170 | } | ||
171 | |||
172 | scope.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 | |||
201 | scope.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 | |||
220 | if (!scope.runManually) { | ||
221 | scope.run(); | ||
222 | } | ||
223 | |||
224 | function nil() {} | ||
225 | |||
226 | })(window.__exported_components_polyfill_scope__); | ||