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