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