Speedometer: Add an ES2015 TodoMVC implementation
authorrniwa@webkit.org <rniwa@webkit.org@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Fri, 12 May 2017 02:52:45 +0000 (02:52 +0000)
committerrniwa@webkit.org <rniwa@webkit.org@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Fri, 12 May 2017 02:52:45 +0000 (02:52 +0000)
https://bugs.webkit.org/show_bug.cgi?id=171448

Patch by Addy Osmani <addyosmani@gmail.com> on 2017-05-11
Reviewed by Ryosuke Niwa.

Adds ES2015 TodoMVC implementation for Speedometer

* Speedometer/resources/todomvc/vanilla-examples/es2015/index.html: Added.
* Speedometer/resources/todomvc/vanilla-examples/es2015/node_modules/todomvc-app-css/index.css: Added.
* Speedometer/resources/todomvc/vanilla-examples/es2015/node_modules/todomvc-app-css/package.json: Added.
* Speedometer/resources/todomvc/vanilla-examples/es2015/node_modules/todomvc-app-css/readme.md: Added.
* Speedometer/resources/todomvc/vanilla-examples/es2015/node_modules/todomvc-common/base.css: Added.
* Speedometer/resources/todomvc/vanilla-examples/es2015/node_modules/todomvc-common/base.js: Added.
* Speedometer/resources/todomvc/vanilla-examples/es2015/node_modules/todomvc-common/package.json: Added.
* Speedometer/resources/todomvc/vanilla-examples/es2015/node_modules/todomvc-common/readme.md: Added.
* Speedometer/resources/todomvc/vanilla-examples/es2015/package.json: Added.
* Speedometer/resources/todomvc/vanilla-examples/es2015/src/.jshintrc: Added.
* Speedometer/resources/todomvc/vanilla-examples/es2015/src/app.js: Added.
* Speedometer/resources/todomvc/vanilla-examples/es2015/src/controller.js: Added.
* Speedometer/resources/todomvc/vanilla-examples/es2015/src/helpers.js: Added.
* Speedometer/resources/todomvc/vanilla-examples/es2015/src/model.js: Added.
* Speedometer/resources/todomvc/vanilla-examples/es2015/src/store.js: Added.
* Speedometer/resources/todomvc/vanilla-examples/es2015/src/template.js: Added.
* Speedometer/resources/todomvc/vanilla-examples/es2015/src/view.js: Added.

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

18 files changed:
PerformanceTests/ChangeLog
PerformanceTests/Speedometer/resources/todomvc/vanilla-examples/es2015/index.html [new file with mode: 0644]
PerformanceTests/Speedometer/resources/todomvc/vanilla-examples/es2015/node_modules/todomvc-app-css/index.css [new file with mode: 0644]
PerformanceTests/Speedometer/resources/todomvc/vanilla-examples/es2015/node_modules/todomvc-app-css/package.json [new file with mode: 0755]
PerformanceTests/Speedometer/resources/todomvc/vanilla-examples/es2015/node_modules/todomvc-app-css/readme.md [new file with mode: 0755]
PerformanceTests/Speedometer/resources/todomvc/vanilla-examples/es2015/node_modules/todomvc-common/base.css [new file with mode: 0644]
PerformanceTests/Speedometer/resources/todomvc/vanilla-examples/es2015/node_modules/todomvc-common/base.js [new file with mode: 0644]
PerformanceTests/Speedometer/resources/todomvc/vanilla-examples/es2015/node_modules/todomvc-common/package.json [new file with mode: 0755]
PerformanceTests/Speedometer/resources/todomvc/vanilla-examples/es2015/node_modules/todomvc-common/readme.md [new file with mode: 0755]
PerformanceTests/Speedometer/resources/todomvc/vanilla-examples/es2015/package.json [new file with mode: 0755]
PerformanceTests/Speedometer/resources/todomvc/vanilla-examples/es2015/src/.jshintrc [new file with mode: 0755]
PerformanceTests/Speedometer/resources/todomvc/vanilla-examples/es2015/src/app.js [new file with mode: 0644]
PerformanceTests/Speedometer/resources/todomvc/vanilla-examples/es2015/src/controller.js [new file with mode: 0644]
PerformanceTests/Speedometer/resources/todomvc/vanilla-examples/es2015/src/helpers.js [new file with mode: 0644]
PerformanceTests/Speedometer/resources/todomvc/vanilla-examples/es2015/src/model.js [new file with mode: 0644]
PerformanceTests/Speedometer/resources/todomvc/vanilla-examples/es2015/src/store.js [new file with mode: 0644]
PerformanceTests/Speedometer/resources/todomvc/vanilla-examples/es2015/src/template.js [new file with mode: 0644]
PerformanceTests/Speedometer/resources/todomvc/vanilla-examples/es2015/src/view.js [new file with mode: 0644]

index 0ff7ee4..259f8c8 100644 (file)
@@ -1,5 +1,32 @@
 2017-05-11  Addy Osmani  <addyosmani@gmail.com>
 
+        Speedometer: Add an ES2015 TodoMVC implementation
+        https://bugs.webkit.org/show_bug.cgi?id=171448
+
+        Reviewed by Ryosuke Niwa.
+
+        Adds ES2015 TodoMVC implementation for Speedometer
+
+        * Speedometer/resources/todomvc/vanilla-examples/es2015/index.html: Added.
+        * Speedometer/resources/todomvc/vanilla-examples/es2015/node_modules/todomvc-app-css/index.css: Added.
+        * Speedometer/resources/todomvc/vanilla-examples/es2015/node_modules/todomvc-app-css/package.json: Added.
+        * Speedometer/resources/todomvc/vanilla-examples/es2015/node_modules/todomvc-app-css/readme.md: Added.
+        * Speedometer/resources/todomvc/vanilla-examples/es2015/node_modules/todomvc-common/base.css: Added.
+        * Speedometer/resources/todomvc/vanilla-examples/es2015/node_modules/todomvc-common/base.js: Added.
+        * Speedometer/resources/todomvc/vanilla-examples/es2015/node_modules/todomvc-common/package.json: Added.
+        * Speedometer/resources/todomvc/vanilla-examples/es2015/node_modules/todomvc-common/readme.md: Added.
+        * Speedometer/resources/todomvc/vanilla-examples/es2015/package.json: Added.
+        * Speedometer/resources/todomvc/vanilla-examples/es2015/src/.jshintrc: Added.
+        * Speedometer/resources/todomvc/vanilla-examples/es2015/src/app.js: Added.
+        * Speedometer/resources/todomvc/vanilla-examples/es2015/src/controller.js: Added.
+        * Speedometer/resources/todomvc/vanilla-examples/es2015/src/helpers.js: Added.
+        * Speedometer/resources/todomvc/vanilla-examples/es2015/src/model.js: Added.
+        * Speedometer/resources/todomvc/vanilla-examples/es2015/src/store.js: Added.
+        * Speedometer/resources/todomvc/vanilla-examples/es2015/src/template.js: Added.
+        * Speedometer/resources/todomvc/vanilla-examples/es2015/src/view.js: Added.
+
+2017-05-11  Addy Osmani  <addyosmani@gmail.com>
+
         Speedometer: Add an Elm TodoMVC implementation
         https://bugs.webkit.org/show_bug.cgi?id=171464
 
diff --git a/PerformanceTests/Speedometer/resources/todomvc/vanilla-examples/es2015/index.html b/PerformanceTests/Speedometer/resources/todomvc/vanilla-examples/es2015/index.html
new file mode 100644 (file)
index 0000000..db593ac
--- /dev/null
@@ -0,0 +1,46 @@
+<!DOCTYPE html>
+<html>
+  <head>
+    <meta charset="utf-8">
+    <title>ES6 • TodoMVC</title>
+    <link rel="stylesheet" href="node_modules/todomvc-common/base.css">
+    <link rel="stylesheet" href="node_modules/todomvc-app-css/index.css">
+  </head>
+  <body>
+    <section class="todoapp">
+        <header class="header">
+            <h1>todos</h1>
+            <input class="new-todo" placeholder="What needs to be done?" autofocus>
+        </header>
+
+        <section class="main">
+            <input class="toggle-all" type="checkbox">
+            <label for="toggle-all">Mark all as complete</label>
+            <ul class="todo-list"></ul>
+        </section>
+
+        <footer class="footer">
+            <span class="todo-count"></span>
+            <ul class="filters">
+                <li><a href="#/" class="selected">All</a></li>
+                <li><a href="#/active">Active</a></li>
+                <li><a href="#/completed">Completed</a></li>
+            </ul>
+            <button class="clear-completed">Clear completed</button>
+        </footer>
+    </section>
+
+    <footer class="info">
+        <p>Double-click to edit a todo</p>
+        <br>
+        <p>Part of <a href="http://todomvc.com">TodoMVC</a></p>
+    </footer>
+    <script src="src/controller.js"></script>
+    <script src="src/helpers.js"></script>
+    <script src="src/template.js"></script>
+    <script src="src/store.js"></script>
+    <script src="src/model.js"></script>
+    <script src="src/view.js"></script>
+    <script src="src/app.js"></script>
+  </body>
+</html>
diff --git a/PerformanceTests/Speedometer/resources/todomvc/vanilla-examples/es2015/node_modules/todomvc-app-css/index.css b/PerformanceTests/Speedometer/resources/todomvc/vanilla-examples/es2015/node_modules/todomvc-app-css/index.css
new file mode 100644 (file)
index 0000000..c819b47
--- /dev/null
@@ -0,0 +1,377 @@
+html,
+body {
+    margin: 0;
+    padding: 0;
+}
+
+button {
+    margin: 0;
+    padding: 0;
+    border: 0;
+    background: none;
+    font-size: 100%;
+    vertical-align: baseline;
+    font-family: inherit;
+    font-weight: inherit;
+    color: inherit;
+    -webkit-appearance: none;
+    appearance: none;
+    -webkit-font-smoothing: antialiased;
+    -moz-font-smoothing: antialiased;
+    font-smoothing: antialiased;
+}
+
+body {
+    font: 14px 'Helvetica Neue', Helvetica, Arial, sans-serif;
+    line-height: 1.4em;
+    background: #f5f5f5;
+    color: #4d4d4d;
+    min-width: 230px;
+    max-width: 550px;
+    margin: 0 auto;
+    -webkit-font-smoothing: antialiased;
+    -moz-font-smoothing: antialiased;
+    font-smoothing: antialiased;
+    font-weight: 300;
+}
+
+button,
+input[type="checkbox"] {
+    outline: none;
+}
+
+.hidden {
+    display: none;
+}
+
+.todoapp {
+    background: #fff;
+    margin: 130px 0 40px 0;
+    position: relative;
+    box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2),
+                0 25px 50px 0 rgba(0, 0, 0, 0.1);
+}
+
+.todoapp input::-webkit-input-placeholder {
+    font-style: italic;
+    font-weight: 300;
+    color: #e6e6e6;
+}
+
+.todoapp input::-moz-placeholder {
+    font-style: italic;
+    font-weight: 300;
+    color: #e6e6e6;
+}
+
+.todoapp input::input-placeholder {
+    font-style: italic;
+    font-weight: 300;
+    color: #e6e6e6;
+}
+
+.todoapp h1 {
+    position: absolute;
+    top: -155px;
+    width: 100%;
+    font-size: 100px;
+    font-weight: 100;
+    text-align: center;
+    color: rgba(175, 47, 47, 0.15);
+    -webkit-text-rendering: optimizeLegibility;
+    -moz-text-rendering: optimizeLegibility;
+    text-rendering: optimizeLegibility;
+}
+
+.new-todo,
+.edit {
+    position: relative;
+    margin: 0;
+    width: 100%;
+    font-size: 24px;
+    font-family: inherit;
+    font-weight: inherit;
+    line-height: 1.4em;
+    border: 0;
+    outline: none;
+    color: inherit;
+    padding: 6px;
+    border: 1px solid #999;
+    box-shadow: inset 0 -1px 5px 0 rgba(0, 0, 0, 0.2);
+    box-sizing: border-box;
+    -webkit-font-smoothing: antialiased;
+    -moz-font-smoothing: antialiased;
+    font-smoothing: antialiased;
+}
+
+.new-todo {
+    padding: 16px 16px 16px 60px;
+    border: none;
+    background: rgba(0, 0, 0, 0.003);
+    box-shadow: inset 0 -2px 1px rgba(0,0,0,0.03);
+}
+
+.main {
+    position: relative;
+    z-index: 2;
+    border-top: 1px solid #e6e6e6;
+}
+
+label[for='toggle-all'] {
+    display: none;
+}
+
+.toggle-all {
+    position: absolute;
+    top: -55px;
+    left: -12px;
+    width: 60px;
+    height: 34px;
+    text-align: center;
+    border: none; /* Mobile Safari */
+}
+
+.toggle-all:before {
+    content: '❯';
+    font-size: 22px;
+    color: #e6e6e6;
+    padding: 10px 27px 10px 27px;
+}
+
+.toggle-all:checked:before {
+    color: #737373;
+}
+
+.todo-list {
+    margin: 0;
+    padding: 0;
+    list-style: none;
+}
+
+.todo-list li {
+    position: relative;
+    font-size: 24px;
+    border-bottom: 1px solid #ededed;
+}
+
+.todo-list li:last-child {
+    border-bottom: none;
+}
+
+.todo-list li.editing {
+    border-bottom: none;
+    padding: 0;
+}
+
+.todo-list li.editing .edit {
+    display: block;
+    width: 506px;
+    padding: 13px 17px 12px 17px;
+    margin: 0 0 0 43px;
+}
+
+.todo-list li.editing .view {
+    display: none;
+}
+
+.todo-list li .toggle {
+    text-align: center;
+    width: 40px;
+    /* auto, since non-WebKit browsers doesn't support input styling */
+    height: auto;
+    position: absolute;
+    top: 0;
+    bottom: 0;
+    margin: auto 0;
+    border: none; /* Mobile Safari */
+    -webkit-appearance: none;
+    appearance: none;
+}
+
+.todo-list li .toggle:after {
+    content: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40" viewBox="-10 -18 100 135"><circle cx="50" cy="50" r="50" fill="none" stroke="#ededed" stroke-width="3"/></svg>');
+}
+
+.todo-list li .toggle:checked:after {
+    content: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40" viewBox="-10 -18 100 135"><circle cx="50" cy="50" r="50" fill="none" stroke="#bddad5" stroke-width="3"/><path fill="#5dc2af" d="M72 25L42 71 27 56l-4 4 20 20 34-52z"/></svg>');
+}
+
+.todo-list li label {
+    white-space: pre;
+    word-break: break-word;
+    padding: 15px 60px 15px 15px;
+    margin-left: 45px;
+    display: block;
+    line-height: 1.2;
+    transition: color 0.4s;
+}
+
+.todo-list li.completed label {
+    color: #d9d9d9;
+    text-decoration: line-through;
+}
+
+.todo-list li .destroy {
+    display: none;
+    position: absolute;
+    top: 0;
+    right: 10px;
+    bottom: 0;
+    width: 40px;
+    height: 40px;
+    margin: auto 0;
+    font-size: 30px;
+    color: #cc9a9a;
+    margin-bottom: 11px;
+    transition: color 0.2s ease-out;
+}
+
+.todo-list li .destroy:hover {
+    color: #af5b5e;
+}
+
+.todo-list li .destroy:after {
+    content: '×';
+}
+
+.todo-list li:hover .destroy {
+    display: block;
+}
+
+.todo-list li .edit {
+    display: none;
+}
+
+.todo-list li.editing:last-child {
+    margin-bottom: -1px;
+}
+
+.footer {
+    color: #777;
+    padding: 10px 15px;
+    height: 20px;
+    text-align: center;
+    border-top: 1px solid #e6e6e6;
+}
+
+.footer:before {
+    content: '';
+    position: absolute;
+    right: 0;
+    bottom: 0;
+    left: 0;
+    height: 50px;
+    overflow: hidden;
+    box-shadow: 0 1px 1px rgba(0, 0, 0, 0.2),
+                0 8px 0 -3px #f6f6f6,
+                0 9px 1px -3px rgba(0, 0, 0, 0.2),
+                0 16px 0 -6px #f6f6f6,
+                0 17px 2px -6px rgba(0, 0, 0, 0.2);
+}
+
+.todo-count {
+    float: left;
+    text-align: left;
+}
+
+.todo-count strong {
+    font-weight: 300;
+}
+
+.filters {
+    margin: 0;
+    padding: 0;
+    list-style: none;
+    position: absolute;
+    right: 0;
+    left: 0;
+}
+
+.filters li {
+    display: inline;
+}
+
+.filters li a {
+    color: inherit;
+    margin: 3px;
+    padding: 3px 7px;
+    text-decoration: none;
+    border: 1px solid transparent;
+    border-radius: 3px;
+}
+
+.filters li a.selected,
+.filters li a:hover {
+    border-color: rgba(175, 47, 47, 0.1);
+}
+
+.filters li a.selected {
+    border-color: rgba(175, 47, 47, 0.2);
+}
+
+.clear-completed,
+html .clear-completed:active {
+    float: right;
+    position: relative;
+    line-height: 20px;
+    text-decoration: none;
+    cursor: pointer;
+}
+
+.clear-completed:hover {
+    text-decoration: underline;
+}
+
+.info {
+    margin: 65px auto 0;
+    color: #bfbfbf;
+    font-size: 10px;
+    text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5);
+    text-align: center;
+}
+
+.info p {
+    line-height: 1;
+}
+
+.info a {
+    color: inherit;
+    text-decoration: none;
+    font-weight: 400;
+}
+
+.info a:hover {
+    text-decoration: underline;
+}
+
+/*
+    Hack to remove background from Mobile Safari.
+    Can't use it globally since it destroys checkboxes in Firefox
+*/
+@media screen and (-webkit-min-device-pixel-ratio:0) {
+    .toggle-all,
+    .todo-list li .toggle {
+        background: none;
+    }
+
+    .todo-list li .toggle {
+        height: 40px;
+    }
+
+    .toggle-all {
+        -webkit-transform: rotate(90deg);
+        transform: rotate(90deg);
+        -webkit-appearance: none;
+        appearance: none;
+    }
+}
+
+@media (max-width: 430px) {
+    .footer {
+        height: 50px;
+    }
+
+    .filters {
+        bottom: 10px;
+    }
+}
diff --git a/PerformanceTests/Speedometer/resources/todomvc/vanilla-examples/es2015/node_modules/todomvc-app-css/package.json b/PerformanceTests/Speedometer/resources/todomvc/vanilla-examples/es2015/node_modules/todomvc-app-css/package.json
new file mode 100755 (executable)
index 0000000..f524ebc
--- /dev/null
@@ -0,0 +1,69 @@
+{
+  "name": "todomvc-app-css",
+  "style": "index.css",
+  "version": "2.0.2",
+  "description": "CSS for TodoMVC apps",
+  "license": "CC-BY-4.0",
+  "repository": {
+    "type": "git",
+    "url": "git+https://github.com/tastejs/todomvc-app-css.git"
+  },
+  "author": {
+    "name": "Sindre Sorhus",
+    "email": "sindresorhus@gmail.com",
+    "url": "sindresorhus.com"
+  },
+  "files": [
+    "index.css"
+  ],
+  "keywords": [
+    "todomvc",
+    "tastejs",
+    "app",
+    "todo",
+    "template",
+    "css",
+    "style",
+    "stylesheet"
+  ],
+  "gitHead": "fd1e83f8f53a0b85537415365f2a6301bbb4199f",
+  "bugs": {
+    "url": "https://github.com/tastejs/todomvc-app-css/issues"
+  },
+  "homepage": "https://github.com/tastejs/todomvc-app-css",
+  "_id": "todomvc-app-css@2.0.2",
+  "scripts": {},
+  "_shasum": "31ff679dc3a409b260bc7e0a6eaca5f8757a89a1",
+  "_from": "todomvc-app-css@>=2.0.1 <3.0.0",
+  "_npmVersion": "2.14.7",
+  "_nodeVersion": "4.2.1",
+  "_npmUser": {
+    "name": "sindresorhus",
+    "email": "sindresorhus@gmail.com"
+  },
+  "dist": {
+    "shasum": "31ff679dc3a409b260bc7e0a6eaca5f8757a89a1",
+    "tarball": "http://registry.npmjs.org/todomvc-app-css/-/todomvc-app-css-2.0.2.tgz"
+  },
+  "maintainers": [
+    {
+      "name": "sindresorhus",
+      "email": "sindresorhus@gmail.com"
+    },
+    {
+      "name": "addyosmani",
+      "email": "addyosmani@gmail.com"
+    },
+    {
+      "name": "passy",
+      "email": "phartig@rdrei.net"
+    },
+    {
+      "name": "stephenplusplus",
+      "email": "sawchuk@gmail.com"
+    }
+  ],
+  "directories": {},
+  "_resolved": "https://registry.npmjs.org/todomvc-app-css/-/todomvc-app-css-2.0.2.tgz",
+  "readme": "ERROR: No README data found!"
+}
diff --git a/PerformanceTests/Speedometer/resources/todomvc/vanilla-examples/es2015/node_modules/todomvc-app-css/readme.md b/PerformanceTests/Speedometer/resources/todomvc/vanilla-examples/es2015/node_modules/todomvc-app-css/readme.md
new file mode 100755 (executable)
index 0000000..6ddbebf
--- /dev/null
@@ -0,0 +1,28 @@
+# todomvc-app-css
+
+> CSS for TodoMVC apps
+
+![](screenshot.png)
+
+
+## Install
+
+
+```
+$ npm install --save todomvc-app-css
+```
+
+
+## Getting started
+
+```html
+<link rel="stylesheet" href="node_modules/todomvc-app-css/index.css">
+```
+
+See the [TodoMVC app template](https://github.com/tastejs/todomvc-app-template).
+
+
+
+## License
+
+<a rel="license" href="http://creativecommons.org/licenses/by/4.0/deed.en_US"><img alt="Creative Commons License" style="border-width:0" src="http://i.creativecommons.org/l/by/4.0/80x15.png" /></a><br />This <span xmlns:dct="http://purl.org/dc/terms/" href="http://purl.org/dc/dcmitype/InteractiveResource" rel="dct:type">work</span> by <a xmlns:cc="http://creativecommons.org/ns#" href="http://sindresorhus.com" property="cc:attributionName" rel="cc:attributionURL">Sindre Sorhus</a> is licensed under a <a rel="license" href="http://creativecommons.org/licenses/by/4.0/deed.en_US">Creative Commons Attribution 4.0 International License</a>.
diff --git a/PerformanceTests/Speedometer/resources/todomvc/vanilla-examples/es2015/node_modules/todomvc-common/base.css b/PerformanceTests/Speedometer/resources/todomvc/vanilla-examples/es2015/node_modules/todomvc-common/base.css
new file mode 100644 (file)
index 0000000..4d25d3c
--- /dev/null
@@ -0,0 +1,141 @@
+hr {
+    margin: 20px 0;
+    border: 0;
+    border-top: 1px dashed #c5c5c5;
+    border-bottom: 1px dashed #f7f7f7;
+}
+
+.learn a {
+    font-weight: normal;
+    text-decoration: none;
+    color: #b83f45;
+}
+
+.learn a:hover {
+    text-decoration: underline;
+    color: #787e7e;
+}
+
+.learn h3,
+.learn h4,
+.learn h5 {
+    margin: 10px 0;
+    font-weight: 500;
+    line-height: 1.2;
+    color: #000;
+}
+
+.learn h3 {
+    font-size: 24px;
+}
+
+.learn h4 {
+    font-size: 18px;
+}
+
+.learn h5 {
+    margin-bottom: 0;
+    font-size: 14px;
+}
+
+.learn ul {
+    padding: 0;
+    margin: 0 0 30px 25px;
+}
+
+.learn li {
+    line-height: 20px;
+}
+
+.learn p {
+    font-size: 15px;
+    font-weight: 300;
+    line-height: 1.3;
+    margin-top: 0;
+    margin-bottom: 0;
+}
+
+#issue-count {
+    display: none;
+}
+
+.quote {
+    border: none;
+    margin: 20px 0 60px 0;
+}
+
+.quote p {
+    font-style: italic;
+}
+
+.quote p:before {
+    content: '“';
+    font-size: 50px;
+    opacity: .15;
+    position: absolute;
+    top: -20px;
+    left: 3px;
+}
+
+.quote p:after {
+    content: '”';
+    font-size: 50px;
+    opacity: .15;
+    position: absolute;
+    bottom: -42px;
+    right: 3px;
+}
+
+.quote footer {
+    position: absolute;
+    bottom: -40px;
+    right: 0;
+}
+
+.quote footer img {
+    border-radius: 3px;
+}
+
+.quote footer a {
+    margin-left: 5px;
+    vertical-align: middle;
+}
+
+.speech-bubble {
+    position: relative;
+    padding: 10px;
+    background: rgba(0, 0, 0, .04);
+    border-radius: 5px;
+}
+
+.speech-bubble:after {
+    content: '';
+    position: absolute;
+    top: 100%;
+    right: 30px;
+    border: 13px solid transparent;
+    border-top-color: rgba(0, 0, 0, .04);
+}
+
+.learn-bar > .learn {
+    position: absolute;
+    width: 272px;
+    top: 8px;
+    left: -300px;
+    padding: 10px;
+    border-radius: 5px;
+    background-color: rgba(255, 255, 255, .6);
+    transition-property: left;
+    transition-duration: 500ms;
+}
+
+@media (min-width: 899px) {
+    .learn-bar {
+        width: auto;
+        padding-left: 300px;
+    }
+
+    .learn-bar > .learn {
+        left: 8px;
+    }
+}
diff --git a/PerformanceTests/Speedometer/resources/todomvc/vanilla-examples/es2015/node_modules/todomvc-common/base.js b/PerformanceTests/Speedometer/resources/todomvc/vanilla-examples/es2015/node_modules/todomvc-common/base.js
new file mode 100644 (file)
index 0000000..81e006b
--- /dev/null
@@ -0,0 +1,249 @@
+/* global _ */
+(function () {
+    'use strict';
+
+    /* jshint ignore:start */
+    // Underscore's Template Module
+    // Courtesy of underscorejs.org
+    var _ = (function (_) {
+        _.defaults = function (object) {
+            if (!object) {
+                return object;
+            }
+            for (var argsIndex = 1, argsLength = arguments.length; argsIndex < argsLength; argsIndex++) {
+                var iterable = arguments[argsIndex];
+                if (iterable) {
+                    for (var key in iterable) {
+                        if (object[key] == null) {
+                            object[key] = iterable[key];
+                        }
+                    }
+                }
+            }
+            return object;
+        }
+
+        // By default, Underscore uses ERB-style template delimiters, change the
+        // following template settings to use alternative delimiters.
+        _.templateSettings = {
+            evaluate    : /<%([\s\S]+?)%>/g,
+            interpolate : /<%=([\s\S]+?)%>/g,
+            escape      : /<%-([\s\S]+?)%>/g
+        };
+
+        // When customizing `templateSettings`, if you don't want to define an
+        // interpolation, evaluation or escaping regex, we need one that is
+        // guaranteed not to match.
+        var noMatch = /(.)^/;
+
+        // Certain characters need to be escaped so that they can be put into a
+        // string literal.
+        var escapes = {
+            "'":      "'",
+            '\\':     '\\',
+            '\r':     'r',
+            '\n':     'n',
+            '\t':     't',
+            '\u2028': 'u2028',
+            '\u2029': 'u2029'
+        };
+
+        var escaper = /\\|'|\r|\n|\t|\u2028|\u2029/g;
+
+        // JavaScript micro-templating, similar to John Resig's implementation.
+        // Underscore templating handles arbitrary delimiters, preserves whitespace,
+        // and correctly escapes quotes within interpolated code.
+        _.template = function(text, data, settings) {
+            var render;
+            settings = _.defaults({}, settings, _.templateSettings);
+
+            // Combine delimiters into one regular expression via alternation.
+            var matcher = new RegExp([
+                (settings.escape || noMatch).source,
+                (settings.interpolate || noMatch).source,
+                (settings.evaluate || noMatch).source
+            ].join('|') + '|$', 'g');
+
+            // Compile the template source, escaping string literals appropriately.
+            var index = 0;
+            var source = "__p+='";
+            text.replace(matcher, function(match, escape, interpolate, evaluate, offset) {
+                source += text.slice(index, offset)
+                    .replace(escaper, function(match) { return '\\' + escapes[match]; });
+
+                if (escape) {
+                    source += "'+\n((__t=(" + escape + "))==null?'':_.escape(__t))+\n'";
+                }
+                if (interpolate) {
+                    source += "'+\n((__t=(" + interpolate + "))==null?'':__t)+\n'";
+                }
+                if (evaluate) {
+                    source += "';\n" + evaluate + "\n__p+='";
+                }
+                index = offset + match.length;
+                return match;
+            });
+            source += "';\n";
+
+            // If a variable is not specified, place data values in local scope.
+            if (!settings.variable) source = 'with(obj||{}){\n' + source + '}\n';
+
+            source = "var __t,__p='',__j=Array.prototype.join," +
+                "print=function(){__p+=__j.call(arguments,'');};\n" +
+                source + "return __p;\n";
+
+            try {
+                render = new Function(settings.variable || 'obj', '_', source);
+            } catch (e) {
+                e.source = source;
+                throw e;
+            }
+
+            if (data) return render(data, _);
+            var template = function(data) {
+                return render.call(this, data, _);
+            };
+
+            // Provide the compiled function source as a convenience for precompilation.
+            template.source = 'function(' + (settings.variable || 'obj') + '){\n' + source + '}';
+
+            return template;
+        };
+
+        return _;
+    })({});
+
+    if (location.hostname === 'todomvc.com') {
+        (function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
+        (i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
+        m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
+        })(window,document,'script','https://www.google-analytics.com/analytics.js','ga');
+        ga('create', 'UA-31081062-1', 'auto');
+        ga('send', 'pageview');
+    }
+    /* jshint ignore:end */
+
+    function redirect() {
+        if (location.hostname === 'tastejs.github.io') {
+            location.href = location.href.replace('tastejs.github.io/todomvc', 'todomvc.com');
+        }
+    }
+
+    function findRoot() {
+        var base = location.href.indexOf('examples/');
+        return location.href.substr(0, base);
+    }
+
+    function getFile(file, callback) {
+        if (!location.host) {
+            return console.info('Miss the info bar? Run TodoMVC from a server to avoid a cross-origin error.');
+        }
+
+        var xhr = new XMLHttpRequest();
+
+        xhr.open('GET', findRoot() + file, true);
+        xhr.send();
+
+        xhr.onload = function () {
+            if (xhr.status === 200 && callback) {
+                callback(xhr.responseText);
+            }
+        };
+    }
+
+    function Learn(learnJSON, config) {
+        if (!(this instanceof Learn)) {
+            return new Learn(learnJSON, config);
+        }
+
+        var template, framework;
+
+        if (typeof learnJSON !== 'object') {
+            try {
+                learnJSON = JSON.parse(learnJSON);
+            } catch (e) {
+                return;
+            }
+        }
+
+        if (config) {
+            template = config.template;
+            framework = config.framework;
+        }
+
+        if (!template && learnJSON.templates) {
+            template = learnJSON.templates.todomvc;
+        }
+
+        if (!framework && document.querySelector('[data-framework]')) {
+            framework = document.querySelector('[data-framework]').dataset.framework;
+        }
+
+        this.template = template;
+
+        if (learnJSON.backend) {
+            this.frameworkJSON = learnJSON.backend;
+            this.frameworkJSON.issueLabel = framework;
+            this.append({
+                backend: true
+            });
+        } else if (learnJSON[framework]) {
+            this.frameworkJSON = learnJSON[framework];
+            this.frameworkJSON.issueLabel = framework;
+            this.append();
+        }
+
+        this.fetchIssueCount();
+    }
+
+    Learn.prototype.append = function (opts) {
+        var aside = document.createElement('aside');
+        aside.innerHTML = _.template(this.template, this.frameworkJSON);
+        aside.className = 'learn';
+
+        if (opts && opts.backend) {
+            // Remove demo link
+            var sourceLinks = aside.querySelector('.source-links');
+            var heading = sourceLinks.firstElementChild;
+            var sourceLink = sourceLinks.lastElementChild;
+            // Correct link path
+            var href = sourceLink.getAttribute('href');
+            sourceLink.setAttribute('href', href.substr(href.lastIndexOf('http')));
+            sourceLinks.innerHTML = heading.outerHTML + sourceLink.outerHTML;
+        } else {
+            // Localize demo links
+            var demoLinks = aside.querySelectorAll('.demo-link');
+            Array.prototype.forEach.call(demoLinks, function (demoLink) {
+                if (demoLink.getAttribute('href').substr(0, 4) !== 'http') {
+                    demoLink.setAttribute('href', findRoot() + demoLink.getAttribute('href'));
+                }
+            });
+        }
+
+        document.body.className = (document.body.className + ' learn-bar').trim();
+        document.body.insertAdjacentHTML('afterBegin', aside.outerHTML);
+    };
+
+    Learn.prototype.fetchIssueCount = function () {
+        var issueLink = document.getElementById('issue-count-link');
+        if (issueLink) {
+            var url = issueLink.href.replace('https://github.com', 'https://api.github.com/repos');
+            var xhr = new XMLHttpRequest();
+            xhr.open('GET', url, true);
+            xhr.onload = function (e) {
+                var parsedResponse = JSON.parse(e.target.responseText);
+                if (parsedResponse instanceof Array) {
+                    var count = parsedResponse.length;
+                    if (count !== 0) {
+                        issueLink.innerHTML = 'This app has ' + count + ' open issues';
+                        document.getElementById('issue-count').style.display = 'inline';
+                    }
+                }
+            };
+            xhr.send();
+        }
+    };
+
+    redirect();
+    getFile('learn.json', Learn);
+})();
diff --git a/PerformanceTests/Speedometer/resources/todomvc/vanilla-examples/es2015/node_modules/todomvc-common/package.json b/PerformanceTests/Speedometer/resources/todomvc/vanilla-examples/es2015/node_modules/todomvc-common/package.json
new file mode 100755 (executable)
index 0000000..8056c4b
--- /dev/null
@@ -0,0 +1,64 @@
+{
+  "name": "todomvc-common",
+  "version": "1.0.2",
+  "description": "Common TodoMVC utilities used by our apps",
+  "license": "MIT",
+  "repository": {
+    "type": "git",
+    "url": "git+https://github.com/tastejs/todomvc-common.git"
+  },
+  "author": {
+    "name": "TasteJS"
+  },
+  "main": "base.js",
+  "files": [
+    "base.js",
+    "base.css"
+  ],
+  "keywords": [
+    "todomvc",
+    "tastejs",
+    "util",
+    "utilities"
+  ],
+  "gitHead": "e82d0c79e01687ce7407df786cc784ad82166cb3",
+  "bugs": {
+    "url": "https://github.com/tastejs/todomvc-common/issues"
+  },
+  "homepage": "https://github.com/tastejs/todomvc-common",
+  "_id": "todomvc-common@1.0.2",
+  "scripts": {},
+  "_shasum": "eb3ab61281ac74809f5869c917c7b08bc84234e0",
+  "_from": "todomvc-common@>=1.0.2 <2.0.0",
+  "_npmVersion": "2.7.4",
+  "_nodeVersion": "0.12.2",
+  "_npmUser": {
+    "name": "sindresorhus",
+    "email": "sindresorhus@gmail.com"
+  },
+  "dist": {
+    "shasum": "eb3ab61281ac74809f5869c917c7b08bc84234e0",
+    "tarball": "http://registry.npmjs.org/todomvc-common/-/todomvc-common-1.0.2.tgz"
+  },
+  "maintainers": [
+    {
+      "name": "sindresorhus",
+      "email": "sindresorhus@gmail.com"
+    },
+    {
+      "name": "addyosmani",
+      "email": "addyosmani@gmail.com"
+    },
+    {
+      "name": "passy",
+      "email": "phartig@rdrei.net"
+    },
+    {
+      "name": "stephenplusplus",
+      "email": "sawchuk@gmail.com"
+    }
+  ],
+  "directories": {},
+  "_resolved": "https://registry.npmjs.org/todomvc-common/-/todomvc-common-1.0.2.tgz",
+  "readme": "ERROR: No README data found!"
+}
diff --git a/PerformanceTests/Speedometer/resources/todomvc/vanilla-examples/es2015/node_modules/todomvc-common/readme.md b/PerformanceTests/Speedometer/resources/todomvc/vanilla-examples/es2015/node_modules/todomvc-common/readme.md
new file mode 100755 (executable)
index 0000000..7a5de51
--- /dev/null
@@ -0,0 +1,15 @@
+# todomvc-common
+
+> Common TodoMVC utilities used by our apps
+
+
+## Install
+
+```
+$ npm install --save todomvc-common
+```
+
+
+## License
+
+MIT © [TasteJS](http://tastejs.com)
diff --git a/PerformanceTests/Speedometer/resources/todomvc/vanilla-examples/es2015/package.json b/PerformanceTests/Speedometer/resources/todomvc/vanilla-examples/es2015/package.json
new file mode 100755 (executable)
index 0000000..a26bc92
--- /dev/null
@@ -0,0 +1,7 @@
+{
+  "private": true,
+  "dependencies": {
+    "todomvc-app-css": "^2.0.1",
+    "todomvc-common": "^1.0.2"
+  }
+}
diff --git a/PerformanceTests/Speedometer/resources/todomvc/vanilla-examples/es2015/src/.jshintrc b/PerformanceTests/Speedometer/resources/todomvc/vanilla-examples/es2015/src/.jshintrc
new file mode 100755 (executable)
index 0000000..ffd48eb
--- /dev/null
@@ -0,0 +1,3 @@
+{
+       "esnext": true
+}
diff --git a/PerformanceTests/Speedometer/resources/todomvc/vanilla-examples/es2015/src/app.js b/PerformanceTests/Speedometer/resources/todomvc/vanilla-examples/es2015/src/app.js
new file mode 100644 (file)
index 0000000..e65d9c5
--- /dev/null
@@ -0,0 +1,27 @@
+'use strict';
+
+let todo;
+const setView = () => todo.controller.setView(document.location.hash);
+
+class Todo {
+    /**
+     * Init new Todo List
+     * @param  {string} The name of your list
+     */
+    constructor(name) {
+        this.storage = new Store(name);
+        this.model = new Model(this.storage);
+
+        this.template = new Template();
+        this.view = new View(this.template);
+
+        this.controller = new Controller(this.model, this.view);
+    }
+}
+
+$on(window, 'load', () => {
+    todo = new Todo('todos-vanillajs');
+    setView();
+});
+
+$on(window, 'hashchange', setView);
diff --git a/PerformanceTests/Speedometer/resources/todomvc/vanilla-examples/es2015/src/controller.js b/PerformanceTests/Speedometer/resources/todomvc/vanilla-examples/es2015/src/controller.js
new file mode 100644 (file)
index 0000000..d2693ec
--- /dev/null
@@ -0,0 +1,214 @@
+'use strict';
+
+class Controller {
+    /**
+     * Take a model & view, then act as controller between them
+     * @param  {object} model The model instance
+     * @param  {object} view  The view instance
+     */
+    constructor(model, view) {
+        this.model = model;
+        this.view = view;
+
+        this.view.bind('newTodo', title => this.addItem(title));
+        this.view.bind('itemEdit', item => this.editItem(item.id));
+        this.view.bind('itemEditDone', item => this.editItemSave(item.id, item.title));
+        this.view.bind('itemEditCancel', item => this.editItemCancel(item.id));
+        this.view.bind('itemRemove', item => this.removeItem(item.id));
+        this.view.bind('itemToggle', item => this.toggleComplete(item.id, item.completed));
+        this.view.bind('removeCompleted', () => this.removeCompletedItems());
+        this.view.bind('toggleAll', status => this.toggleAll(status.completed));
+    }
+
+    /**
+     * Load & Initialize the view
+     * @param {string}  '' | 'active' | 'completed'
+     */
+    setView(hash){
+        let route = hash.split('/')[1];
+        let page = route || '';
+        this._updateFilter(page);
+    }
+
+    /**
+     * Event fires on load. Gets all items & displays them
+     */
+    showAll(){
+        this.model.read(data => this.view.render('showEntries', data));
+    }
+
+    /**
+     * Renders all active tasks
+     */
+    showActive(){
+        this.model.read({completed: false}, data => this.view.render('showEntries', data));
+    }
+
+    /**
+     * Renders all completed tasks
+     */
+    showCompleted(){
+        this.model.read({completed: true}, data => this.view.render('showEntries', data));
+    }
+
+    /**
+     * An event to fire whenever you want to add an item. Simply pass in the event
+     * object and it'll handle the DOM insertion and saving of the new item.
+     */
+    addItem(title){
+        if (title.trim() === '') {
+            return;
+        }
+
+        this.model.create(title, () => {
+            this.view.render('clearNewTodo');
+            this._filter(true);
+        });
+    }
+
+    /*
+     * Triggers the item editing mode.
+     */
+    editItem(id){
+        this.model.read(id, data => {
+            let title = data[0].title;
+            this.view.render('editItem', {id, title});
+        });
+    }
+
+    /*
+     * Finishes the item editing mode successfully.
+     */
+    editItemSave(id, title){
+        title = title.trim();
+
+        if (title.length !== 0) {
+            this.model.update(id, {title}, () => {
+                this.view.render('editItemDone', {id, title});
+            });
+        } else {
+            this.removeItem(id);
+        }
+    }
+
+    /*
+     * Cancels the item editing mode.
+     */
+    editItemCancel(id){
+        this.model.read(id, data => {
+            let title = data[0].title;
+            this.view.render('editItemDone', {id, title});
+        });
+    }
+
+    /**
+     * Find the DOM element with given ID,
+     * Then remove it from DOM & Storage
+     */
+    removeItem(id){
+        this.model.remove(id, () => this.view.render('removeItem', id));
+        this._filter();
+    }
+
+    /**
+     * Will remove all completed items from the DOM and storage.
+     */
+    removeCompletedItems(){
+        this.model.read({completed: true}, data => {
+            for (let item of data) {
+                this.removeItem(item.id);
+            }
+        });
+
+        this._filter();
+    }
+
+    /**
+     * Give it an ID of a model and a checkbox and it will update the item
+     * in storage based on the checkbox's state.
+     *
+     * @param {number} id The ID of the element to complete or uncomplete
+     * @param {object} checkbox The checkbox to check the state of complete
+     *                          or not
+     * @param {boolean|undefined} silent Prevent re-filtering the todo items
+     */
+    toggleComplete(id, completed, silent){
+        this.model.update(id, {completed}, () => {
+            this.view.render('elementComplete', {id, completed});
+        });
+
+        if (!silent) {
+            this._filter();
+        }
+    }
+
+    /**
+     * Will toggle ALL checkboxes' on/off state and completeness of models.
+     * Just pass in the event object.
+     */
+    toggleAll(completed){
+        this.model.read({completed: !completed}, data => {
+            for (let item of data) {
+                this.toggleComplete(item.id, completed, true);
+            }
+        });
+
+        this._filter();
+    }
+
+    /**
+     * Updates the pieces of the page which change depending on the remaining
+     * number of todos.
+     */
+    _updateCount(){
+        this.model.getCount(todos => {
+            const completed = todos.completed;
+            const visible = completed > 0;
+            const checked = completed === todos.total;
+
+            this.view.render('updateElementCount', todos.active);
+            this.view.render('clearCompletedButton', {completed, visible});
+
+            this.view.render('toggleAll', {checked});
+            this.view.render('contentBlockVisibility', {visible: todos.total > 0});
+        });
+    }
+
+    /**
+     * Re-filters the todo items, based on the active route.
+     * @param {boolean|undefined} force  forces a re-painting of todo items.
+     */
+    _filter(force){
+        let active = this._activeRoute;
+        const activeRoute = active.charAt(0).toUpperCase() + active.substr(1);
+
+        // Update the elements on the page, which change with each completed todo
+        this._updateCount();
+
+        // If the last active route isn't "All", or we're switching routes, we
+        // re-create the todo item elements, calling:
+        //   this.show[All|Active|Completed]()
+        if (force || this._lastActiveRoute !== 'All' || this._lastActiveRoute !== activeRoute) {
+            this['show' + activeRoute]();
+        }
+
+        this._lastActiveRoute = activeRoute;
+    }
+
+    /**
+     * Simply updates the filter nav's selected states
+     */
+    _updateFilter(currentPage){
+        // Store a reference to the active route, allowing us to re-filter todo
+        // items as they are marked complete or incomplete.
+        this._activeRoute = currentPage;
+
+        if (currentPage === '') {
+            this._activeRoute = 'All';
+        }
+
+        this._filter();
+
+        this.view.render('setFilter', currentPage);
+    }
+}
diff --git a/PerformanceTests/Speedometer/resources/todomvc/vanilla-examples/es2015/src/helpers.js b/PerformanceTests/Speedometer/resources/todomvc/vanilla-examples/es2015/src/helpers.js
new file mode 100644 (file)
index 0000000..154fc48
--- /dev/null
@@ -0,0 +1,53 @@
+'use strict';
+
+
+// Allow for looping on nodes by chaining:
+// qsa('.foo').forEach(function () {})
+NodeList.prototype.forEach = Array.prototype.forEach;
+
+// Get element(s) by CSS selector:
+function qs(selector, scope) {
+    return (scope || document).querySelector(selector);
+}
+
+function qsa(selector, scope) {
+    return (scope || document).querySelectorAll(selector);
+}
+
+// addEventListener wrapper:
+function $on(target, type, callback, useCapture) {
+    target.addEventListener(type, callback, !!useCapture);
+}
+
+// Attach a handler to event for all elements that match the selector,
+// now or in the future, based on a root element
+function $delegate(target, selector, type, handler) {
+    let dispatchEvent = event => {
+        const targetElement = event.target;
+        const potentialElements = qsa(selector, target);
+        const hasMatch = Array.prototype.indexOf.call(potentialElements, targetElement) >= 0;
+
+        if (hasMatch) {
+            handler.call(targetElement, event);
+        }
+    };
+
+    // https://developer.mozilla.org/en-US/docs/Web/Events/blur
+    const useCapture = type === 'blur' || type === 'focus';
+
+    $on(target, type, dispatchEvent, useCapture);
+}
+
+// Find the element's parent with the given tag name:
+// $parent(qs('a'), 'div')
+function $parent(element, tagName) {
+    if (!element.parentNode) {
+        return;
+    }
+
+    if (element.parentNode.tagName.toLowerCase() === tagName.toLowerCase()) {
+        return element.parentNode;
+    }
+
+    return $parent(element.parentNode, tagName);
+}
diff --git a/PerformanceTests/Speedometer/resources/todomvc/vanilla-examples/es2015/src/model.js b/PerformanceTests/Speedometer/resources/todomvc/vanilla-examples/es2015/src/model.js
new file mode 100644 (file)
index 0000000..cf8d581
--- /dev/null
@@ -0,0 +1,117 @@
+'use strict';
+
+
+/**
+ * Creates a new Model instance and hooks up the storage.
+ * @constructor
+ * @param {object} storage A reference to the client side storage class
+ */
+class Model {
+    constructor(storage) {
+        this.storage = storage;
+    }
+
+    /**
+     * Creates a new todo model
+     *
+     * @param {string} [title] The title of the task
+     * @param {function} [callback] The callback to fire after the model is created
+     */
+    create(title, callback){
+        title = title || '';
+
+        let newItem = {
+            title: title.trim(),
+            completed: false
+        };
+
+        this.storage.save(newItem, callback);
+    }
+
+    /**
+     * Finds and returns a model in storage. If no query is given it'll simply
+     * return everything. If you pass in a string or number it'll look that up as
+     * the ID of the model to find. Lastly, you can pass it an object to match
+     * against.
+     *
+     * @param {string|number|object} [query] A query to match models against
+     * @param {function} [callback] The callback to fire after the model is found
+     *
+     * @example
+     * model.read(1, func) // Will find the model with an ID of 1
+     * model.read('1') // Same as above
+     * //Below will find a model with foo equalling bar and hello equalling world.
+     * model.read({ foo: 'bar', hello: 'world' })
+     */
+    read(query, callback){
+        const queryType = typeof query;
+
+        if (queryType === 'function') {
+            callback = query;
+            this.storage.findAll(callback);
+        } else if (queryType === 'string' || queryType === 'number') {
+            query = parseInt(query, 10);
+            this.storage.find({id: query}, callback);
+        } else {
+            this.storage.find(query, callback);
+        }
+    }
+
+    /**
+     * Updates a model by giving it an ID, data to update, and a callback to fire when
+     * the update is complete.
+     *
+     * @param {number} id The id of the model to update
+     * @param {object} data The properties to update and their new value
+     * @param {function} callback The callback to fire when the update is complete.
+     */
+    update(id, data, callback){
+        this.storage.save(data, callback, id);
+    }
+
+    /**
+     * Removes a model from storage
+     *
+     * @param {number} id The ID of the model to remove
+     * @param {function} callback The callback to fire when the removal is complete.
+     */
+    remove(id, callback){
+        this.storage.remove(id, callback);
+    }
+
+    /**
+     * WARNING: Will remove ALL data from storage.
+     *
+     * @param {function} callback The callback to fire when the storage is wiped.
+     */
+    removeAll(callback){
+        this.storage.drop(callback);
+    }
+
+    /**
+     * Returns a count of all todos
+     */
+    getCount(callback){
+        let todos = {
+            active: 0,
+            completed: 0,
+            total: 0
+        };
+
+        this.storage.findAll(data => {
+            for (let todo of data) {
+                if (todo.completed) {
+                    todos.completed++;
+                } else {
+                    todos.active++;
+                }
+
+                todos.total++;
+            }
+
+            if (callback) {
+                callback(todos);
+            }
+        });
+    }
+}
diff --git a/PerformanceTests/Speedometer/resources/todomvc/vanilla-examples/es2015/src/store.js b/PerformanceTests/Speedometer/resources/todomvc/vanilla-examples/es2015/src/store.js
new file mode 100644 (file)
index 0000000..6ba97bd
--- /dev/null
@@ -0,0 +1,150 @@
+/*jshint eqeqeq:false */
+'use strict';
+
+/**
+ * Creates a new client side storage object and will create an empty
+ * collection if no collection already exists.
+ *
+ * @param {string} name The name of our DB we want to use
+ * @param {function} callback Our fake DB uses callbacks because in
+ * real life you probably would be making AJAX calls
+ */
+class Store {
+    constructor(name, callback) {
+        this._dbName = name;
+        this.memoryStorage = this.memoryStorage || {};
+
+        if (!this.memoryStorage[name]) {
+            let data = {
+                todos: []
+            };
+
+            this.memoryStorage[name] = JSON.stringify(data);
+        }
+
+        if (callback) {
+            callback.call(this, JSON.parse(this.memoryStorage[name]));
+        }
+    }
+
+    /**
+     * Finds items based on a query given as a JS object
+     *
+     * @param {object} query The query to match against (i.e. {foo: 'bar'})
+     * @param {function} callback   The callback to fire when the query has
+     * completed running
+     *
+     * @example
+     * db.find({foo: 'bar', hello: 'world'}, function (data) {
+         *   // data will return any items that have foo: bar and
+         *   // hello: world in their properties
+         * })
+     */
+    find(query, callback){
+        if (!callback) {
+            return;
+        }
+
+        let todos = JSON.parse(this.memoryStorage[this._dbName]).todos;
+
+        callback.call(this, todos.filter(todo => {
+            for (let q in query) {
+                if (query[q] !== todo[q]) {
+                    return false;
+                }
+            }
+            return true;
+        }));
+    }
+
+    /**
+     * Will retrieve all data from the collection
+     *
+     * @param {function} callback The callback to fire upon retrieving data
+     */
+    findAll(callback){
+        if (callback) {
+            callback.call(this, JSON.parse(this.memoryStorage[this._dbName]).todos);
+        }
+    }
+
+    /**
+     * Will save the given data to the DB. If no item exists it will create a new
+     * item, otherwise it'll simply update an existing item's properties
+     *
+     * @param {object} updateData The data to save back into the DB
+     * @param {function} callback The callback to fire after saving
+     * @param {number} id An optional param to enter an ID of an item to update
+     */
+    save(updateData, callback, id){
+        const data = JSON.parse(this.memoryStorage[this._dbName]);
+        let todos = data.todos;
+        const len = todos.length;
+
+        // If an ID was actually given, find the item and update each property
+        if (id) {
+            for (let i = 0; i < len; i++) {
+                if (todos[i].id === id) {
+                    for (let key in updateData) {
+                        todos[i][key] = updateData[key];
+                    }
+                    break;
+                }
+            }
+
+            this.memoryStorage[this._dbName] = JSON.stringify(data);
+
+            if (callback) {
+                callback.call(this, JSON.parse(this.memoryStorage[this._dbName]).todos);
+            }
+        } else {
+            // Generate an ID
+            updateData.id = new Date().getTime();
+
+            todos.push(updateData);
+            this.memoryStorage[this._dbName] = JSON.stringify(data);
+
+            if (callback) {
+                callback.call(this, [updateData]);
+            }
+        }
+    }
+
+    /**
+     * Will remove an item from the Store based on its ID
+     *
+     * @param {number} id The ID of the item you want to remove
+     * @param {function} callback The callback to fire after saving
+     */
+    remove(id, callback){
+        const data = JSON.parse(this.memoryStorage[this._dbName]);
+        let todos = data.todos;
+        const len = todos.length;
+
+        for (let i = 0; i < todos.length; i++) {
+            if (todos[i].id == id) {
+                todos.splice(i, 1);
+                break;
+            }
+        }
+
+        this.memoryStorage[this._dbName] = JSON.stringify(data);
+
+        if (callback) {
+            callback.call(this, JSON.parse(this.memoryStorage[this._dbName]).todos);
+        }
+    }
+
+    /**
+     * Will drop all storage and start fresh
+     *
+     * @param {function} callback The callback to fire after dropping the data
+     */
+    drop(callback){
+        this.memoryStorage[this._dbName] = JSON.stringify({todos: []});
+
+        if (callback) {
+            callback.call(this, JSON.parse(this.memoryStorage[this._dbName]).todos);
+        }
+    }
+}
diff --git a/PerformanceTests/Speedometer/resources/todomvc/vanilla-examples/es2015/src/template.js b/PerformanceTests/Speedometer/resources/todomvc/vanilla-examples/es2015/src/template.js
new file mode 100644 (file)
index 0000000..77d4dd4
--- /dev/null
@@ -0,0 +1,95 @@
+'use strict';
+
+
+const htmlEscapes = {
+    '&': '&amp',
+    '<': '&lt',
+    '>': '&gt',
+    '"': '&quot',
+    '\'': '&#x27',
+    '`': '&#x60'
+};
+
+const reUnescapedHtml = /[&<>"'`]/g;
+const reHasUnescapedHtml = new RegExp(reUnescapedHtml.source);
+
+let escape = str => (str && reHasUnescapedHtml.test(str)) ? str.replace(reUnescapedHtml, escapeHtmlChar) : str;
+let escapeHtmlChar = chr => htmlEscapes[chr];
+
+class Template {
+    constructor() {
+        this.defaultTemplate = `
+            <li data-id="{{id}}" class="{{completed}}">
+                <div class="view">
+                    <input class="toggle" type="checkbox" {{checked}}>
+                    <label>{{title}}</label>
+                    <button class="destroy"></button>
+                </div>
+            </li>
+        `;
+    }
+
+    /**
+     * Creates an <li> HTML string and returns it for placement in your app.
+     *
+     * NOTE: In real life you should be using a templating engine such as Mustache
+     * or Handlebars, however, this is a vanilla JS example.
+     *
+     * @param {object} data The object containing keys you want to find in the
+     *                      template to replace.
+     * @returns {string} HTML String of an <li> element
+     *
+     * @example
+     * view.show({
+         *  id: 1,
+         *  title: "Hello World",
+         *  completed: 0,
+         * })
+     */
+    show(data){
+        let i = 0;
+        let view = '';
+        const len = data.length;
+
+        for (i; i < len; i++) {
+            let completed = '';
+            let checked = '';
+            let template = this.defaultTemplate;
+
+            if (data[i].completed) {
+                completed = 'completed';
+                checked = 'checked';
+            }
+
+            template = template.replace('{{id}}', data[i].id);
+            template = template.replace('{{title}}', escape(data[i].title));
+            template = template.replace('{{completed}}', completed);
+            template = template.replace('{{checked}}', checked);
+
+            view += template;
+        }
+
+        return view;
+    }
+
+    /**
+     * Displays a counter of how many to dos are left to complete
+     *
+     * @param {number} activeTodos The number of active todos.
+     * @returns {string} String containing the count
+     */
+    itemCounter(activeTodos){
+        let plural = activeTodos === 1 ? '' : 's';
+        return `<strong>${activeTodos}</strong> item${plural} left`;
+    }
+
+    /**
+     * Updates the text within the "Clear completed" button
+     *
+     * @param  {[type]} completedTodos The number of completed todos.
+     * @returns {string} String containing the count
+     */
+    clearCompletedButton(completedTodos){
+        return (completedTodos > 0) ? 'Clear completed' : '';
+    }
+}
diff --git a/PerformanceTests/Speedometer/resources/todomvc/vanilla-examples/es2015/src/view.js b/PerformanceTests/Speedometer/resources/todomvc/vanilla-examples/es2015/src/view.js
new file mode 100644 (file)
index 0000000..4ba35d2
--- /dev/null
@@ -0,0 +1,178 @@
+'use strict';
+
+
+// import {qs, qsa, $on, $parent, $delegate} from './helpers';
+
+let _itemId = element => parseInt($parent(element, 'li').dataset.id, 10);
+
+let _setFilter = currentPage => {
+    qs('.filters .selected').className = '';
+    qs(`.filters [href="#/${currentPage}"]`).className = 'selected';
+};
+
+let _elementComplete = (id, completed) => {
+    let listItem = qs(`[data-id="${id}"]`);
+
+    if (!listItem) {
+        return;
+    }
+
+    listItem.className = completed ? 'completed' : '';
+
+    // In case it was toggled from an event and not by clicking the checkbox
+    qs('input', listItem).checked = completed;
+};
+
+let _editItem = (id, title) => {
+    let listItem = qs(`[data-id="${id}"]`);
+
+    if (!listItem) {
+        return;
+    }
+
+    listItem.className += ' editing';
+
+    let input = document.createElement('input');
+    input.className = 'edit';
+
+    listItem.appendChild(input);
+    input.focus();
+    input.value = title;
+};
+
+/**
+ * View that abstracts away the browser's DOM completely.
+ * It has two simple entry points:
+ *
+ *   - bind(eventName, handler)
+ *     Takes a todo application event and registers the handler
+ *   - render(command, parameterObject)
+ *     Renders the given command with the options
+ */
+class View {
+    constructor(template) {
+        this.template = template;
+
+        this.ENTER_KEY = 13;
+        this.ESCAPE_KEY = 27;
+
+        this.$todoList = qs('.todo-list');
+        this.$todoItemCounter = qs('.todo-count');
+        this.$clearCompleted = qs('.clear-completed');
+        this.$main = qs('.main');
+        this.$footer = qs('.footer');
+        this.$toggleAll = qs('.toggle-all');
+        this.$newTodo = qs('.new-todo');
+
+        this.viewCommands = {
+            showEntries: parameter => this.$todoList.innerHTML = this.template.show(parameter),
+            removeItem: parameter => this._removeItem(parameter),
+            updateElementCount: parameter => this.$todoItemCounter.innerHTML = this.template.itemCounter(parameter),
+            clearCompletedButton: parameter => this._clearCompletedButton(parameter.completed, parameter.visible),
+            contentBlockVisibility: parameter => this.$main.style.display = this.$footer.style.display = parameter.visible ? 'block' : 'none',
+            toggleAll: parameter => this.$toggleAll.checked = parameter.checked,
+            setFilter: parameter => _setFilter(parameter),
+            clearNewTodo: parameter => this.$newTodo.value = '',
+            elementComplete: parameter => _elementComplete(parameter.id, parameter.completed),
+            editItem: parameter => _editItem(parameter.id, parameter.title),
+            editItemDone: parameter => this._editItemDone(parameter.id, parameter.title),
+        };
+    }
+
+    _removeItem(id) {
+        let elem = qs(`[data-id="${id}"]`);
+
+        if (elem) {
+            this.$todoList.removeChild(elem);
+        }
+    }
+
+    _clearCompletedButton(completedCount, visible) {
+        this.$clearCompleted.innerHTML = this.template.clearCompletedButton(completedCount);
+        this.$clearCompleted.style.display = visible ? 'block' : 'none';
+    }
+
+    _editItemDone(id, title) {
+        let listItem = qs(`[data-id="${id}"]`);
+
+        if (!listItem) {
+            return;
+        }
+
+        let input = qs('input.edit', listItem);
+        listItem.removeChild(input);
+
+        listItem.className = listItem.className.replace(' editing', '');
+
+        qsa('label', listItem).forEach(label => label.textContent = title);
+    }
+
+    render(viewCmd, parameter) {
+        this.viewCommands[viewCmd](parameter);
+    }
+
+    _bindItemEditDone(handler) {
+        let self = this;
+
+        $delegate(self.$todoList, 'li .edit', 'blur', function () {
+            if (!this.dataset.iscanceled) {
+                handler({
+                    id: _itemId(this),
+                    title: this.value
+                });
+            }
+        });
+
+        // Remove the cursor from the input when you hit enter just like if it were a real form
+        $delegate(self.$todoList, 'li .edit', 'keypress', function (event) {
+            if (event.keyCode === self.ENTER_KEY) {
+                this.blur();
+            }
+        });
+    }
+
+    _bindItemEditCancel(handler) {
+        let self = this;
+
+        $delegate(self.$todoList, 'li .edit', 'keyup', function (event) {
+            if (event.keyCode === self.ESCAPE_KEY) {
+                let id = _itemId(this);
+                this.dataset.iscanceled = true;
+                this.blur();
+
+                handler({ id });
+            }
+        });
+    }
+
+    bind(event, handler) {
+        if (event === 'newTodo') {
+            $on(this.$newTodo, 'change', () => handler(this.$newTodo.value));
+        } else if (event === 'removeCompleted') {
+            $on(this.$clearCompleted, 'click', handler);
+        } else if (event === 'toggleAll') {
+            $on(this.$toggleAll, 'click', function(){
+                handler({completed: this.checked});
+            });
+        } else if (event === 'itemEdit') {
+            $delegate(this.$todoList, 'li label', 'dblclick', function(){
+                handler({id: _itemId(this)});
+            });
+        } else if (event === 'itemRemove') {
+            $delegate(this.$todoList, '.destroy', 'click', function(){
+                handler({id: _itemId(this)});
+            });
+        } else if (event === 'itemToggle') {
+            $delegate(this.$todoList, '.toggle', 'click', function(){
+                handler({
+                    id: _itemId(this),
+                    completed: this.checked
+                });
+            });
+        } else if (event === 'itemEditDone') {
+            this._bindItemEditDone(handler);
+        } else if (event === 'itemEditCancel') {
+            this._bindItemEditCancel(handler);
+        }
+    }
+}