Revert an erronously change in the previous commit.
[WebKit.git] / Websites / perf.webkit.org / public / v3 / components / base.js
1
2 class ComponentBase {
3     constructor(name)
4     {
5         this._componentName = name || ComponentBase._componentByClass.get(new.target);
6
7         const currentlyConstructed = ComponentBase._currentlyConstructedByInterface;
8         let element = currentlyConstructed.get(new.target);
9         if (!element) {
10             currentlyConstructed.set(new.target, this);
11             element = document.createElement(this._componentName);
12             currentlyConstructed.delete(new.target);
13         }
14         element.component = () => { return this };
15
16         this._element = element;
17         this._shadow = null;
18         this._actionCallbacks = new Map;
19
20         if (!ComponentBase.useNativeCustomElements)
21             element.addEventListener('DOMNodeInsertedIntoDocument', () => this.enqueueToRender());
22         if (!ComponentBase.useNativeCustomElements && new.target.enqueueToRenderOnResize)
23             ComponentBase._connectedComponentToRenderOnResize(this);
24     }
25
26     element() { return this._element; }
27     content(id = null)
28     {
29         this._ensureShadowTree();
30         if (this._shadow && id != null)
31             return this._shadow.getElementById(id);
32         return this._shadow;
33     }
34
35     part(id)
36     {
37         this._ensureShadowTree();
38         if (!this._shadow)
39             return null;
40         const part = this._shadow.getElementById(id);
41         if (!part)
42             return null;
43         return part.component();
44     }
45
46     dispatchAction(actionName, ...args)
47     {
48         const callback = this._actionCallbacks.get(actionName);
49         if (callback)
50             return callback.apply(this, args);
51     }
52
53     listenToAction(actionName, callback)
54     {
55         this._actionCallbacks.set(actionName, callback);
56     }
57
58     render() { this._ensureShadowTree(); }
59
60     enqueueToRender()
61     {
62         Instrumentation.startMeasuringTime('ComponentBase', 'updateRendering');
63
64         if (!ComponentBase._componentsToRender) {
65             ComponentBase._componentsToRender = new Set;
66             requestAnimationFrame(() => ComponentBase.renderingTimerDidFire());
67         }
68         ComponentBase._componentsToRender.add(this);
69
70         Instrumentation.endMeasuringTime('ComponentBase', 'updateRendering');
71     }
72
73     static renderingTimerDidFire()
74     {
75         Instrumentation.startMeasuringTime('ComponentBase', 'renderingTimerDidFire');
76
77         do {
78             const currentSet = [...ComponentBase._componentsToRender];
79             ComponentBase._componentsToRender.clear();
80             for (let component of currentSet) {
81                 Instrumentation.startMeasuringTime('ComponentBase', 'renderingTimerDidFire.render');
82                 component.render();
83                 Instrumentation.endMeasuringTime('ComponentBase', 'renderingTimerDidFire.render');
84             }
85         } while (ComponentBase._componentsToRender.size);
86         ComponentBase._componentsToRender = null;
87
88         Instrumentation.endMeasuringTime('ComponentBase', 'renderingTimerDidFire');
89     }
90
91     static _connectedComponentToRenderOnResize(component)
92     {
93         if (!ComponentBase._componentsToRenderOnResize) {
94             ComponentBase._componentsToRenderOnResize = new Set;
95             window.addEventListener('resize', () => {
96                 for (let component of ComponentBase._componentsToRenderOnResize)
97                     component.enqueueToRender();
98             });
99         }
100         ComponentBase._componentsToRenderOnResize.add(component);
101     }
102
103     static _disconnectedComponentToRenderOnResize(component)
104     {
105         ComponentBase._componentsToRenderOnResize.delete(component);
106     }
107
108     renderReplace(element, content) { ComponentBase.renderReplace(element, content); }
109
110     static renderReplace(element, content)
111     {
112         element.innerHTML = '';
113         if (content)
114             ComponentBase._addContentToElement(element, content);
115     }
116
117     _ensureShadowTree()
118     {
119         if (this._shadow)
120             return;
121
122         const newTarget = this.__proto__.constructor;
123         const htmlTemplate = newTarget['htmlTemplate'];
124         const cssTemplate = newTarget['cssTemplate'];
125
126         if (!htmlTemplate && !cssTemplate)
127             return;
128
129         const shadow = this._element.attachShadow({mode: 'closed'});
130
131         if (htmlTemplate) {
132             const template = document.createElement('template');
133             template.innerHTML = newTarget.htmlTemplate();
134             shadow.appendChild(document.importNode(template.content, true));
135             this._recursivelyReplaceUnknownElementsByComponents(shadow);
136         }
137
138         if (cssTemplate) {
139             const style = document.createElement('style');
140             style.textContent = newTarget.cssTemplate();
141             shadow.appendChild(style);
142         }
143         this._shadow = shadow;
144         this.didConstructShadowTree();
145     }
146
147     didConstructShadowTree() { }
148
149     _recursivelyReplaceUnknownElementsByComponents(parent)
150     {
151         let nextSibling;
152         for (let child = parent.firstChild; child; child = child.nextSibling) {
153             if (child instanceof HTMLElement && !child.component) {
154                 const elementInterface = ComponentBase._componentByName.get(child.localName);
155                 if (elementInterface) {
156                     const component = new elementInterface();
157                     const newChild = component.element();
158
159                     for (let i = 0; i < child.attributes.length; i++) {
160                         const attr = child.attributes[i];
161                         newChild.setAttribute(attr.name, attr.value);
162                     }
163
164                     parent.replaceChild(newChild, child);
165                     child = newChild;
166                 }
167             }
168             this._recursivelyReplaceUnknownElementsByComponents(child);
169         }
170     }
171
172     static defineElement(name, elementInterface)
173     {
174         ComponentBase._componentByName.set(name, elementInterface);
175         ComponentBase._componentByClass.set(elementInterface, name);
176
177         const enqueueToRenderOnResize = elementInterface.enqueueToRenderOnResize;
178
179         if (!ComponentBase.useNativeCustomElements)
180             return;
181
182         class elementClass extends HTMLElement {
183             constructor()
184             {
185                 super();
186
187                 const currentlyConstructed = ComponentBase._currentlyConstructedByInterface;
188                 const component = currentlyConstructed.get(elementInterface);
189                 if (component)
190                     return; // ComponentBase's constructor will take care of the rest.
191
192                 currentlyConstructed.set(elementInterface, this);
193                 new elementInterface();
194                 currentlyConstructed.delete(elementInterface);
195             }
196
197             connectedCallback()
198             {
199                 this.component().enqueueToRender();
200                 if (enqueueToRenderOnResize)
201                     ComponentBase._connectedComponentToRenderOnResize(this.component());
202             }
203
204             disconnectedCallback()
205             {
206                 if (enqueueToRenderOnResize)
207                     ComponentBase._disconnectedComponentToRenderOnResize(this.component());
208             }
209         }
210
211         const nameDescriptor = Object.getOwnPropertyDescriptor(elementClass, 'name');
212         nameDescriptor.value = `${elementInterface.name}Element`;
213         Object.defineProperty(elementClass, 'name', nameDescriptor);
214
215         customElements.define(name, elementClass);
216     }
217
218     static createElement(name, attributes, content)
219     {
220         var element = document.createElement(name);
221         if (!content && (Array.isArray(attributes) || attributes instanceof Node
222             || attributes instanceof ComponentBase || typeof(attributes) != 'object')) {
223             content = attributes;
224             attributes = {};
225         }
226
227         if (attributes) {
228             for (let name in attributes) {
229                 if (name.startsWith('on'))
230                     element.addEventListener(name.substring(2), attributes[name]);
231                 else if (attributes[name] === true)
232                     element.setAttribute(name, name);
233                 else if (attributes[name] !== false)
234                     element.setAttribute(name, attributes[name]);
235             }
236         }
237
238         if (content)
239             ComponentBase._addContentToElement(element, content);
240
241         return element;
242     }
243
244     static _addContentToElement(element, content)
245     {
246         if (Array.isArray(content)) {
247             for (var nestedChild of content)
248                 this._addContentToElement(element, nestedChild);
249         } else if (content instanceof Node)
250             element.appendChild(content);
251          else if (content instanceof ComponentBase)
252             element.appendChild(content.element());
253         else
254             element.appendChild(document.createTextNode(content));
255     }
256
257     static createLink(content, titleOrCallback, callback, isExternal)
258     {
259         var title = titleOrCallback;
260         if (callback === undefined) {
261             title = content;
262             callback = titleOrCallback;
263         }
264
265         var attributes = {
266             href: '#',
267             title: title,
268         };
269
270         if (typeof(callback) === 'string')
271             attributes['href'] = callback;
272         else
273             attributes['onclick'] = ComponentBase.createEventHandler(callback);
274
275         if (isExternal)
276             attributes['target'] = '_blank';
277         return ComponentBase.createElement('a', attributes, content);
278     }
279
280     createEventHandler(callback) { return ComponentBase.createEventHandler(callback); }
281     static createEventHandler(callback)
282     {
283         return function (event) {
284             event.preventDefault();
285             event.stopPropagation();
286             callback.call(this, event);
287         };
288     }
289 }
290
291 ComponentBase.useNativeCustomElements = !!window.customElements;
292 ComponentBase._componentByName = new Map;
293 ComponentBase._componentByClass = new Map;
294 ComponentBase._currentlyConstructedByInterface = new Map;
295 ComponentBase._componentsToRender = null;
296 ComponentBase._componentsToRenderOnResize = null;