Improve test freshness page interaction experience.
[WebKit.git] / Websites / perf.webkit.org / public / v3 / components / base.js
index 6ddedf8..1fd79e0 100644 (file)
@@ -1,7 +1,8 @@
 
-class ComponentBase {
+class ComponentBase extends CommonComponentBase {
     constructor(name)
     {
+        super();
         this._componentName = name || ComponentBase._componentByClass.get(new.target);
 
         const currentlyConstructed = ComponentBase._currentlyConstructedByInterface;
@@ -15,18 +16,47 @@ class ComponentBase {
 
         this._element = element;
         this._shadow = null;
+        this._actionCallbacks = new Map;
+        this._oldSizeToCheckForResize = null;
 
-        if (!window.customElements && new.target.enqueueToRenderOnResize)
+        if (!ComponentBase.useNativeCustomElements)
+            element.addEventListener('DOMNodeInsertedIntoDocument', () => this.enqueueToRender());
+        if (!ComponentBase.useNativeCustomElements && new.target.enqueueToRenderOnResize)
             ComponentBase._connectedComponentToRenderOnResize(this);
     }
 
     element() { return this._element; }
-    content()
+    content(id = null)
     {
         this._ensureShadowTree();
+        if (this._shadow && id != null)
+            return this._shadow.getElementById(id);
         return this._shadow;
     }
 
+    part(id)
+    {
+        this._ensureShadowTree();
+        if (!this._shadow)
+            return null;
+        const part = this._shadow.getElementById(id);
+        if (!part)
+            return null;
+        return part.component();
+    }
+
+    dispatchAction(actionName, ...args)
+    {
+        const callback = this._actionCallbacks.get(actionName);
+        if (callback)
+            return callback.apply(this, args);
+    }
+
+    listenToAction(actionName, callback)
+    {
+        this._actionCallbacks.set(actionName, callback);
+    }
+
     render() { this._ensureShadowTree(); }
 
     enqueueToRender()
@@ -46,18 +76,55 @@ class ComponentBase {
     {
         Instrumentation.startMeasuringTime('ComponentBase', 'renderingTimerDidFire');
 
+        const componentsToRender = ComponentBase._componentsToRender;
+        this._renderLoop();
+        if (ComponentBase._componentsToRenderOnResize) {
+            const resizedComponents = this._resizedComponents(ComponentBase._componentsToRenderOnResize);
+            if (resizedComponents.length) {
+                ComponentBase._componentsToRender = new Set(resizedComponents);
+                this._renderLoop();
+            }
+        }
+
+        Instrumentation.endMeasuringTime('ComponentBase', 'renderingTimerDidFire');
+    }
+
+    static _renderLoop()
+    {
+        const componentsToRender = ComponentBase._componentsToRender;
         do {
-            const currentSet = [...ComponentBase._componentsToRender];
-            ComponentBase._componentsToRender.clear();
+            const currentSet = [...componentsToRender];
+            componentsToRender.clear();
+            const resizeSet = ComponentBase._componentsToRenderOnResize;
             for (let component of currentSet) {
                 Instrumentation.startMeasuringTime('ComponentBase', 'renderingTimerDidFire.render');
                 component.render();
+                if (resizeSet && resizeSet.has(component)) {
+                    const element = component.element();
+                    component._oldSizeToCheckForResize = {width: element.offsetWidth, height: element.offsetHeight};
+                }
                 Instrumentation.endMeasuringTime('ComponentBase', 'renderingTimerDidFire.render');
             }
-        } while (ComponentBase._componentsToRender.size);
+        } while (componentsToRender.size);
         ComponentBase._componentsToRender = null;
+    }
 
-        Instrumentation.endMeasuringTime('ComponentBase', 'renderingTimerDidFire');
+    static _resizedComponents(componentSet)
+    {
+        if (!componentSet)
+            return [];
+
+        const resizedList = [];
+        for (let component of componentSet) {
+            const element = component.element();
+            const width = element.offsetWidth;
+            const height = element.offsetHeight;
+            const oldSize = component._oldSizeToCheckForResize;
+            if (oldSize && oldSize.width == width && oldSize.height == height)
+                continue;
+            resizedList.push(component);
+        }
+        return resizedList;
     }
 
     static _connectedComponentToRenderOnResize(component)
@@ -65,7 +132,8 @@ class ComponentBase {
         if (!ComponentBase._componentsToRenderOnResize) {
             ComponentBase._componentsToRenderOnResize = new Set;
             window.addEventListener('resize', () => {
-                for (let component of ComponentBase._componentsToRenderOnResize)
+                const resized = this._resizedComponents(ComponentBase._componentsToRenderOnResize);
+                for (const component of resized)
                     component.enqueueToRender();
             });
         }
@@ -77,61 +145,63 @@ class ComponentBase {
         ComponentBase._componentsToRenderOnResize.delete(component);
     }
 
-    renderReplace(element, content) { ComponentBase.renderReplace(element, content); }
-
-    static renderReplace(element, content)
-    {
-        element.innerHTML = '';
-        if (content)
-            ComponentBase._addContentToElement(element, content);
-    }
-
     _ensureShadowTree()
     {
         if (this._shadow)
             return;
 
-        const newTarget = this.__proto__.constructor;
-        const htmlTemplate = newTarget['htmlTemplate'];
-        const cssTemplate = newTarget['cssTemplate'];
+        const thisClass = this.__proto__.constructor;
+
+        let content;
+        let stylesheet;
+        if (!thisClass.hasOwnProperty('_parsed') || !thisClass._parsed) {
+            thisClass._parsed = true;
 
-        if (!htmlTemplate && !cssTemplate)
+            const contentTemplate = thisClass['contentTemplate'];
+            if (contentTemplate)
+                content = ComponentBase._constructNodeTreeFromTemplate(contentTemplate);
+            else if (thisClass.htmlTemplate) {
+                const templateElement = document.createElement('template');
+                templateElement.innerHTML = thisClass.htmlTemplate();
+                content = [templateElement.content];
+            }
+
+            const styleTemplate = thisClass['styleTemplate'];
+            if (styleTemplate)
+                stylesheet = ComponentBase._constructStylesheetFromTemplate(styleTemplate);
+            else if (thisClass.cssTemplate)
+                stylesheet = thisClass.cssTemplate();
+
+            thisClass._parsedContent = content;
+            thisClass._parsedStylesheet = stylesheet;
+        } else {
+            content = thisClass._parsedContent;
+            stylesheet = thisClass._parsedStylesheet;
+        }
+
+        if (!content && !stylesheet)
             return;
 
         const shadow = this._element.attachShadow({mode: 'closed'});
 
-        if (htmlTemplate) {
-            const template = document.createElement('template');
-            template.innerHTML = newTarget.htmlTemplate();
-            shadow.appendChild(document.importNode(template.content, true));
-            this._recursivelyReplaceUnknownElementsByComponents(shadow);
+        if (content) {
+            for (const node of content)
+                shadow.appendChild(document.importNode(node, true));
+            this._recursivelyUpgradeUnknownElements(shadow, (node) => {
+                return node instanceof Element ? ComponentBase._componentByName.get(node.localName) : null;
+            });
         }
 
-        if (cssTemplate) {
+        if (stylesheet) {
             const style = document.createElement('style');
-            style.textContent = newTarget.cssTemplate();
+            style.textContent = stylesheet;
             shadow.appendChild(style);
         }
-
         this._shadow = shadow;
+        this.didConstructShadowTree();
     }
 
-    _recursivelyReplaceUnknownElementsByComponents(parent)
-    {
-        let nextSibling;
-        for (let child = parent.firstChild; child; child = child.nextSibling) {
-            if (child instanceof HTMLElement && !child.component) {
-                const elementInterface = ComponentBase._componentByName.get(child.localName);
-                if (elementInterface) {
-                    const component = new elementInterface();
-                    const newChild = component.element();
-                    parent.replaceChild(newChild, child);
-                    child = newChild;
-                }
-            }
-            this._recursivelyReplaceUnknownElementsByComponents(child);
-        }
-    }
+    didConstructShadowTree() { }
 
     static defineElement(name, elementInterface)
     {
@@ -140,7 +210,7 @@ class ComponentBase {
 
         const enqueueToRenderOnResize = elementInterface.enqueueToRenderOnResize;
 
-        if (!window.customElements)
+        if (!ComponentBase.useNativeCustomElements)
             return;
 
         class elementClass extends HTMLElement {
@@ -160,6 +230,7 @@ class ComponentBase {
 
             connectedCallback()
             {
+                this.component().enqueueToRender();
                 if (enqueueToRenderOnResize)
                     ComponentBase._connectedComponentToRenderOnResize(this.component());
             }
@@ -178,76 +249,24 @@ class ComponentBase {
         customElements.define(name, elementClass);
     }
 
-    static createElement(name, attributes, content)
-    {
-        var element = document.createElement(name);
-        if (!content && (attributes instanceof Array || attributes instanceof Node
-            || attributes instanceof ComponentBase || typeof(attributes) != 'object')) {
-            content = attributes;
-            attributes = {};
-        }
-
-        if (attributes) {
-            for (var name in attributes) {
-                if (name.startsWith('on'))
-                    element.addEventListener(name.substring(2), attributes[name]);
-                else
-                    element.setAttribute(name, attributes[name]);
-            }
-        }
-
-        if (content)
-            ComponentBase._addContentToElement(element, content);
-
-        return element;
-    }
-
-    static _addContentToElement(element, content)
-    {
-        if (content instanceof Array) {
-            for (var nestedChild of content)
-                this._addContentToElement(element, nestedChild);
-        } else if (content instanceof Node)
-            element.appendChild(content);
-         else if (content instanceof ComponentBase)
-            element.appendChild(content.element());
-        else
-            element.appendChild(document.createTextNode(content));
-    }
-
-    static createLink(content, titleOrCallback, callback, isExternal)
-    {
-        var title = titleOrCallback;
-        if (callback === undefined) {
-            title = content;
-            callback = titleOrCallback;
-        }
-
-        var attributes = {
-            href: '#',
-            title: title,
-        };
-
-        if (typeof(callback) === 'string')
-            attributes['href'] = callback;
-        else
-            attributes['onclick'] = ComponentBase.createActionHandler(callback);
-
-        if (isExternal)
-            attributes['target'] = '_blank';
-        return ComponentBase.createElement('a', attributes, content);
-    }
-
-    static createActionHandler(callback)
+    createEventHandler(callback, options={}) { return ComponentBase.createEventHandler(callback, options); }
+    static createEventHandler(callback, options={})
     {
         return function (event) {
-            event.preventDefault();
-            event.stopPropagation();
+            if (!('preventDefault' in options) || options['preventDefault'])
+                event.preventDefault();
+            if (!('stopPropagation' in options) || options['stopPropagation'])
+                event.stopPropagation();
             callback.call(this, event);
         };
     }
 }
 
+CommonComponentBase._context = document;
+CommonComponentBase._isNode = (node) => node instanceof Node;
+CommonComponentBase._baseClass = ComponentBase;
+
+ComponentBase.useNativeCustomElements = !!window.customElements;
 ComponentBase._componentByName = new Map;
 ComponentBase._componentByClass = new Map;
 ComponentBase._currentlyConstructedByInterface = new Map;