Adopt custom elements API in perf dashboard
authorrniwa@webkit.org <rniwa@webkit.org@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Mon, 16 Jan 2017 00:12:22 +0000 (00:12 +0000)
committerrniwa@webkit.org <rniwa@webkit.org@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Mon, 16 Jan 2017 00:12:22 +0000 (00:12 +0000)
https://bugs.webkit.org/show_bug.cgi?id=167045

Reviewed by Darin Adler.

Adopt custom elements API in ComponentBase, and create the shadow tree lazily in content() and render()
instead of eagerly creating it inside the constructor.

For now, create a separate element class for each component in ComponentBase.defineElement instead of
making ComponentBase inherit from HTMLElement to preserve the semantics we have as well as to test
the boundaries of what custom elements API allows for framework authors.

In order to ensure one-to-one correspondence between elements and their components, we use a static map,
ComponentBase._currentlyConstructedByInterface, to remember which element or component is being created
and use that in custom element's constructor to update element.component() and this._element.

Also dropped the support for not having attachShadow as we've shipped this feature in Safari 10.

Finally, added tests to be ran inside a browser to test the front end code in browser-tests.

* browser-tests/component-base-tests.js: Added. Basic tests for ComponentBase.
* browser-tests/index.html: Added.

* public/v3/components/base.js:
(ComponentBase): Don't create the shadow tree. Use the currently constructed element as this._element if
there is one (the custom element's constructor is getting called). Otherwise create a new element but
store this component in the map to avoid creating a new component in the custom element's constructor.
(ComponentBase.prototype.content): Lazily create the shadow tree now.
(ComponentBase.prototype.render): Ditto.
(ComponentBase.prototype._ensureShadowTree): Renamed from _constructShadowTree. Dropped the support for
not having shadow DOM API. This is now required. Also use importNode instead of cloneNode in cloning
the template content since the latter would not get upgraded.
(ComponentBase.prototype._recursivelyReplaceUnknownElementsByComponents): Modernized the code. Don't
re-create a component if its element had already been upgraded by its custom element constructor.
(ComponentBase.defineElement): Add this component to the static maps. _componentByName is used by
_recursivelyReplaceUnknownElementsByComponents to instantiate new components in the browsers that don't
support custom elements API and _componentByClass is used by ComponentBase's constructor to lookup the
element name. The latter should go away once all components fully adopt ComponentBase.defineElement.
(ComponentBase.defineElement.elementClass): A class to define a custom element for the component.
We need to reconfigure the property since class's name is not writable but configurable.

* public/v3/components/button-base.js:
(ButtonBase.htmlTemplate): Added. Extracted the common code from CloseButton and WarningIcon.
(ButtonBase.buttonContent): Added. An abstract method overridden by CloseButton and WarningIcon.
(ButtonBase.sizeFactor): Added. Overridden by WarningIcon.
(ButtonBase.cssTemplate): Updated to use :host.
* public/v3/components/close-button.js:
(CloseButton.buttonContent): Renamed from htmlTemplate.
* public/v3/components/spinner-icon.js:
(SpinnerIcon.cssTemplate): Removed webkit prefixed properties, and updated it to animate stroke instead
of opacity to reduce the power usage.
(SpinnerIcon.htmlTemplate): Factored stroke, stroke-width, and stroke-linecap into cssTemplate.
* public/v3/components/warning-icon.js:
(WarningIcon.cssTemplate): Deleted.
(WarningIcon.sizeFactor): Added.
(WarningIcon.buttonContent): Renamed from htmlTemplate.
* public/v3/pages/summary-page.js:
(SummaryPage._constructRatioGraph): Fixed a bug that we were not never calling spinner.updateRendering().
(SummaryPage.prototype._renderCell):

git-svn-id: https://svn.webkit.org/repository/webkit/trunk@210783 268f45cc-cd09-0410-ab3c-d52691b4dbfc

Websites/perf.webkit.org/ChangeLog
Websites/perf.webkit.org/browser-tests/component-base-tests.js [new file with mode: 0644]
Websites/perf.webkit.org/browser-tests/index.html [new file with mode: 0644]
Websites/perf.webkit.org/public/v3/components/base.js
Websites/perf.webkit.org/public/v3/components/button-base.js
Websites/perf.webkit.org/public/v3/components/close-button.js
Websites/perf.webkit.org/public/v3/components/spinner-icon.js
Websites/perf.webkit.org/public/v3/components/warning-icon.js
Websites/perf.webkit.org/public/v3/main.js
Websites/perf.webkit.org/public/v3/pages/summary-page.js

index 4c347cea785df5c02c3cad145e5f51c100971123..0714d7761c60eb47eb9ade563b196e2c3aa3bdbd 100644 (file)
@@ -1,3 +1,65 @@
+2017-01-12  Ryosuke Niwa  <rniwa@webkit.org>
+
+        Adopt custom elements API in perf dashboard
+        https://bugs.webkit.org/show_bug.cgi?id=167045
+
+        Reviewed by Darin Adler.
+
+        Adopt custom elements API in ComponentBase, and create the shadow tree lazily in content() and render()
+        instead of eagerly creating it inside the constructor.
+
+        For now, create a separate element class for each component in ComponentBase.defineElement instead of
+        making ComponentBase inherit from HTMLElement to preserve the semantics we have as well as to test
+        the boundaries of what custom elements API allows for framework authors.
+
+        In order to ensure one-to-one correspondence between elements and their components, we use a static map,
+        ComponentBase._currentlyConstructedByInterface, to remember which element or component is being created
+        and use that in custom element's constructor to update element.component() and this._element.
+
+        Also dropped the support for not having attachShadow as we've shipped this feature in Safari 10.
+
+        Finally, added tests to be ran inside a browser to test the front end code in browser-tests.
+
+        * browser-tests/component-base-tests.js: Added. Basic tests for ComponentBase.
+        * browser-tests/index.html: Added.
+
+        * public/v3/components/base.js:
+        (ComponentBase): Don't create the shadow tree. Use the currently constructed element as this._element if
+        there is one (the custom element's constructor is getting called). Otherwise create a new element but
+        store this component in the map to avoid creating a new component in the custom element's constructor.
+        (ComponentBase.prototype.content): Lazily create the shadow tree now.
+        (ComponentBase.prototype.render): Ditto.
+        (ComponentBase.prototype._ensureShadowTree): Renamed from _constructShadowTree. Dropped the support for
+        not having shadow DOM API. This is now required. Also use importNode instead of cloneNode in cloning
+        the template content since the latter would not get upgraded.
+        (ComponentBase.prototype._recursivelyReplaceUnknownElementsByComponents): Modernized the code. Don't
+        re-create a component if its element had already been upgraded by its custom element constructor.
+        (ComponentBase.defineElement): Add this component to the static maps. _componentByName is used by
+        _recursivelyReplaceUnknownElementsByComponents to instantiate new components in the browsers that don't
+        support custom elements API and _componentByClass is used by ComponentBase's constructor to lookup the
+        element name. The latter should go away once all components fully adopt ComponentBase.defineElement.
+        (ComponentBase.defineElement.elementClass): A class to define a custom element for the component.
+        We need to reconfigure the property since class's name is not writable but configurable.
+
+        * public/v3/components/button-base.js:
+        (ButtonBase.htmlTemplate): Added. Extracted the common code from CloseButton and WarningIcon.
+        (ButtonBase.buttonContent): Added. An abstract method overridden by CloseButton and WarningIcon.
+        (ButtonBase.sizeFactor): Added. Overridden by WarningIcon.
+        (ButtonBase.cssTemplate): Updated to use :host.
+        * public/v3/components/close-button.js:
+        (CloseButton.buttonContent): Renamed from htmlTemplate.
+        * public/v3/components/spinner-icon.js:
+        (SpinnerIcon.cssTemplate): Removed webkit prefixed properties, and updated it to animate stroke instead
+        of opacity to reduce the power usage.
+        (SpinnerIcon.htmlTemplate): Factored stroke, stroke-width, and stroke-linecap into cssTemplate.
+        * public/v3/components/warning-icon.js:
+        (WarningIcon.cssTemplate): Deleted.
+        (WarningIcon.sizeFactor): Added.
+        (WarningIcon.buttonContent): Renamed from htmlTemplate.
+        * public/v3/pages/summary-page.js:
+        (SummaryPage._constructRatioGraph): Fixed a bug that we were not never calling spinner.updateRendering().
+        (SummaryPage.prototype._renderCell):
+
 2017-01-13  Ryosuke Niwa  <rniwa@webkit.org>
 
         Instrument calls to render()
diff --git a/Websites/perf.webkit.org/browser-tests/component-base-tests.js b/Websites/perf.webkit.org/browser-tests/component-base-tests.js
new file mode 100644 (file)
index 0000000..ef50ce3
--- /dev/null
@@ -0,0 +1,184 @@
+
+describe('ComponentBase', function() {
+
+    function createTestToCheckExistenceOfShadowTree(callback, options = {htmlTemplate: false, cssTemplate: true})
+    {
+        const context = new BrowsingContext();
+        return context.importScript('../public/v3/components/base.js', 'ComponentBase').then((ComponentBase) => {
+            class SomeComponent extends ComponentBase { }
+            if (options.htmlTemplate)
+                SomeComponent.htmlTemplate = () => { return '<div style="height: 10px;"></div>'; };
+            if (options.cssTemplate)
+                SomeComponent.cssTemplate = () => { return ':host { height: 10px; }'; };
+
+            let instance = new SomeComponent('some-component');
+            instance.element().style.display = 'block';
+            context.document.body.appendChild(instance.element());
+            return callback(instance, () => { return instance.element().offsetHeight == 10; });
+        });
+    }
+
+    describe('constructor', () => {
+        it('is a function', () => {
+            return new BrowsingContext().importScript('../public/v3/components/base.js', 'ComponentBase').then((ComponentBase) => {
+                expect(ComponentBase).toBeA('function');
+            });
+        });
+
+        it('can be instantiated', () => {
+            return new BrowsingContext().importScript('../public/v3/components/base.js', 'ComponentBase').then((ComponentBase) => {
+                let callCount = 0;
+                class SomeComponent extends ComponentBase {
+                    constructor() {
+                        super('some-component');
+                        callCount++;
+                    }
+                }
+                let instance = new SomeComponent;
+                expect(instance).toBeA(ComponentBase);
+                expect(instance).toBeA(SomeComponent);
+                expect(callCount).toBe(1);
+            });
+        });
+
+        it('must not create shadow tree eagerly', () => {
+            return createTestToCheckExistenceOfShadowTree((instance, hasShadowTree) => {
+                expect(hasShadowTree()).toBe(false);
+            });
+        });
+    });
+
+    describe('element()', () => {
+        it('must return an element', () => {
+            const context = new BrowsingContext();
+            return context.importScript('../public/v3/components/base.js', 'ComponentBase').then((ComponentBase) => {
+                class SomeComponent extends ComponentBase { }
+                let instance = new SomeComponent('some-component');
+                expect(instance.element()).toBeA(context.global.HTMLElement);
+            });
+        });
+
+        it('must return an element whose component() matches the component', () => {
+            return new BrowsingContext().importScript('../public/v3/components/base.js', 'ComponentBase').then((ComponentBase) => {
+                class SomeComponent extends ComponentBase { }
+                let instance = new SomeComponent('some-component');
+                expect(instance.element().component()).toBe(instance);
+            });
+        });
+
+        it('must not create shadow tree eagerly', () => {
+            return createTestToCheckExistenceOfShadowTree((instance, hasShadowTree) => {
+                instance.element();
+                expect(hasShadowTree()).toBe(false);
+            });
+        });
+    });
+
+    describe('content()', () => {
+        it('must create shadow tree', () => {
+            return createTestToCheckExistenceOfShadowTree((instance, hasShadowTree) => {
+                instance.content();
+                expect(hasShadowTree()).toBe(true);
+            });
+        });
+
+        it('must return the same shadow tree each time it is called', () => {
+            return createTestToCheckExistenceOfShadowTree((instance, hasShadowTree) => {
+                expect(instance.content()).toBe(instance.content());
+            });
+        });
+    });
+
+    describe('render()', () => {
+        it('must create shadow tree', () => {
+            return createTestToCheckExistenceOfShadowTree((instance, hasShadowTree) => {
+                instance.render();
+                expect(hasShadowTree()).toBe(true);
+            });
+        });
+
+        it('must not create shadow tree when neither htmlTemplate nor cssTemplate are present', () => {
+            return createTestToCheckExistenceOfShadowTree((instance, hasShadowTree) => {
+                instance.render();
+                expect(hasShadowTree()).toBe(false);
+            }, {htmlTemplate: false, cssTemplate: false});
+        });
+
+        it('must create shadow tree when htmlTemplate is present and cssTemplate is not', () => {
+            return createTestToCheckExistenceOfShadowTree((instance, hasShadowTree) => {
+                instance.render();
+                expect(hasShadowTree()).toBe(true);
+            }, {htmlTemplate: true, cssTemplate: false});
+        });
+
+        it('must create shadow tree when cssTemplate is present and htmlTemplate is not', () => {
+            return createTestToCheckExistenceOfShadowTree((instance, hasShadowTree) => {
+                instance.render();
+                expect(hasShadowTree()).toBe(true);
+            }, {htmlTemplate: false, cssTemplate: true});
+        });
+    });
+
+    describe('defineElement()', () => {
+
+        it('must define a custom element with a class of an appropriate name', () => {
+            const context = new BrowsingContext();
+            return context.importScript('../public/v3/components/base.js', 'ComponentBase').then((ComponentBase) => {
+                class SomeComponent extends ComponentBase { }
+                ComponentBase.defineElement('some-component', SomeComponent);
+
+                let elementClass = context.global.customElements.get('some-component');
+                expect(elementClass).toBeA('function');
+                expect(elementClass.name).toBe('SomeComponentElement');
+            });
+        });
+
+        it('must define a custom element that can be instantiated via document.createElement', () => {
+            const context = new BrowsingContext();
+            return context.importScript('../public/v3/components/base.js', 'ComponentBase').then((ComponentBase) => {
+                let instances = [];
+                class SomeComponent extends ComponentBase {
+                    constructor() {
+                        super();
+                        instances.push(this);
+                    }
+                }
+                ComponentBase.defineElement('some-component', SomeComponent);
+
+                expect(instances.length).toBe(0);
+                let element = context.document.createElement('some-component');
+                expect(instances.length).toBe(1);
+
+                expect(element).toBeA(context.global.HTMLElement);
+                expect(element.component()).toBe(instances[0]);
+                expect(instances[0].element()).toBe(element);
+                expect(instances.length).toBe(1);
+            });
+        });
+
+        it('must define a custom element that can be instantiated via new', () => {
+            const context = new BrowsingContext();
+            return context.importScript('../public/v3/components/base.js', 'ComponentBase').then((ComponentBase) => {
+                let instances = [];
+                class SomeComponent extends ComponentBase {
+                    constructor() {
+                        super();
+                        instances.push(this);
+                    }
+                }
+                ComponentBase.defineElement('some-component', SomeComponent);
+
+                expect(instances.length).toBe(0);
+                let component = new SomeComponent;
+                expect(instances.length).toBe(1);
+
+                expect(component).toBe(instances[0]);
+                expect(component.element()).toBeA(context.global.HTMLElement);
+                expect(component.element().component()).toBe(component);
+                expect(instances.length).toBe(1);
+            });
+        });
+
+    });
+
+});
diff --git a/Websites/perf.webkit.org/browser-tests/index.html b/Websites/perf.webkit.org/browser-tests/index.html
new file mode 100644 (file)
index 0000000..e40dd46
--- /dev/null
@@ -0,0 +1,101 @@
+<!DOCTYPE html>
+<html>
+<head>
+<link rel="stylesheet" href="https://cdn.rawgit.com/mochajs/mocha/2.2.5/mocha.css">
+<script src="https://cdnjs.cloudflare.com/ajax/libs/mocha/2.2.5/mocha.js"></script>
+<script src="https://cdnjs.cloudflare.com/ajax/libs/expect/1.20.2/expect.min.js"></script>
+<script>
+
+mocha.setup('bdd');
+
+</script>
+</head>
+<body>
+<div id="mocha"></div>
+<script src="component-base-tests.js"></script>
+<script>
+
+afterEach(() => {
+    BrowsingContext.cleanup();
+});
+
+class BrowsingContext {
+
+    constructor()
+    {
+        let iframe = document.createElement('iframe');
+        document.body.appendChild(iframe);
+        iframe.style.position = 'absolute';
+        iframe.style.left = '0px';
+        iframe.style.top = '0px';
+        BrowsingContext._iframes.push(iframe);
+
+        this._iframe = iframe;
+        this.symbols = {};
+        this.global = this._iframe.contentWindow;
+        this.document = this._iframe.contentDocument;
+    }
+
+    importScript(path, ...symbolList)
+    {
+        const doc = this._iframe.contentDocument;
+        const global = this._iframe.contentWindow;
+        return new Promise((resolve, reject) => {
+            let script = doc.createElement('script');
+            script.addEventListener('load', resolve);
+            script.addEventListener('error', reject);
+            script.src = path;
+            doc.body.appendChild(script);
+        }).then(() => {
+            const script = doc.createElement('script');
+            script.textContent = `window.importedSymbols = [${symbolList.join(', ')}];`;
+            doc.body.appendChild(script);
+
+            const importedSymbols = global.importedSymbols;
+            for (let i = 0; i < symbolList.length; i++)
+                this.symbols[symbolList[i]] = importedSymbols[i];
+
+            return symbolList.length == 1 ? importedSymbols[0] : importedSymbols;
+        });
+    }
+
+    static cleanup()
+    {
+        BrowsingContext._iframes.forEach((iframe) => { iframe.remove(); });
+        BrowsingContext._iframes = [];
+    }
+
+    static createWithScripts(scriptList)
+    {
+        let iframe = document.createElement('iframe');
+        document.body.appendChild(iframe);
+        const doc = iframe.contentDocument;
+
+        let symbolList = [];
+        return Promise.all(scriptList.map((entry) => {
+            let [path, ...symbols] = entry;
+            symbolList = symbolList.concat(symbols);
+            return new Promise((resolve, reject) => {
+                let script = doc.createElement('script');
+                script.addEventListener('load', resolve);
+                script.addEventListener('error', reject);
+                script.src = path;
+                doc.body.appendChild(script);
+            });
+        })).then(() => {
+            const script = doc.createElement('script');
+            script.textContent = `var symbols = { ${symbolList.join(', ')} };`;
+            doc.body.appendChild(script);
+            return iframe.contentWindow;
+        });
+    }
+}
+BrowsingContext._iframes = [];
+
+mocha.checkLeaks();
+mocha.globals(['expect', 'BrowsingContext']);
+mocha.run();
+
+</script>
+</body>
+</html>
index 214fac54429ba04e5f308d33fab194cf5e190612..612b0fb69e556ef38e2e7d98452033c35bedde03 100644 (file)
@@ -1,16 +1,30 @@
 
-// FIXME: ComponentBase should inherit from HTMLElement when custom elements API is available.
 class ComponentBase {
     constructor(name)
     {
-        this._element = document.createElement(name);
-        this._element.component = (function () { return this; }).bind(this);
-        this._shadow = this._constructShadowTree();
+        this._componentName = name || ComponentBase._componentByClass.get(new.target);
+
+        const currentlyConstructed = ComponentBase._currentlyConstructedByInterface;
+        let element = currentlyConstructed.get(new.target);
+        if (!element) {
+            currentlyConstructed.set(new.target, this);
+            element = document.createElement(this._componentName);
+            currentlyConstructed.delete(new.target);
+        }
+        element.component = () => { return this };
+
+        this._element = element;
+        this._shadow = null;
     }
 
     element() { return this._element; }
-    content() { return this._shadow; }
-    render() { }
+    content()
+    {
+        this._ensureShadowTree();
+        return this._shadow;
+    }
+
+    render() { this._ensureShadowTree(); }
 
     updateRendering()
     {
@@ -28,52 +42,45 @@ class ComponentBase {
             ComponentBase._addContentToElement(element, content);
     }
 
-    _constructShadowTree()
+    _ensureShadowTree()
     {
-        var newTarget = this.__proto__.constructor;
+        if (this._shadow)
+            return;
 
-        var htmlTemplate = newTarget['htmlTemplate'];
-        var cssTemplate = newTarget['cssTemplate'];
+        const newTarget = this.__proto__.constructor;
+        const htmlTemplate = newTarget['htmlTemplate'];
+        const cssTemplate = newTarget['cssTemplate'];
 
         if (!htmlTemplate && !cssTemplate)
-            return null;
+            return;
 
-        var shadow = null;
-        if ('attachShadow' in Element.prototype)
-            shadow = this._element.attachShadow({mode: 'closed'});
-        else if ('createShadowRoot' in Element.prototype) // Legacy Chromium API.
-            shadow = this._element.createShadowRoot();
-        else
-            shadow = this._element;
+        const shadow = this._element.attachShadow({mode: 'closed'});
 
         if (htmlTemplate) {
-            var template = document.createElement('template');
+            const template = document.createElement('template');
             template.innerHTML = newTarget.htmlTemplate();
-            shadow.appendChild(template.content.cloneNode(true));
+            shadow.appendChild(document.importNode(template.content, true));
             this._recursivelyReplaceUnknownElementsByComponents(shadow);
         }
 
         if (cssTemplate) {
-            var style = document.createElement('style');
+            const style = document.createElement('style');
             style.textContent = newTarget.cssTemplate();
             shadow.appendChild(style);
         }
 
-        return shadow;
+        this._shadow = shadow;
     }
 
     _recursivelyReplaceUnknownElementsByComponents(parent)
     {
-        if (!ComponentBase._map)
-            return;
-
-        var nextSibling;
-        for (var child = parent.firstChild; child; child = child.nextSibling) {
-            if (child instanceof HTMLUnknownElement || child instanceof HTMLElement) {
-                var elementInterface = ComponentBase._map[child.localName];
+        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) {
-                    var component = new elementInterface();
-                    var newChild = component.element();
+                    const component = new elementInterface();
+                    const newChild = component.element();
                     parent.replaceChild(newChild, child);
                     child = newChild;
                 }
@@ -84,9 +91,30 @@ class ComponentBase {
 
     static defineElement(name, elementInterface)
     {
-        if (!ComponentBase._map)
-            ComponentBase._map = {};
-        ComponentBase._map[name] = elementInterface;
+        ComponentBase._componentByName.set(name, elementInterface);
+        ComponentBase._componentByClass.set(elementInterface, name);
+
+        class elementClass extends HTMLElement {
+            constructor()
+            {
+                super();
+
+                const currentlyConstructed = ComponentBase._currentlyConstructedByInterface;
+                const component = currentlyConstructed.get(elementInterface);
+                if (component)
+                    return; // ComponentBase's constructor will take care of the rest.
+
+                currentlyConstructed.set(elementInterface, this);
+                new elementInterface();
+                currentlyConstructed.delete(elementInterface);
+            }
+        }
+
+        const nameDescriptor = Object.getOwnPropertyDescriptor(elementClass, 'name');
+        nameDescriptor.value = `${elementInterface.name}Element`;
+        Object.defineProperty(elementClass, 'name', nameDescriptor);
+
+        customElements.define(name, elementClass);
     }
 
     static createElement(name, attributes, content)
@@ -159,6 +187,10 @@ class ComponentBase {
     }
 }
 
+ComponentBase._componentByName = new Map;
+ComponentBase._componentByClass = new Map;
+ComponentBase._currentlyConstructedByInterface = new Map;
+
 ComponentBase.css = Symbol();
 ComponentBase.html = Symbol();
 ComponentBase.map = {};
index bfb3f821cb0dedad8d936af1c91f756ae6beb751..8f2f8b00d005f48abca0f1a154c0635237965045 100644 (file)
@@ -10,21 +10,37 @@ class ButtonBase extends ComponentBase {
         this.content().querySelector('a').addEventListener('click', ComponentBase.createActionHandler(callback));
     }
 
+    static htmlTemplate()
+    {
+        return `<a class="button" href="#"><svg viewBox="0 0 100 100">${this.buttonContent()}</svg></a>`;
+    }
+
+    static buttonContent() { throw 'NotImplemented'; }
+    static sizeFactor() { return 1; }
+
     static cssTemplate()
     {
+        const sizeFactor = this.sizeFactor();
         return `
+            :host {
+                display: inline-block;
+                width: ${sizeFactor}rem;
+                height: ${sizeFactor}rem;
+            }
+
             .button {
                 vertical-align: bottom;
-                display: inline-block;
-                width: 1rem;
-                height: 1rem;
+                display: block;
                 opacity: 0.3;
             }
 
+            .button svg {
+                display: block;
+            }
+
             .button:hover {
                 opacity: 0.6;
             }
         `;
     }
-
 }
index 270bda88107eb90608b9ebb769df07a0c916ff2a..4cdac71289e748c1991ea1c8933f92d7a735b903 100644 (file)
@@ -5,16 +5,13 @@ class CloseButton extends ButtonBase {
         super('close-button');
     }
 
-    static htmlTemplate()
+    static buttonContent()
     {
-        return `
-            <a class="button" href="#"><svg viewBox="0 0 100 100">
-                <g stroke="black" stroke-width="10">
-                    <circle cx="50" cy="50" r="45" fill="transparent"/>
-                    <polygon points="30,30 70,70" />
-                    <polygon points="30,70 70,30" />
-                </g>
-            </svg></a>`;
+        return `<g stroke="black" stroke-width="10">
+            <circle cx="50" cy="50" r="45" fill="transparent"/>
+            <polygon points="30,30 70,70" />
+            <polygon points="30,70 70,30" />
+        </g>`;
     }
 }
 
index 195b650d2c28db731b308e2fd702691d3d9f6589..7ead4b85d8c7dcc1ffc639567a40d9ea4726a1cf 100644 (file)
@@ -8,76 +8,43 @@ class SpinnerIcon extends ComponentBase {
     static cssTemplate()
     {
         return `
-        .spinner {
+        :host {
+            display: inline-block;
             width: 2rem;
             height: 2rem;
-            -webkit-transform: translateZ(0);
+            will-change: opacity; /* Threre is no will-change: stroke. */
         }
-        .spinner line {
+        line {
             animation: spinner-animation 1.6s linear infinite;
-            -webkit-animation: spinner-animation 1.6s linear infinite;
-            opacity: 0.1;
-        }
-        .spinner line:nth-child(0) {
-            -webkit-animation-delay: 0.0s;
-            animation-delay: 0.0s;
-        }
-        .spinner line:nth-child(1) {
-            -webkit-animation-delay: 0.2s;
-            animation-delay: 0.2s;
-        }
-        .spinner line:nth-child(2) {
-            -webkit-animation-delay: 0.4s;
-            animation-delay: 0.4s;
-        }
-        .spinner line:nth-child(3) {
-            -webkit-animation-delay: 0.6s;
-            animation-delay: 0.6s;
-        }
-        .spinner line:nth-child(4) {
-            -webkit-animation-delay: 0.8s;
-            animation-delay: 0.8s;
-        }
-        .spinner line:nth-child(5) {
-            -webkit-animation-delay: 1s;
-            animation-delay: 1s;
-        }
-        .spinner line:nth-child(6) {
-            -webkit-animation-delay: 1.2s;
-            animation-delay: 1.2s;
-        }
-        .spinner line:nth-child(7) {
-            -webkit-animation-delay: 1.4s;
-            animation-delay: 1.4s;
-        }
-        .spinner line:nth-child(8) {
-            -webkit-animation-delay: 1.6s;
-            animation-delay: 1.6s;
-        }
+            stroke: rgb(230, 230, 230);
+            stroke-width: 10;
+            stroke-linecap: round;
+        }
+        line:nth-child(0) { animation-delay: 0.0s; }
+        line:nth-child(1) { animation-delay: 0.2s; }
+        line:nth-child(2) { animation-delay: 0.4s; }
+        line:nth-child(3) { animation-delay: 0.6s; }
+        line:nth-child(4) { animation-delay: 0.8s; }
+        line:nth-child(5) { animation-delay: 1.0s; }
+        line:nth-child(6) { animation-delay: 1.2s; }
+        line:nth-child(7) { animation-delay: 1.4s; }
         @keyframes spinner-animation {
-            0% { opacity: 0.9; }
-            50% { opacity: 0.1; }
-            100% { opacity: 0.1; }
-        }
-        @-webkit-keyframes spinner-animation {
-            0% { opacity: 0.9; }
-            50% { opacity: 0.1; }
-            100% { opacity: 0.1; }
-        }
-        `;
+            0% { stroke: rgb(25, 25, 25); }
+            50% { stroke: rgb(230, 230, 230); }
+        }`;
     }
 
     static htmlTemplate()
     {
         return `<svg class="spinner" viewBox="0 0 100 100">
-            <line x1="10" y1="50" x2="30" y2="50" stroke="black" stroke-width="10" stroke-linecap="round"/>
-            <line x1="21.72" y1="21.72" x2="35.86" y2="35.86" stroke="black" stroke-width="10" stroke-linecap="round"/>
-            <line x1="50" y1="10" x2="50" y2="30" stroke="black" stroke-width="10" stroke-linecap="round"/>
-            <line x1="78.28" y1="21.72" x2="64.14" y2="35.86" stroke="black" stroke-width="10" stroke-linecap="round"/>
-            <line x1="70" y1="50" x2="90" y2="50" stroke="black" stroke-width="10" stroke-linecap="round"/>
-            <line x1="65.86" y1="65.86" x2="78.28" y2="78.28" stroke="black" stroke-width="10" stroke-linecap="round"/>
-            <line x1="50" y1="70" x2="50" y2="90" stroke="black" stroke-width="10" stroke-linecap="round"/>
-            <line x1="21.72" y1="78.28" x2="35.86" y2="65.86" stroke="black" stroke-width="10" stroke-linecap="round"/>
+            <line x1="10" y1="50" x2="30" y2="50"/>
+            <line x1="21.72" y1="21.72" x2="35.86" y2="35.86"/>
+            <line x1="50" y1="10" x2="50" y2="30"/>
+            <line x1="78.28" y1="21.72" x2="64.14" y2="35.86"/>
+            <line x1="70" y1="50" x2="90" y2="50"/>
+            <line x1="65.86" y1="65.86" x2="78.28" y2="78.28"/>
+            <line x1="50" y1="70" x2="50" y2="90"/>
+            <line x1="21.72" y1="78.28" x2="35.86" y2="65.86"/>
         </svg>`;
     }
 
index d0b3889d6219ec3cb6d2d8b662141093d550c233..7b3efda53d12a17b6abb3bb8ffea1840c3c3ae13 100644 (file)
@@ -5,29 +5,14 @@ class WarningIcon extends ButtonBase {
         super('warning-icon');
     }
 
-    static cssTemplate()
-    {
-        return super.cssTemplate() + `
-            .button {
-                display: block;
-                width: 0.7rem;
-                height: 0.7rem;
-            }
-            .button svg {
-                display: block;
-            }
-        `;
-    }
+    static sizeFactor() { return 0.7; }
 
-    static htmlTemplate()
+    static buttonContent()
     {
-        return `<a class="button" href="#"><svg viewBox="0 0 100 100">
-            <g stroke="#9f6000" fill="#9f6000" stroke-width="7">
+        return `<g stroke="#9f6000" fill="#9f6000" stroke-width="7">
                 <polygon points="0,0, 100,0, 0,100" />
-            </g>
-        </svg></a>`;
+            </g>`;
     }
-
 }
 
 ComponentBase.defineElement('warning-icon', WarningIcon);
index ae5dfc2faf7a8704fb67662cff8bcba5fc7b9fce..6966393a9b5fa1b6264d4f9ac45b452cdc9adef7 100644 (file)
@@ -7,6 +7,17 @@ class SpinningPage extends Page {
 }
 
 function main() {
+    const requriedFeatures = {
+        'Custom Elements API': () => { return !!window.customElements; },
+        'Shadow DOM API': () => { return !!Element.prototype.attachShadow; },
+        'Latest DOM': () => { return !!Element.prototype.getRootNode; },
+    };
+
+    for (let name in requriedFeatures) {
+        if (!requriedFeatures[name]())
+            return alert(`Your browser does not support ${name}. Try using the latest Safari or Chrome.`);
+    }
+
     (new SpinningPage).open();
 
     Manifest.fetch().then(function (manifest) {
index b56786bd9eecf4121102155e08503ed5c03208d4..5397732a7736655ed3188a140bab54c7874993d9 100644 (file)
@@ -112,14 +112,17 @@ class SummaryPage extends PageWithHeading {
 
         var state = ChartsPage.createStateForConfigurationList(configurationList);
         var anchor = link(ratioGraph, this.router().url('charts', state));
-        var cell = element('td', [anchor, new SpinnerIcon]);
+        var spinner = new SpinnerIcon;
+        var cell = element('td', [anchor, spinner]);
 
-        this._renderQueue.push(this._renderCell.bind(this, cell, anchor, ratioGraph, configurationGroup));
+        this._renderQueue.push(this._renderCell.bind(this, cell, spinner, anchor, ratioGraph, configurationGroup));
         return cell;
     }
 
-    _renderCell(cell, anchor, ratioGraph, configurationGroup)
+    _renderCell(cell, spinner, anchor, ratioGraph, configurationGroup)
     {
+        spinner.updateRendering();
+
         if (configurationGroup.isFetching())
             cell.classList.add('fetching');
         else