42ed8fe4c266fcb4ecec21ae0d9acb0411e562ec
[WebKit.git] / Websites / perf.webkit.org / public / v3 / components / base.js
1
2 class ComponentBase extends CommonComponentBase {
3     constructor(name)
4     {
5         super();
6         this._componentName = name || ComponentBase._componentByClass.get(new.target);
7
8         const currentlyConstructed = ComponentBase._currentlyConstructedByInterface;
9         let element = currentlyConstructed.get(new.target);
10         if (!element) {
11             currentlyConstructed.set(new.target, this);
12             element = document.createElement(this._componentName);
13             currentlyConstructed.delete(new.target);
14         }
15         element.component = () => { return this };
16
17         this._element = element;
18         this._shadow = null;
19         this._actionCallbacks = new Map;
20         this._oldSizeToCheckForResize = null;
21
22         if (!ComponentBase.useNativeCustomElements)
23             element.addEventListener('DOMNodeInsertedIntoDocument', () => this.enqueueToRender());
24         if (!ComponentBase.useNativeCustomElements && new.target.enqueueToRenderOnResize)
25             ComponentBase._connectedComponentToRenderOnResize(this);
26     }
27
28     element() { return this._element; }
29     content(id = null)
30     {
31         this._ensureShadowTree();
32         if (this._shadow && id != null)
33             return this._shadow.getElementById(id);
34         return this._shadow;
35     }
36
37     part(id)
38     {
39         this._ensureShadowTree();
40         if (!this._shadow)
41             return null;
42         const part = this._shadow.getElementById(id);
43         if (!part)
44             return null;
45         return part.component();
46     }
47
48     dispatchAction(actionName, ...args)
49     {
50         const callback = this._actionCallbacks.get(actionName);
51         if (callback)
52             return callback.apply(this, args);
53     }
54
55     listenToAction(actionName, callback)
56     {
57         this._actionCallbacks.set(actionName, callback);
58     }
59
60     render() { this._ensureShadowTree(); }
61
62     enqueueToRender()
63     {
64         Instrumentation.startMeasuringTime('ComponentBase', 'updateRendering');
65
66         if (!ComponentBase._componentsToRender) {
67             ComponentBase._componentsToRender = new Set;
68             requestAnimationFrame(() => ComponentBase.renderingTimerDidFire());
69         }
70         ComponentBase._componentsToRender.add(this);
71
72         Instrumentation.endMeasuringTime('ComponentBase', 'updateRendering');
73     }
74
75     static renderingTimerDidFire()
76     {
77         Instrumentation.startMeasuringTime('ComponentBase', 'renderingTimerDidFire');
78
79         const componentsToRender = ComponentBase._componentsToRender;
80         this._renderLoop();
81         if (ComponentBase._componentsToRenderOnResize) {
82             const resizedComponents = this._resizedComponents(ComponentBase._componentsToRenderOnResize);
83             if (resizedComponents.length) {
84                 ComponentBase._componentsToRender = new Set(resizedComponents);
85                 this._renderLoop();
86             }
87         }
88
89         Instrumentation.endMeasuringTime('ComponentBase', 'renderingTimerDidFire');
90     }
91
92     static _renderLoop()
93     {
94         const componentsToRender = ComponentBase._componentsToRender;
95         do {
96             const currentSet = [...componentsToRender];
97             componentsToRender.clear();
98             const resizeSet = ComponentBase._componentsToRenderOnResize;
99             for (let component of currentSet) {
100                 Instrumentation.startMeasuringTime('ComponentBase', 'renderingTimerDidFire.render');
101                 component.render();
102                 if (resizeSet && resizeSet.has(component)) {
103                     const element = component.element();
104                     component._oldSizeToCheckForResize = {width: element.offsetWidth, height: element.offsetHeight};
105                 }
106                 Instrumentation.endMeasuringTime('ComponentBase', 'renderingTimerDidFire.render');
107             }
108         } while (componentsToRender.size);
109         ComponentBase._componentsToRender = null;
110     }
111
112     static _resizedComponents(componentSet)
113     {
114         if (!componentSet)
115             return [];
116
117         const resizedList = [];
118         for (let component of componentSet) {
119             const element = component.element();
120             const width = element.offsetWidth;
121             const height = element.offsetHeight;
122             const oldSize = component._oldSizeToCheckForResize;
123             if (oldSize && oldSize.width == width && oldSize.height == height)
124                 continue;
125             resizedList.push(component);
126         }
127         return resizedList;
128     }
129
130     static _connectedComponentToRenderOnResize(component)
131     {
132         if (!ComponentBase._componentsToRenderOnResize) {
133             ComponentBase._componentsToRenderOnResize = new Set;
134             window.addEventListener('resize', () => {
135                 const resized = this._resizedComponents(ComponentBase._componentsToRenderOnResize);
136                 for (const component of resized)
137                     component.enqueueToRender();
138             });
139         }
140         ComponentBase._componentsToRenderOnResize.add(component);
141     }
142
143     static _disconnectedComponentToRenderOnResize(component)
144     {
145         ComponentBase._componentsToRenderOnResize.delete(component);
146     }
147
148     _ensureShadowTree()
149     {
150         if (this._shadow)
151             return;
152
153         const thisClass = this.__proto__.constructor;
154
155         let content;
156         let stylesheet;
157         if (!thisClass._parsed) {
158             thisClass._parsed = true;
159
160             const contentTemplate = thisClass['contentTemplate'];
161             if (contentTemplate)
162                 content = ComponentBase._constructNodeTreeFromTemplate(contentTemplate);
163             else if (thisClass.htmlTemplate) {
164                 const templateElement = document.createElement('template');
165                 templateElement.innerHTML = thisClass.htmlTemplate();
166                 content = [templateElement.content];
167             }
168
169             const styleTemplate = thisClass['styleTemplate'];
170             if (styleTemplate)
171                 stylesheet = ComponentBase._constructStylesheetFromTemplate(styleTemplate);
172             else if (thisClass.cssTemplate)
173                 stylesheet = thisClass.cssTemplate();
174
175             thisClass._parsedContent = content;
176             thisClass._parsedStylesheet = stylesheet;
177         } else {
178             content = thisClass._parsedContent;
179             stylesheet = thisClass._parsedStylesheet;
180         }
181
182         if (!content && !stylesheet)
183             return;
184
185         const shadow = this._element.attachShadow({mode: 'closed'});
186
187         if (content) {
188             for (const node of content)
189                 shadow.appendChild(document.importNode(node, true));
190             this._recursivelyUpgradeUnknownElements(shadow, (node) => {
191                 return node instanceof Element ? ComponentBase._componentByName.get(node.localName) : null;
192             });
193         }
194
195         if (stylesheet) {
196             const style = document.createElement('style');
197             style.textContent = stylesheet;
198             shadow.appendChild(style);
199         }
200         this._shadow = shadow;
201         this.didConstructShadowTree();
202     }
203
204     didConstructShadowTree() { }
205
206     static defineElement(name, elementInterface)
207     {
208         ComponentBase._componentByName.set(name, elementInterface);
209         ComponentBase._componentByClass.set(elementInterface, name);
210
211         const enqueueToRenderOnResize = elementInterface.enqueueToRenderOnResize;
212
213         if (!ComponentBase.useNativeCustomElements)
214             return;
215
216         class elementClass extends HTMLElement {
217             constructor()
218             {
219                 super();
220
221                 const currentlyConstructed = ComponentBase._currentlyConstructedByInterface;
222                 const component = currentlyConstructed.get(elementInterface);
223                 if (component)
224                     return; // ComponentBase's constructor will take care of the rest.
225
226                 currentlyConstructed.set(elementInterface, this);
227                 new elementInterface();
228                 currentlyConstructed.delete(elementInterface);
229             }
230
231             connectedCallback()
232             {
233                 this.component().enqueueToRender();
234                 if (enqueueToRenderOnResize)
235                     ComponentBase._connectedComponentToRenderOnResize(this.component());
236             }
237
238             disconnectedCallback()
239             {
240                 if (enqueueToRenderOnResize)
241                     ComponentBase._disconnectedComponentToRenderOnResize(this.component());
242             }
243         }
244
245         const nameDescriptor = Object.getOwnPropertyDescriptor(elementClass, 'name');
246         nameDescriptor.value = `${elementInterface.name}Element`;
247         Object.defineProperty(elementClass, 'name', nameDescriptor);
248
249         customElements.define(name, elementClass);
250     }
251
252     createEventHandler(callback) { return ComponentBase.createEventHandler(callback); }
253     static createEventHandler(callback)
254     {
255         return function (event) {
256             event.preventDefault();
257             event.stopPropagation();
258             callback.call(this, event);
259         };
260     }
261 }
262
263 CommonComponentBase._context = document;
264 CommonComponentBase._isNode = (node) => node instanceof Node;
265 CommonComponentBase._baseClass = ComponentBase;
266
267 ComponentBase.useNativeCustomElements = !!window.customElements;
268 ComponentBase._componentByName = new Map;
269 ComponentBase._componentByClass = new Map;
270 ComponentBase._currentlyConstructedByInterface = new Map;
271 ComponentBase._componentsToRender = null;
272 ComponentBase._componentsToRenderOnResize = null;