Compare commits
313 commits
1.0
...
main-custo
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5936c53eda | ||
|
|
f227c26c11 | ||
|
|
a4d98e20f9 | ||
|
|
68d8f57ddf | ||
|
|
a3b0e71d97 | ||
|
|
7d7504f9bd | ||
|
|
0c3f0fb132 | ||
|
|
9f85f8e3bd | ||
|
|
2ff56c9d76 | ||
|
|
d979723ef6 | ||
|
|
d6b5ffea95 | ||
|
|
4dfa8faeea | ||
|
|
faf9fff140 | ||
|
|
a169b1d56a | ||
|
|
d87b307974 | ||
|
|
f7c3a7014a | ||
|
|
0dc0b1c50e | ||
|
|
e2e3c496f5 | ||
|
|
67bc21f1f4 | ||
|
|
e70778f1a8 | ||
|
|
a64fa26876 | ||
|
|
e2afcb6e4e | ||
|
|
66b5324d02 | ||
|
|
94f0d81686 | ||
|
|
4f87c768c4 | ||
|
|
1552769e5c | ||
|
|
04cfe65d7d | ||
|
|
617c7310fa | ||
|
|
4719e3f8bd | ||
|
|
3ce6105a3a | ||
|
|
2387b9fe89 | ||
|
|
bb4a4ad1c1 | ||
|
|
2a4d041656 | ||
|
|
c2e8d4f6bc | ||
|
|
400a5b87a0 | ||
|
|
0dcab0317e | ||
|
|
ea14d27e03 | ||
|
|
3150f41173 | ||
|
|
5a19851e54 | ||
|
|
a999ad1024 | ||
|
|
e6e8a4f969 | ||
|
|
33dec4f328 | ||
|
|
3ae822bcff | ||
|
|
13cc928140 | ||
|
|
cbf8311a76 | ||
|
|
bd9ab77462 | ||
|
|
8d67f28d02 | ||
|
|
3d2a28e2dc | ||
|
|
406042a18d | ||
|
|
7efb0ac57b | ||
|
|
309437377e | ||
|
|
88c5323a49 | ||
|
|
58e2e4e92b | ||
|
|
7dc43d27a5 | ||
|
|
ad061acd81 | ||
|
|
5654961621 | ||
|
|
377c6c7610 | ||
|
|
3ca4aed3a8 | ||
|
|
c901052f2d | ||
|
|
899c11317d | ||
|
|
82e0a46283 | ||
|
|
f6f67e0237 | ||
|
|
0c3af704a5 | ||
|
|
ca9d23c18f | ||
|
|
3d782244a5 | ||
|
|
2640affe64 | ||
|
|
0fd1528419 | ||
|
|
9837ccc768 | ||
|
|
7b1b81ee13 | ||
|
|
285e32dacb | ||
|
|
31ba222f5b | ||
|
|
7529eedcc5 | ||
|
|
638a9681cf | ||
|
|
b0a465d60c | ||
|
|
3d93f179b6 | ||
|
|
7c0931ee8c | ||
|
|
7ae836e55f | ||
|
|
c0269db7d7 | ||
|
|
6ad43c3f5c | ||
|
|
0f449aa702 | ||
|
|
4b451d8388 | ||
|
|
64a5e7c0ba | ||
|
|
2aa819faed | ||
|
|
91aaaacb42 | ||
|
|
4173e4d8a9 | ||
|
|
a268ec4391 | ||
|
|
2414364b18 | ||
|
|
208e1abee2 | ||
|
|
2ee210ca20 | ||
|
|
abea7b89b9 | ||
|
|
c19468aa56 | ||
|
|
32c7c23a7d | ||
|
|
b77b3dbf2b | ||
|
|
44c809305d | ||
|
|
4316b5fb14 | ||
|
|
81c835f6b7 | ||
|
|
8fd080fb67 | ||
|
|
b10452595e | ||
|
|
0672c38f23 | ||
|
|
790eed91c9 | ||
|
|
679cb52fdf | ||
|
|
5748e44342 | ||
|
|
c306d4237d | ||
|
|
095abdfc44 | ||
|
|
a03f1367ea | ||
|
|
f6c5098d6e | ||
|
|
f841f81faa | ||
|
|
a608e98396 | ||
|
|
fb9b966b37 | ||
|
|
b3ec8c748b | ||
|
|
d86dad330f | ||
|
|
5107c739c1 | ||
|
|
b6abd32982 | ||
|
|
0fef685dcb | ||
|
|
26531fc7f1 | ||
|
|
64798bc754 | ||
|
|
5491573d0e | ||
|
|
8f6982e71a | ||
|
|
cba2b0d2e7 | ||
|
|
36712deb57 | ||
|
|
f02d7e4b64 | ||
|
|
61ad820a5e | ||
|
|
d834dcbb07 | ||
|
|
a9c96c3bd4 | ||
|
|
e11801e3b5 | ||
|
|
e2a033d54b | ||
|
|
91f54a9c6d | ||
|
|
95af4c4956 | ||
|
|
e8624b1096 | ||
|
|
f6793b0bd5 | ||
|
|
815817b5e0 | ||
|
|
b44d98255f | ||
|
|
151d73d535 | ||
|
|
8046654389 | ||
|
|
9025eb1fdc | ||
|
|
ad36a2c483 | ||
|
|
9e692e4074 | ||
|
|
68c56ff662 | ||
|
|
1c4d9c7d10 | ||
|
|
0f6443e065 | ||
|
|
8491149024 | ||
|
|
1f48e366d0 | ||
|
|
bd4dc60b24 | ||
|
|
27c0b2cddf | ||
|
|
2281e13426 | ||
|
|
18c6ed85d9 | ||
|
|
00cac75b70 | ||
|
|
039f2e854f | ||
|
|
b4f675ef5f | ||
|
|
03f74f8b4c | ||
|
|
73fda50cb2 | ||
|
|
e3b8e93ec4 | ||
|
|
c7eef15502 | ||
|
|
42a7b3b5d6 | ||
|
|
d50264b547 | ||
|
|
4e13390704 | ||
|
|
0b89992a1a | ||
|
|
054b6342c6 | ||
|
|
7efcc89499 | ||
|
|
3b012c27f0 | ||
|
|
387720d1a7 | ||
|
|
0da3406f1b | ||
|
|
a5d8934b12 | ||
|
|
392d54feaa | ||
|
|
e295eb8596 | ||
|
|
b8b72cda46 | ||
|
|
38d5a701c0 | ||
|
|
df082e68c5 | ||
|
|
1cac2f1166 | ||
|
|
951f57b106 | ||
|
|
d19454c8e8 | ||
|
|
7465b1a9a8 | ||
|
|
28fbc651da | ||
|
|
32a381fcab | ||
|
|
a0c756901d | ||
|
|
c0d0626159 | ||
|
|
8d13cdf385 | ||
|
|
0d8bf1226d | ||
|
|
282e2b1df7 | ||
|
|
0940a7700c | ||
|
|
c46e191e52 | ||
|
|
3a678028a5 | ||
|
|
ec1326cf1a | ||
|
|
91d378683e | ||
|
|
39e9a52436 | ||
|
|
b8a24fdbb0 | ||
|
|
e1957e3919 | ||
|
|
ec51e3188b | ||
|
|
4f9ffb5e3b | ||
|
|
e2530dbed3 | ||
|
|
7dc36290eb | ||
|
|
07a4618a83 | ||
|
|
43a7448ca3 | ||
|
|
232cadb2ab | ||
|
|
1bc9ece1f5 | ||
|
|
170710fdad | ||
|
|
e0c9ed813e | ||
|
|
dca334b044 | ||
|
|
a876f0e419 | ||
|
|
34c169c724 | ||
|
|
19184dc694 | ||
|
|
36a320c98e | ||
|
|
82f7c6e2b8 | ||
|
|
7c94f49293 | ||
|
|
acbb9b442b | ||
|
|
dbc708200f | ||
|
|
e52236bfc5 | ||
|
|
372bd7a018 | ||
|
|
e2e1fcd94b | ||
|
|
cfa5eccb3b | ||
|
|
d8e35c4921 | ||
|
|
5cf8184de4 | ||
|
|
774b018832 | ||
|
|
2193fda65d | ||
|
|
40136381fb | ||
|
|
822906a474 | ||
|
|
f196015fa7 | ||
|
|
898398f07f | ||
|
|
41bdda72e8 | ||
|
|
daa720b67a | ||
|
|
ac59c3fb1e | ||
|
|
373580bec9 | ||
|
|
0c64eb67a9 | ||
|
|
e046a17583 | ||
|
|
238ffeed42 | ||
|
|
e1e156febb | ||
|
|
19ae86221b | ||
|
|
08c4983721 | ||
|
|
10a3acfb62 | ||
|
|
75cddea7ce | ||
|
|
248bfec3c4 | ||
|
|
24b447eeb3 | ||
|
|
03ea073d46 | ||
|
|
05b6f7a08d | ||
|
|
ab819fa534 | ||
|
|
652fe1bc86 | ||
|
|
9a4cf52cd6 | ||
|
|
e9565fbb1f | ||
|
|
5e6adbfe19 | ||
|
|
bb2ea5f183 | ||
|
|
c1ed921716 | ||
|
|
1d46644e9b | ||
|
|
612f748595 | ||
|
|
619b96f901 | ||
|
|
bd88f95079 | ||
|
|
0744a3eb3a | ||
|
|
d75589873f | ||
|
|
1afa78682d | ||
|
|
2e4baeefc4 | ||
|
|
90562b145f | ||
|
|
6d04874961 | ||
|
|
6c31329c83 | ||
|
|
1933672b81 | ||
|
|
09210bc05e | ||
|
|
6523f66497 | ||
|
|
359605012f | ||
|
|
f16557016d | ||
|
|
354d5039c4 | ||
|
|
2c78996286 | ||
|
|
a20855e4a8 | ||
|
|
52d094dd0e | ||
|
|
ce1b63420f | ||
|
|
ddd75e2206 | ||
|
|
999f4f676b | ||
|
|
33b5e9ae76 | ||
|
|
6f683630ef | ||
|
|
a068b53a7e | ||
|
|
24b094d1cc | ||
|
|
d8bea6b8d4 | ||
|
|
bbec60788f | ||
|
|
e884d518b6 | ||
|
|
0644776001 | ||
|
|
c64ffc8eaa | ||
|
|
661426329a | ||
|
|
cb6cc0652f | ||
|
|
d309edfe9d | ||
|
|
207416957e | ||
|
|
f4eb1ebaa5 | ||
|
|
bf927da6ba | ||
|
|
2fdc20cc2e | ||
|
|
f20363a3c4 | ||
|
|
1b06e553f9 | ||
|
|
7722c8ab06 | ||
|
|
0b592ff361 | ||
|
|
6fcb6e6359 | ||
|
|
76f4bb1e86 | ||
|
|
4c22ba45f6 | ||
|
|
480c9cee0f | ||
|
|
00db41cea3 | ||
|
|
42b6dd8a8d | ||
|
|
dbff607c76 | ||
|
|
9566af6fe6 | ||
|
|
69d5d5bf75 | ||
|
|
c0a1bc139b | ||
|
|
adc3cca860 | ||
|
|
2550aebedb | ||
|
|
ed6143a8cc | ||
|
|
ef8d58c988 | ||
|
|
dce4dfeaa0 | ||
|
|
714b5b4cb2 | ||
|
|
731c9e871d | ||
|
|
e1125d91bc | ||
|
|
52998e962d | ||
|
|
b840b30f7a | ||
|
|
8787833334 | ||
|
|
1e6af3de5f | ||
|
|
281b94e7ae | ||
|
|
694d89c47e | ||
|
|
600ccf0e64 | ||
|
|
9bae5f5ef1 | ||
|
|
3ceae4f0e9 | ||
|
|
6b63846bad | ||
|
|
a1e842164b |
2523 changed files with 497481 additions and 1 deletions
48
.eslintignore
Normal file
48
.eslintignore
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
/node_modules
|
||||
**/dist
|
||||
package-lock.json
|
||||
**/.angular
|
||||
|
||||
/resources/todomvc/architecture-examples/*
|
||||
!/resources/todomvc/architecture-examples/angular
|
||||
!/resources/todomvc/architecture-examples/angular-complex
|
||||
!/resources/todomvc/architecture-examples/backbone
|
||||
!/resources/todomvc/architecture-examples/backbone-complex
|
||||
!/resources/todomvc/architecture-examples/lit
|
||||
/resources/todomvc/architecture-examples/lit/.wireit
|
||||
/resources/todomvc/architecture-examples/lit/dist/index.js
|
||||
!/resources/todomvc/architecture-examples/lit-complex
|
||||
!/resources/todomvc/architecture-examples/preact
|
||||
!/resources/todomvc/architecture-examples/preact-complex
|
||||
!/resources/todomvc/architecture-examples/react
|
||||
!/resources/todomvc/architecture-examples/react-complex
|
||||
!/resources/todomvc/architecture-examples/react-redux
|
||||
!/resources/todomvc/architecture-examples/react-redux-complex
|
||||
!/resources/todomvc/architecture-examples/svelte
|
||||
!/resources/todomvc/architecture-examples/svelte-complex
|
||||
!/resources/todomvc/architecture-examples/vue
|
||||
!/resources/todomvc/architecture-examples/vue-complex
|
||||
|
||||
/resources/todomvc/vanilla-examples/*
|
||||
!/resources/todomvc/vanilla-examples/javascript-es5
|
||||
!/resources/todomvc/vanilla-examples/javascript-es5-complex
|
||||
!/resources/todomvc/vanilla-examples/javascript-es6
|
||||
!/resources/todomvc/vanilla-examples/javascript-es6-complex
|
||||
!/resources/todomvc/vanilla-examples/javascript-es6-webpack
|
||||
!/resources/todomvc/vanilla-examples/javascript-es6-webpack-complex
|
||||
!/resources/todomvc/vanilla-examples/javascript-web-components
|
||||
!/resources/todomvc/vanilla-examples/javascript-web-components-complex/
|
||||
|
||||
!/resources/todomvc/big-dom-generator
|
||||
|
||||
!/resources/editors
|
||||
/resources/editors/assets
|
||||
|
||||
!/resources/newssite
|
||||
!/resources/newssite/news-next
|
||||
!/resources/newssite/news-nuxt
|
||||
!/resources/newssite/news-site-css
|
||||
|
||||
/resources/perf.webkit.org
|
||||
|
||||
/resources/react-stockcharts
|
||||
101
.eslintrc
Normal file
101
.eslintrc
Normal file
|
|
@ -0,0 +1,101 @@
|
|||
/*
|
||||
* Copyright (C) 2014 Apple Inc. All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions
|
||||
* are met:
|
||||
* 1. Redistributions of source code must retain the above copyright
|
||||
* notice, this list of conditions and the following disclaimer.
|
||||
* 2. Redistributions in binary form must reproduce the above copyright
|
||||
* notice, this list of conditions and the following disclaimer in the
|
||||
* documentation and/or other materials provided with the distribution.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
|
||||
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
||||
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
|
||||
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
||||
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
||||
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
||||
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
|
||||
* THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
{
|
||||
"root": true,
|
||||
"extends": ["eslint:recommended", "plugin:@typescript-eslint/base"],
|
||||
"env": {
|
||||
"browser": true,
|
||||
"node": true,
|
||||
"worker": true,
|
||||
"es2022": true,
|
||||
"mocha": true
|
||||
},
|
||||
"parserOptions": {
|
||||
"sourceType": "module"
|
||||
},
|
||||
"globals": {
|
||||
// unit test specific
|
||||
"sinon": true,
|
||||
"expect": true,
|
||||
// injected dependencies
|
||||
"testRunner": true,
|
||||
"startBenchmark": true,
|
||||
"PerfTestRunner": true,
|
||||
"deltas": true
|
||||
},
|
||||
"rules": {
|
||||
"array-bracket-spacing": [2, "never"],
|
||||
"block-spacing": [2, "always"],
|
||||
"brace-style": 2,
|
||||
"comma-dangle": [2, "only-multiline"],
|
||||
"comma-spacing": [2, { "before": false, "after": true }],
|
||||
"comma-style": [2, "last"],
|
||||
"consistent-return": 2,
|
||||
"curly": ["error", "multi-or-nest", "consistent"],
|
||||
"dot-notation": 0,
|
||||
"eol-last": 2,
|
||||
"eqeqeq": 2,
|
||||
"indent": ["error", 4, { "SwitchCase": 1 }],
|
||||
"func-call-spacing": [2, "never"],
|
||||
"key-spacing": [2, { "beforeColon": false, "afterColon": true }],
|
||||
"keyword-spacing": [2, { "before": true, "after": true }],
|
||||
"new-cap": 0,
|
||||
"new-parens": 0,
|
||||
"no-console": 0,
|
||||
"no-constant-condition": 0,
|
||||
"no-extra-bind": 2,
|
||||
"no-extra-parens": [2, "all", { "nestedBinaryExpressions": false }],
|
||||
"no-extra-semi": 2,
|
||||
"no-global-assign": 2,
|
||||
"no-inner-declarations": 0,
|
||||
"no-multi-spaces": 2,
|
||||
"no-multiple-empty-lines": ["error", { "max": 1 }],
|
||||
"no-new": 0,
|
||||
"no-proto": 0,
|
||||
"no-redeclare": 0,
|
||||
"no-return-assign": 2,
|
||||
"no-return-await": 2,
|
||||
"no-self-compare": 2,
|
||||
"no-shadow": 0,
|
||||
"no-trailing-spaces": 2,
|
||||
"no-undef": 2,
|
||||
"no-underscore-dangle": 0,
|
||||
"no-unused-expressions": 2,
|
||||
"no-unused-vars": [2, { "vars": "all", "args": "none" }],
|
||||
"no-use-before-define": 0,
|
||||
"nonblock-statement-body-position": ["error", "below"],
|
||||
"object-curly-spacing": ["error", "always"],
|
||||
"operator-linebreak": ["error", "before"],
|
||||
"prefer-template": 2,
|
||||
"quotes": [2, "double", { "avoidEscape": true }],
|
||||
"semi": 2,
|
||||
"semi-spacing": [2, { "before": false, "after": true }],
|
||||
"space-infix-ops": 2,
|
||||
"spaced-comment": [2, "always"],
|
||||
"strict": 0,
|
||||
"valid-typeof": 2
|
||||
}
|
||||
}
|
||||
75
.github/workflows/test.yml
vendored
Normal file
75
.github/workflows/test.yml
vendored
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: ["main"]
|
||||
pull_request:
|
||||
branches: ["main"]
|
||||
|
||||
# Allows you to run this workflow manually from the Actions tab
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
linters:
|
||||
name: Linters
|
||||
runs-on: macos-latest
|
||||
steps:
|
||||
- name: Checkout Branch
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: "package.json"
|
||||
cache: "npm"
|
||||
|
||||
- name: Install Node Packages
|
||||
run: npm ci
|
||||
|
||||
- name: Run linters
|
||||
run: npm run format
|
||||
|
||||
- name: Check if anything changed
|
||||
run: |
|
||||
git_status="`LC_ALL=C git status --porcelain --ignore-submodules -unormal 2>&1`"
|
||||
if [ -n "$git_status" ]; then
|
||||
printf "Some file(s) changed as the result of formatting, this means that you need to run the formatter on your patch.\n"
|
||||
printf "Here is what changed:\n"
|
||||
printf -- "$git_status\n\n"
|
||||
printf "And here is the diff:\n"
|
||||
git diff -U8
|
||||
exit 1
|
||||
fi
|
||||
|
||||
build:
|
||||
name: Build
|
||||
runs-on: macos-latest
|
||||
strategy:
|
||||
matrix:
|
||||
browser: [chrome, firefox, safari]
|
||||
steps:
|
||||
- name: Install Firefox
|
||||
if: ${{ matrix.browser == 'firefox' }}
|
||||
run: brew install --cask firefox
|
||||
|
||||
- name: Checkout Branch
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: "package.json"
|
||||
cache: "npm"
|
||||
|
||||
- name: Install Node Packages
|
||||
run: npm ci
|
||||
|
||||
- name: Run Unit Tests
|
||||
run: |
|
||||
echo "Running in $BROWSER"
|
||||
npm run test:${{ matrix.browser }}
|
||||
|
||||
- name: Run end2end Tests
|
||||
run: |
|
||||
echo "Running in $BROWSER"
|
||||
npm run test-e2e:${{ matrix.browser }}
|
||||
33
.gitignore
vendored
33
.gitignore
vendored
|
|
@ -2,3 +2,36 @@
|
|||
*.pyc
|
||||
.DS_Store
|
||||
.directory
|
||||
/node_modules
|
||||
|
||||
.vscode
|
||||
.angular
|
||||
|
||||
/resources/todomvc/architecture-examples/angular/node_modules/
|
||||
/resources/todomvc/architecture-examples/angular-complex/node_modules/
|
||||
/resources/todomvc/architecture-examples/backbone/node_modules/
|
||||
/resources/todomvc/architecture-examples/backbone-complex/node_modules/
|
||||
/resources/todomvc/architecture-examples/jquery/node_modules/
|
||||
/resources/todomvc/architecture-examples/jquery-complex/node_modules/
|
||||
/resources/todomvc/architecture-examples/preact/node_modules/
|
||||
/resources/todomvc/architecture-examples/preact-complex/node_modules/
|
||||
/resources/todomvc/architecture-examples/react/node_modules/
|
||||
/resources/todomvc/architecture-examples/react-complex/node_modules/
|
||||
/resources/todomvc/architecture-examples/react-redux/node_modules/
|
||||
/resources/todomvc/architecture-examples/react-redux-complex/node_modules/
|
||||
/resources/todomvc/architecture-examples/svelte/node_modules/
|
||||
/resources/todomvc/architecture-examples/svelte-complex/node_modules/
|
||||
/resources/todomvc/architecture-examples/vue/node_modules/
|
||||
/resources/todomvc/architecture-examples/vue-complex/node_modules/
|
||||
/resources/todomvc/architecture-examples/lit-complex/node_modules/
|
||||
|
||||
/resources/todomvc/vanilla-examples/javascript-es5/node_modules/
|
||||
/resources/todomvc/vanilla-examples/javascript-es5-complex/node_modules/
|
||||
/resources/todomvc/vanilla-examples/javascript-es6/node_modules/
|
||||
/resources/todomvc/vanilla-examples/javascript-es6-complex/node_modules/
|
||||
/resources/todomvc/vanilla-examples/javascript-es6-webpack/node_modules/
|
||||
/resources/todomvc/vanilla-examples/javascript-es6-webpack-complex/node_modules/
|
||||
/resources/todomvc/vanilla-examples/javascript-web-components-complex/node_modules/
|
||||
|
||||
/resources/charts/node_modules/
|
||||
/resources/todomvc/big-dom-generator/node_modules/
|
||||
|
|
|
|||
1
.npmrc
Normal file
1
.npmrc
Normal file
|
|
@ -0,0 +1 @@
|
|||
engine-strict = true
|
||||
48
.prettierignore
Normal file
48
.prettierignore
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
/node_modules
|
||||
**/dist
|
||||
package-lock.json
|
||||
**/.angular
|
||||
|
||||
/resources/todomvc/architecture-examples/*
|
||||
!/resources/todomvc/architecture-examples/angular
|
||||
!/resources/todomvc/architecture-examples/angular-complex
|
||||
!/resources/todomvc/architecture-examples/backbone
|
||||
!/resources/todomvc/architecture-examples/backbone-complex
|
||||
!/resources/todomvc/architecture-examples/lit
|
||||
/resources/todomvc/architecture-examples/lit/dist/index.js
|
||||
/resources/todomvc/architecture-examples/lit/.wireit
|
||||
!/resources/todomvc/architecture-examples/lit-complex
|
||||
!/resources/todomvc/architecture-examples/preact
|
||||
!/resources/todomvc/architecture-examples/preact-complex
|
||||
!/resources/todomvc/architecture-examples/react
|
||||
!/resources/todomvc/architecture-examples/react-complex
|
||||
!/resources/todomvc/architecture-examples/react-redux
|
||||
!/resources/todomvc/architecture-examples/react-redux-complex
|
||||
!/resources/todomvc/architecture-examples/svelte
|
||||
!/resources/todomvc/architecture-examples/svelte-complex
|
||||
!/resources/todomvc/architecture-examples/vue
|
||||
!/resources/todomvc/architecture-examples/vue-complex
|
||||
|
||||
/resources/todomvc/vanilla-examples/*
|
||||
!/resources/todomvc/vanilla-examples/javascript-es5
|
||||
!/resources/todomvc/vanilla-examples/javascript-es5-complex
|
||||
!/resources/todomvc/vanilla-examples/javascript-es6
|
||||
!/resources/todomvc/vanilla-examples/javascript-es6-complex
|
||||
!/resources/todomvc/vanilla-examples/javascript-es6-webpack
|
||||
!/resources/todomvc/vanilla-examples/javascript-es6-webpack-complex
|
||||
!/resources/todomvc/vanilla-examples/javascript-web-components
|
||||
!/resources/todomvc/vanilla-examples/javascript-web-components-complex/
|
||||
|
||||
!/resources/todomvc/big-dom-generator
|
||||
|
||||
!/resources/editors
|
||||
/resources/editors/assets
|
||||
|
||||
!/resources/newssite
|
||||
!/resources/newssite/news-next
|
||||
!/resources/newssite/news-nuxt
|
||||
!/resources/newssite/news-site-css
|
||||
|
||||
/resources/perf.webkit.org
|
||||
|
||||
/resources/react-stockcharts
|
||||
12
.prettierrc
Normal file
12
.prettierrc
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"arrowParens": "always",
|
||||
"bracketSameLine": false,
|
||||
"bracketSpacing": true,
|
||||
"htmlWhitespaceSensitivity": "css",
|
||||
"printWidth": 250,
|
||||
"semi": true,
|
||||
"singleQuote": false,
|
||||
"tabWidth": 4,
|
||||
"trailingComma": "es5",
|
||||
"useTabs": false
|
||||
}
|
||||
34
Development.md
Normal file
34
Development.md
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
# Development Environment
|
||||
|
||||
## Prerequisite
|
||||
|
||||
Node is required for local development.
|
||||
We recommend using a Node version manager ([nvm](https://github.com/nvm-sh/nvm) for example) to ensure a stable development environment is used.
|
||||
Below are the required node / npm versions:
|
||||
|
||||
```sh
|
||||
"node": ">18.13.0",
|
||||
"npm": ">8.19.3"
|
||||
```
|
||||
|
||||
## Installation
|
||||
|
||||
In your working directory, open terminal and paste the following commands:
|
||||
|
||||
```sh
|
||||
git clone https://github.com/WebKit/Speedometer.git
|
||||
cd Speedometer
|
||||
npm install
|
||||
```
|
||||
|
||||
## Run Development Server
|
||||
|
||||
1. In your terminal run:
|
||||
```sh
|
||||
PORT=8900 npm run dev # Passing the env var PORT is optional
|
||||
```
|
||||
2. Open your browser of choice and navigate to [http://127.0.0.1:8900](http://127.0.0.1:8900)
|
||||
|
||||
## Local Server
|
||||
|
||||
Speedometer uses [http-server](https://github.com/http-party/http-server), which is a static HTTP server. Meaning it does not provide hot-reloading. By default, the dev script disables caching and local changes can be viewed by simply refreshing your browser window. Additional options of the http-server can be found [here](https://github.com/http-party/http-server#available-options).
|
||||
34
Governance.md
Normal file
34
Governance.md
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
# Speedometer Governance Policy
|
||||
|
||||
Speedometer uses multistakeholder governance.
|
||||
This will allow us to share work, and build a collaborative understanding of performance on the web
|
||||
in order to drive resourcing towards appropriate areas.
|
||||
This also provides a structure that can endure to provide maintenance and adapt to the future web.
|
||||
|
||||
An eligible “browser project” is a core end-to-end web browser engine with an integrated JavaScript engine
|
||||
which distributes implementations widely. The project may delegate decision making within Speedometer
|
||||
to multiple representatives (for example, to review code commits or to provide consensus for major changes).
|
||||
The participating browser projects at this time are Blink/V8, Gecko/SpiderMonkey, and WebKit/JavaScriptCore.
|
||||
|
||||
The intent is that the working team should be able to move quickly for most changes,
|
||||
with a higher level of process and consensus expected based on the impact of the change.
|
||||
|
||||
- **Trivial change** - This is a change that has no effect on the official benchmark and includes changes
|
||||
to whitespaces, comments, documentation outside policies and governance model, and unofficial test cases.
|
||||
A trivial change requires approval by a reviewer, who is not the author of the change,
|
||||
from one of the participating browser projects.
|
||||
The intent is to ensure basic code quality & license compatibility, not to reach agreement.
|
||||
For example, one participating browser project might be both writing and reviewing a new benchmark in
|
||||
a subfolder to test in their own CI, or reviewing code written by an external contributor.
|
||||
- **Non-trivial change** - This is a change that has small impact on the official benchmark and includes
|
||||
changes to official test cases, test runners, bug fixes, and the appearance of the benchmark.
|
||||
A non-trivial change requires approval by at least two of the participating browser projects
|
||||
(including either authoring or reviewing the change) and none other strongly opposed to the change
|
||||
within 10 business days.
|
||||
- **Major change** - This is a change that has major implications on the official benchmark such as
|
||||
releasing of a new version of the benchmark or any revisions to governance policies and processes,
|
||||
including changes to the participating browser projects.
|
||||
A major change requires a consensus, meaning approvals by each of the participating browser projects.
|
||||
|
||||
This governance policy and associated code will be hosted inside the Speedometer repository within
|
||||
the WebKit GitHub organization under the 2-clause BSD license.
|
||||
5
LICENSE
5
LICENSE
|
|
@ -1,4 +1,7 @@
|
|||
Copyright (C) 2022 Apple Inc. All rights reserved.
|
||||
Copyright (C) 2013-2024 Apple Inc. All rights reserved.
|
||||
Copyright (C) 2017-2024 Google Inc. All rights reserved.
|
||||
Copyright (C) 2022-2024 Mozilla Corporation. All rights reserved.
|
||||
Copyright (C) 2023-2024 Microsoft Corporation. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions
|
||||
|
|
|
|||
38
README.md
Normal file
38
README.md
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
# What is Speedometer?
|
||||
|
||||
Speedometer is a benchmark for web browsers that measures Web application responsiveness
|
||||
by timing simulated user interactions on various workloads. Our primary goal is to make it
|
||||
reflect the real-world Web as much as possible. When a browser improves its score on the
|
||||
benchmark, actual users should benefit. In order to achieve this, it should:
|
||||
|
||||
- Test end-to-end user journeys instead of testing specific features in a tight loop. Each
|
||||
test should exercise the full set of what’s needed from the engine in order for a user to
|
||||
accomplish a task.
|
||||
- Evolve over time, adapting to the present Web on a regular basis. This should be informed
|
||||
by current usage data, and by consensus about features which are important for engines to
|
||||
optimize to provide a consistent experience for users and site authors.
|
||||
- Be accessible to the public and useful to browser engineers. It should run in every modern
|
||||
browser by visiting a normal web page. It should run relatively quickly, while providing
|
||||
enough test coverage to be reflective of the real-world Web.
|
||||
|
||||
# Score and Test Measurement
|
||||
|
||||
Each test can contain several steps who contributed to the test duration. Note that the prepare step of a test is _unmeasured_ and thus does not contribute to the score. All following test
|
||||
steps are **measured** and summed up in the test step time.
|
||||
|
||||
<img src="./resources/measurement-timeline.svg" />
|
||||
|
||||
Each step consists of the following phases:
|
||||
|
||||
<dl>
|
||||
<dt>Sync:</dt>
|
||||
<dd>Time spent in synchronous JS execution.</dd>
|
||||
<dt>Async:</dt>
|
||||
<dd>Time spent in asynchronous JS like Promise or setTimeout tasks scheduled by the sync-step.</dd>
|
||||
</dl>
|
||||
|
||||
## Score
|
||||
|
||||
The final benchmark score is calculated based of the inverse of [geomean](https://en.wikipedia.org/wiki/Geometric_mean) of all tests. We average the score over multiple iterations with the [arithmetic mean](https://en.wikipedia.org/wiki/Arithmetic_mean). By using the geomean to combine the test durations we maintain the invariant that relative improvements are favoured equally amongst all tests, even though they can have vastly different durations.
|
||||
|
||||
<img src="./resources/score.svg" />
|
||||
29
Testing.md
Normal file
29
Testing.md
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
# Testing
|
||||
|
||||
Speedometer uses [Selenium](https://www.selenium.dev/) for testing of the application itself.
|
||||
Tests are located in the `/tests` folder.
|
||||
|
||||
[Sinon](https://sinonjs.org/): Standalone test spies, stubs and mocks for JavaScript.
|
||||
[Mocha](https://mochajs.org/): Testing framework.
|
||||
|
||||
## Local Testing
|
||||
|
||||
To run this locally you'll need the browsers installed along with the corresponding driver:
|
||||
|
||||
- [chromedriver](https://chromedriver.chromium.org/getting-started)
|
||||
- [geckodriver](https://github.com/mozilla/geckodriver/releases)
|
||||
- [safaridriver](https://developer.apple.com/documentation/webkit/testing_with_webdriver_in_safari)
|
||||
- [msedgedriver](https://developer.microsoft.com/microsoft-edge/tools/webdriver).
|
||||
|
||||
Once installed you can run the following scripts:
|
||||
|
||||
```bash
|
||||
npm run test:chrome
|
||||
npm run test:firefox
|
||||
npm run test:safari
|
||||
npm run test:edge
|
||||
```
|
||||
|
||||
## Automated Testing
|
||||
|
||||
Currently Speedometer's tests run automatically, when pushing to the main branch or when opening a pr.
|
||||
105
about.html
Normal file
105
about.html
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
|
||||
<meta name="viewport" content="width=850" />
|
||||
<title>Speedometer 3.0 About</title>
|
||||
<link rel="stylesheet" href="resources/main.css" />
|
||||
<link rel="icon" href="resources/favicon.png" />
|
||||
</head>
|
||||
<body>
|
||||
<main>
|
||||
<section id="about" class="visible" data-title="About">
|
||||
<a href="./" class="logo">
|
||||
<img srcset="resources/logo@2x.png 2x" src="resources/logo.png" alt="Speedometer" />
|
||||
<div class="version">3.0</div>
|
||||
</a>
|
||||
<div class="section-grid">
|
||||
<h1 class="section-header">About Speedometer 3</h1>
|
||||
<div class="section-content">
|
||||
<p>Speedometer 3 is a benchmark for web browsers that measures Web application responsiveness by timing simulated user interactions on various workloads.</p>
|
||||
<p>
|
||||
It's developed as an open source project, with decisions being made under a <a href="https://github.com/WebKit/Speedometer/blob/main/Governance.md">multistakeholder governance model</a> between the three widely distributed
|
||||
web browser engine projects.
|
||||
</p>
|
||||
<p>
|
||||
The following high level user journeys are implemented in the current version. Each of these journeys has one or more workloads which test important aspects of it — for example commonly used patterns, frameworks, or
|
||||
technologies.
|
||||
</p>
|
||||
<ul>
|
||||
<li>Working with a todo list</li>
|
||||
<ul>
|
||||
<li>Measures the time to add, complete, and remove 100 todo items in a basic list.</li>
|
||||
<li>Each example implements the same todo application (TodoMVC) using different techniques and frameworks.</li>
|
||||
<li>
|
||||
Workloads: <a href="https://github.com/WebKit/Speedometer/tree/release/3.0/resources/todomvc/vanilla-examples/javascript-es5" target="_blank">TodoMVC-JavaScript-ES5</a>,
|
||||
<a href="https://github.com/WebKit/Speedometer/tree/release/3.0/resources/todomvc/vanilla-examples/javascript-es6-webpack-complex" target="_blank">TodoMVC-JavaScript-ES6-Webpack-Complex-DOM</a>,
|
||||
<a href="https://github.com/WebKit/Speedometer/tree/release/3.0/resources/todomvc/vanilla-examples/javascript-web-components" target="_blank">TodoMVC-WebComponents</a>,
|
||||
<a href="https://github.com/WebKit/Speedometer/tree/release/3.0/resources/todomvc/architecture-examples/react-complex" target="_blank">TodoMVC-React-Complex-DOM</a>,
|
||||
<a href="https://github.com/WebKit/Speedometer/tree/release/3.0/resources/todomvc/architecture-examples/react-redux" target="_blank">TodoMVC-React-Redux</a>,
|
||||
<a href="https://github.com/WebKit/Speedometer/tree/release/3.0/resources/todomvc/architecture-examples/backbone" target="_blank">TodoMVC-Backbone</a>,
|
||||
<a href="https://github.com/WebKit/Speedometer/tree/release/3.0/resources/todomvc/architecture-examples/angular-complex" target="_blank">TodoMVC-Angular-Complex-DOM</a>,
|
||||
<a href="https://github.com/WebKit/Speedometer/tree/release/3.0/resources/todomvc/architecture-examples/vue" target="_blank">TodoMVC-Vue</a>,
|
||||
<a href="https://github.com/WebKit/Speedometer/tree/release/3.0/resources/todomvc/architecture-examples/jquery" target="_blank">TodoMVC-jQuery</a>,
|
||||
<a href="https://github.com/WebKit/Speedometer/tree/release/3.0/resources/todomvc/architecture-examples/preact-complex" target="_blank">TodoMVC-Preact-Complex-DOM</a>,
|
||||
<a href="https://github.com/WebKit/Speedometer/tree/release/3.0/resources/todomvc/architecture-examples/svelte-complex" target="_blank">TodoMVC-Svelte-Complex-DOM</a>,
|
||||
<a href="https://github.com/WebKit/Speedometer/tree/release/3.0/resources/todomvc/architecture-examples/lit-complex" target="_blank">TodoMVC-Lit-Complex-DOM</a>
|
||||
</li>
|
||||
</ul>
|
||||
<li>Editing rich text</li>
|
||||
<ul>
|
||||
<li>Loading and styling text inside WYSIWYG and code editors.</li>
|
||||
<li>
|
||||
Workloads: <a href="https://github.com/WebKit/Speedometer/tree/release/3.0/resources/editors" target="_blank">Editor-CodeMirror</a>,
|
||||
<a href="https://github.com/WebKit/Speedometer/tree/release/3.0/resources/editors" target="_blank">Editor-TipTap</a>
|
||||
</li>
|
||||
</ul>
|
||||
<li>Rendering charts</li>
|
||||
<ul>
|
||||
<li>Loading and interacting with SVG and canvas charts.</li>
|
||||
<li>
|
||||
Workloads: <a href="https://github.com/WebKit/Speedometer/tree/release/3.0/resources/charts" target="_blank">Charts-observable-plot</a>,
|
||||
<a href="https://github.com/WebKit/Speedometer/tree/release/3.0/resources/charts" target="_blank">Charts-chartjs</a>,
|
||||
<a href="https://github.com/WebKit/Speedometer/tree/release/3.0/resources/react-stockcharts" target="_blank">React-Stockcharts-SVG</a>,
|
||||
<a href="https://github.com/WebKit/Speedometer/tree/release/3.0/resources/perf.webkit.org" target="_blank">Perf-Dashboard</a>
|
||||
</li>
|
||||
</ul>
|
||||
<li>Reading a news site</li>
|
||||
<ul>
|
||||
<li>Navigating across pages and interacting with a typical looking news site.</li>
|
||||
<li>
|
||||
Workloads: <a href="https://github.com/WebKit/Speedometer/tree/release/3.0/resources/newssite/news-next" target="_blank">NewsSite-Next</a>,
|
||||
<a href="https://github.com/WebKit/Speedometer/tree/release/3.0/resources/newssite/news-nuxt" target="_blank">NewsSite-Nuxt</a>
|
||||
</li>
|
||||
</ul>
|
||||
</ul>
|
||||
|
||||
<p class="note"><strong>Notes about methodology</strong></p>
|
||||
<ul>
|
||||
<li>Although user-driven actions like mouse movements and keyboard input cannot be fully emulated in JavaScript, Speedometer does its best to faithfully replay a typical workload within the demo applications.</li>
|
||||
<li>To make the run time long enough to measure with the limited precision, we synchronously execute a large number of the operations, such as adding one hundred to-do items.</li>
|
||||
<li>
|
||||
Modern browser engines execute some work asynchronously as an optimization strategy to reduce the run time of synchronous operations. While returning control back to JavaScript execution as soon as possible is worth
|
||||
pursuing, the run time cost of such an asynchronous work should still be taken into a holistic measurement of web application performance. In addition, some JavaScript frameworks call into DOM APIs asynchronously as an
|
||||
optimization technique. Speedometer approximates the run time of this asynchronous work in the UI thread with a zero-second timer that is scheduled immediately after each execution of synchronous operations.
|
||||
</li>
|
||||
<li>Speedometer does not attempt to measure concurrent asynchronous work (e.g. in Web Workers).</li>
|
||||
<li>Speedometer should not be used as a way to compare the performance of different JavaScript frameworks.</li>
|
||||
<li>
|
||||
The goal of all workloads is to represent a scenario that could be found on the Web. Although all workloads strive to use patterns that are commonly used, some implementation details are Speedometer specific and should
|
||||
not be used as a guideline on how to implement and deploy a standalone app. For example, due to constraints within the test harness, workloads must not depend on a server infrastructure to function properly and are
|
||||
built as static files ahead of time.
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="buttons section-footer">
|
||||
<div class="button-row">
|
||||
<a class="button" href="./" title="Show main section.">Home</a>
|
||||
<a class="button" href="instructions.html" title="Show test instructions.">Test Instructions</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
10
babel.config.json
Normal file
10
babel.config.json
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"plugins": [
|
||||
[
|
||||
"@babel/plugin-proposal-decorators",
|
||||
{
|
||||
"legacy": true
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
42
experimental/todomvc-localstorage/README.md
Normal file
42
experimental/todomvc-localstorage/README.md
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
# Speedometer 3.0: TodoMVC: JavaScript Es5
|
||||
|
||||
## Description
|
||||
|
||||
This application uses JavaScript with ES5 language features to implement a todo application.
|
||||
|
||||
JavaScript (JS) is a lightweight, interpreted, or just-in-time compiled programming language with first-class functions. While it is most well-known as the scripting language for Web pages, many non-browser environments also use it..
|
||||
|
||||
[JavaScript - developer.mozilla.org](http://developer.mozilla.org/en-US/docs/JavaScript)
|
||||
|
||||
## Implementation details
|
||||
|
||||
This implementation uses an explicit MVC pattern, with a clear file structure to reflect the architecture. The storage solution uses an in-memory data object that implements a simple array to hold the todos.
|
||||
|
||||
## Built steps
|
||||
|
||||
A simple build script copies all necessary files to a `dist` folder.
|
||||
It does not rely on compilers or transpilers and serves raw html, css and js files to the user.
|
||||
|
||||
```
|
||||
npm run build
|
||||
```
|
||||
|
||||
## Requirements
|
||||
|
||||
The only requirement is an installation of Node, to be able to install dependencies and run scripts to serve a local server.
|
||||
|
||||
```
|
||||
* Node (min version: 18.13.0)
|
||||
* NPM (min version: 8.19.3)
|
||||
```
|
||||
|
||||
## Local preview
|
||||
|
||||
```
|
||||
terminal:
|
||||
1. npm install
|
||||
2. npm run dev
|
||||
|
||||
browser:
|
||||
1. http://localhost:7001/
|
||||
```
|
||||
25
experimental/todomvc-localstorage/dist/app.js
vendored
Normal file
25
experimental/todomvc-localstorage/dist/app.js
vendored
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
/* global app, $on */
|
||||
(function () {
|
||||
"use strict";
|
||||
|
||||
/**
|
||||
* Sets up a brand new Todo list.
|
||||
*
|
||||
* @param {string} name The name of your new to do list.
|
||||
*/
|
||||
function Todo(name) {
|
||||
this.storage = new app.Store(name);
|
||||
this.model = new app.Model(this.storage);
|
||||
this.template = new app.Template();
|
||||
this.view = new app.View(this.template);
|
||||
this.controller = new app.Controller(this.model, this.view);
|
||||
}
|
||||
|
||||
var todo = new Todo("javascript-es5");
|
||||
|
||||
function setView() {
|
||||
todo.controller.setView(document.location.hash);
|
||||
}
|
||||
$on(window, "load", setView);
|
||||
$on(window, "hashchange", setView);
|
||||
})();
|
||||
141
experimental/todomvc-localstorage/dist/base.css
vendored
Normal file
141
experimental/todomvc-localstorage/dist/base.css
vendored
Normal file
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
266
experimental/todomvc-localstorage/dist/controller.js
vendored
Normal file
266
experimental/todomvc-localstorage/dist/controller.js
vendored
Normal file
|
|
@ -0,0 +1,266 @@
|
|||
(function (window) {
|
||||
"use strict";
|
||||
|
||||
/**
|
||||
* Takes a model and view and acts as the controller between them
|
||||
*
|
||||
* @constructor
|
||||
* @param {object} model The model instance
|
||||
* @param {object} view The view instance
|
||||
*/
|
||||
function Controller(model, view) {
|
||||
var self = this;
|
||||
self.model = model;
|
||||
self.view = view;
|
||||
|
||||
self.view.bind("newTodo", function (title) {
|
||||
self.addItem(title);
|
||||
});
|
||||
|
||||
self.view.bind("itemEdit", function (item) {
|
||||
self.editItem(item.id);
|
||||
});
|
||||
|
||||
self.view.bind("itemEditDone", function (item) {
|
||||
self.editItemSave(item.id, item.title);
|
||||
});
|
||||
|
||||
self.view.bind("itemEditCancel", function (item) {
|
||||
self.editItemCancel(item.id);
|
||||
});
|
||||
|
||||
self.view.bind("itemRemove", function (item) {
|
||||
self.removeItem(item.id);
|
||||
});
|
||||
|
||||
self.view.bind("itemToggle", function (item) {
|
||||
self.toggleComplete(item.id, item.completed);
|
||||
});
|
||||
|
||||
self.view.bind("removeCompleted", function () {
|
||||
self.removeCompletedItems();
|
||||
});
|
||||
|
||||
self.view.bind("toggleAll", function (status) {
|
||||
self.toggleAll(status.completed);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads and initialises the view
|
||||
*
|
||||
* @param {string} '' | 'active' | 'completed'
|
||||
*/
|
||||
Controller.prototype.setView = function (locationHash) {
|
||||
var route = locationHash.split("/")[1];
|
||||
var page = route || "";
|
||||
this._updateFilterState(page);
|
||||
};
|
||||
|
||||
/**
|
||||
* An event to fire on load. Will get all items and display them in the
|
||||
* todo-list
|
||||
*/
|
||||
Controller.prototype.showAll = function () {
|
||||
var self = this;
|
||||
self.model.read(function (data) {
|
||||
self.view.render("showEntries", data);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Renders all active tasks
|
||||
*/
|
||||
Controller.prototype.showActive = function () {
|
||||
var self = this;
|
||||
self.model.read({ completed: false }, function (data) {
|
||||
self.view.render("showEntries", data);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Renders all completed tasks
|
||||
*/
|
||||
Controller.prototype.showCompleted = function () {
|
||||
var self = this;
|
||||
self.model.read({ completed: true }, function (data) {
|
||||
self.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.
|
||||
*/
|
||||
Controller.prototype.addItem = function (title) {
|
||||
var self = this;
|
||||
|
||||
if (title.trim() === "")
|
||||
return;
|
||||
|
||||
self.model.create(title, function () {
|
||||
self.view.render("clearNewTodo");
|
||||
self._filter(true);
|
||||
});
|
||||
};
|
||||
|
||||
/*
|
||||
* Triggers the item editing mode.
|
||||
*/
|
||||
Controller.prototype.editItem = function (id) {
|
||||
var self = this;
|
||||
self.model.read(id, function (data) {
|
||||
self.view.render("editItem", { id: id, title: data[0].title });
|
||||
});
|
||||
};
|
||||
|
||||
/*
|
||||
* Finishes the item editing mode successfully.
|
||||
*/
|
||||
Controller.prototype.editItemSave = function (id, title) {
|
||||
var self = this;
|
||||
title = title.trim();
|
||||
|
||||
if (title.length !== 0) {
|
||||
self.model.update(id, { title: title }, function () {
|
||||
self.view.render("editItemDone", { id: id, title: title });
|
||||
});
|
||||
} else {
|
||||
self.removeItem(id);
|
||||
}
|
||||
};
|
||||
|
||||
/*
|
||||
* Cancels the item editing mode.
|
||||
*/
|
||||
Controller.prototype.editItemCancel = function (id) {
|
||||
var self = this;
|
||||
self.model.read(id, function (data) {
|
||||
self.view.render("editItemDone", { id: id, title: data[0].title });
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* By giving it an ID it'll find the DOM element matching that ID,
|
||||
* remove it from the DOM and also remove it from storage.
|
||||
*
|
||||
* @param {number} id The ID of the item to remove from the DOM and
|
||||
* storage
|
||||
*/
|
||||
Controller.prototype.removeItem = function (id) {
|
||||
var self = this;
|
||||
self.model.remove(id, function () {
|
||||
self.view.render("removeItem", id);
|
||||
});
|
||||
|
||||
self._filter();
|
||||
};
|
||||
|
||||
/**
|
||||
* Will remove all completed items from the DOM and storage.
|
||||
*/
|
||||
Controller.prototype.removeCompletedItems = function () {
|
||||
var self = this;
|
||||
self.model.read({ completed: true }, function (data) {
|
||||
data.forEach(function (item) {
|
||||
self.removeItem(item.id);
|
||||
});
|
||||
});
|
||||
|
||||
self._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
|
||||
*/
|
||||
Controller.prototype.toggleComplete = function (id, completed, silent) {
|
||||
var self = this;
|
||||
self.model.update(id, { completed: completed }, function () {
|
||||
self.view.render("elementComplete", {
|
||||
id: id,
|
||||
completed: completed,
|
||||
});
|
||||
});
|
||||
|
||||
if (!silent)
|
||||
self._filter();
|
||||
};
|
||||
|
||||
/**
|
||||
* Will toggle ALL checkboxes' on/off state and completeness of models.
|
||||
* Just pass in the event object.
|
||||
*/
|
||||
Controller.prototype.toggleAll = function (completed) {
|
||||
var self = this;
|
||||
self.model.read({ completed: !completed }, function (data) {
|
||||
data.forEach(function (item) {
|
||||
self.toggleComplete(item.id, completed, true);
|
||||
});
|
||||
});
|
||||
|
||||
self._filter();
|
||||
};
|
||||
|
||||
/**
|
||||
* Updates the pieces of the page which change depending on the remaining
|
||||
* number of todos.
|
||||
*/
|
||||
Controller.prototype._updateCount = function () {
|
||||
var self = this;
|
||||
self.model.getCount(function (todos) {
|
||||
self.view.render("updateElementCount", todos.active);
|
||||
self.view.render("clearCompletedButton", {
|
||||
completed: todos.completed,
|
||||
visible: todos.completed > 0,
|
||||
});
|
||||
|
||||
self.view.render("toggleAll", { checked: todos.completed === todos.total });
|
||||
self.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.
|
||||
*/
|
||||
Controller.prototype._filter = function (force) {
|
||||
var activeRoute = this._activeRoute.charAt(0).toUpperCase() + this._activeRoute.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
|
||||
*/
|
||||
Controller.prototype._updateFilterState = function (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);
|
||||
};
|
||||
|
||||
// Export to window
|
||||
window.app = window.app || {};
|
||||
window.app.Controller = Controller;
|
||||
})(window);
|
||||
51
experimental/todomvc-localstorage/dist/helpers.js
vendored
Normal file
51
experimental/todomvc-localstorage/dist/helpers.js
vendored
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
/* global NodeList */
|
||||
(function (window) {
|
||||
"use strict";
|
||||
|
||||
// Get element(s) by CSS selector:
|
||||
window.qs = function (selector, scope) {
|
||||
return (scope || document).querySelector(selector);
|
||||
};
|
||||
window.qsa = function (selector, scope) {
|
||||
return (scope || document).querySelectorAll(selector);
|
||||
};
|
||||
|
||||
// addEventListener wrapper:
|
||||
window.$on = function (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
|
||||
window.$delegate = function (target, selector, type, handler) {
|
||||
function dispatchEvent(event) {
|
||||
var targetElement = event.target;
|
||||
var potentialElements = window.qsa(selector, target);
|
||||
var hasMatch = Array.prototype.indexOf.call(potentialElements, targetElement) >= 0;
|
||||
|
||||
if (hasMatch)
|
||||
handler.call(targetElement, event);
|
||||
}
|
||||
|
||||
// https://developer.mozilla.org/en-US/docs/Web/Events/blur
|
||||
var useCapture = type === "blur" || type === "focus";
|
||||
|
||||
window.$on(target, type, dispatchEvent, useCapture);
|
||||
};
|
||||
|
||||
// Find the element's parent with the given tag name:
|
||||
// $parent(qs('a'), 'div');
|
||||
window.$parent = function (element, tagName) {
|
||||
if (!element.parentNode)
|
||||
return undefined;
|
||||
|
||||
if (element.parentNode.tagName.toLowerCase() === tagName.toLowerCase())
|
||||
return element.parentNode;
|
||||
|
||||
return window.$parent(element.parentNode, tagName);
|
||||
};
|
||||
|
||||
// Allow for looping on nodes by chaining:
|
||||
// qsa('.foo').forEach(function () {})
|
||||
NodeList.prototype.forEach = Array.prototype.forEach;
|
||||
})(window);
|
||||
393
experimental/todomvc-localstorage/dist/index.css
vendored
Normal file
393
experimental/todomvc-localstorage/dist/index.css
vendored
Normal file
|
|
@ -0,0 +1,393 @@
|
|||
@charset "utf-8";
|
||||
|
||||
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-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
body {
|
||||
font: 14px 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||
line-height: 1.4em;
|
||||
background: #f5f5f5;
|
||||
color: #111111;
|
||||
min-width: 230px;
|
||||
max-width: 550px;
|
||||
margin: 0 auto;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
.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: 400;
|
||||
color: rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
|
||||
.todoapp input::-moz-placeholder {
|
||||
font-style: italic;
|
||||
font-weight: 400;
|
||||
color: rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
|
||||
.todoapp input::input-placeholder {
|
||||
font-style: italic;
|
||||
font-weight: 400;
|
||||
color: rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
|
||||
.todoapp h1 {
|
||||
position: absolute;
|
||||
top: -140px;
|
||||
width: 100%;
|
||||
font-size: 80px;
|
||||
font-weight: 200;
|
||||
text-align: center;
|
||||
color: #b83f45;
|
||||
-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;
|
||||
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-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
.new-todo {
|
||||
padding: 16px 16px 16px 60px;
|
||||
height: 65px;
|
||||
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;
|
||||
}
|
||||
|
||||
.toggle-all {
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
border: none; /* Mobile Safari */
|
||||
opacity: 0;
|
||||
position: absolute;
|
||||
right: 100%;
|
||||
bottom: 100%;
|
||||
}
|
||||
|
||||
.toggle-all + label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 45px;
|
||||
height: 65px;
|
||||
font-size: 0;
|
||||
position: absolute;
|
||||
top: -65px;
|
||||
left: -0;
|
||||
}
|
||||
|
||||
.toggle-all + label:before {
|
||||
content: '❯';
|
||||
display: inline-block;
|
||||
font-size: 22px;
|
||||
color: #949494;
|
||||
padding: 10px 27px 10px 27px;
|
||||
-webkit-transform: rotate(90deg);
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
|
||||
.toggle-all:checked + label:before {
|
||||
color: #484848;
|
||||
}
|
||||
|
||||
.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: calc(100% - 43px);
|
||||
padding: 12px 16px;
|
||||
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 {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.todo-list li .toggle + label {
|
||||
/*
|
||||
Firefox requires `#` to be escaped - https://bugzilla.mozilla.org/show_bug.cgi?id=922433
|
||||
IE and Edge requires *everything* to be escaped to render, so we do that instead of just the `#` - https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/7157459/
|
||||
*/
|
||||
background-image: url('data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2240%22%20height%3D%2240%22%20viewBox%3D%22-10%20-18%20100%20135%22%3E%3Ccircle%20cx%3D%2250%22%20cy%3D%2250%22%20r%3D%2250%22%20fill%3D%22none%22%20stroke%3D%22%23949494%22%20stroke-width%3D%223%22/%3E%3C/svg%3E');
|
||||
background-repeat: no-repeat;
|
||||
background-position: center left;
|
||||
}
|
||||
|
||||
.todo-list li .toggle:checked + label {
|
||||
background-image: url('data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2240%22%20height%3D%2240%22%20viewBox%3D%22-10%20-18%20100%20135%22%3E%3Ccircle%20cx%3D%2250%22%20cy%3D%2250%22%20r%3D%2250%22%20fill%3D%22none%22%20stroke%3D%22%2359A193%22%20stroke-width%3D%223%22%2F%3E%3Cpath%20fill%3D%22%233EA390%22%20d%3D%22M72%2025L42%2071%2027%2056l-4%204%2020%2020%2034-52z%22%2F%3E%3C%2Fsvg%3E');
|
||||
}
|
||||
|
||||
.todo-list li label {
|
||||
word-break: break-all;
|
||||
padding: 15px 15px 15px 60px;
|
||||
display: block;
|
||||
line-height: 1.2;
|
||||
transition: color 0.4s;
|
||||
font-weight: 400;
|
||||
color: #484848;
|
||||
}
|
||||
|
||||
.todo-list li.completed label {
|
||||
color: #949494;
|
||||
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: #949494;
|
||||
transition: color 0.2s ease-out;
|
||||
}
|
||||
|
||||
.todo-list li .destroy:hover,
|
||||
.todo-list li .destroy:focus {
|
||||
color: #C18585;
|
||||
}
|
||||
|
||||
.todo-list li .destroy:after {
|
||||
content: '×';
|
||||
display: block;
|
||||
height: 100%;
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
.todo-list li:hover .destroy {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.todo-list li .edit {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.todo-list li.editing:last-child {
|
||||
margin-bottom: -1px;
|
||||
}
|
||||
|
||||
.footer {
|
||||
padding: 10px 15px;
|
||||
height: 20px;
|
||||
text-align: center;
|
||||
font-size: 15px;
|
||||
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:hover {
|
||||
border-color: #DB7676;
|
||||
}
|
||||
|
||||
.filters li a.selected {
|
||||
border-color: #CE4646;
|
||||
}
|
||||
|
||||
.clear-completed,
|
||||
html .clear-completed:active {
|
||||
float: right;
|
||||
position: relative;
|
||||
line-height: 19px;
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.clear-completed:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.info {
|
||||
margin: 65px auto 0;
|
||||
color: #4d4d4d;
|
||||
font-size: 11px;
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 430px) {
|
||||
.footer {
|
||||
height: 50px;
|
||||
}
|
||||
|
||||
.filters {
|
||||
bottom: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
:focus,
|
||||
.toggle:focus + label,
|
||||
.toggle-all:focus + label {
|
||||
box-shadow: 0 0 2px 2px #CF7D7D;
|
||||
outline: 0;
|
||||
}
|
||||
55
experimental/todomvc-localstorage/dist/index.html
vendored
Normal file
55
experimental/todomvc-localstorage/dist/index.html
vendored
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en" data-framework="javascript" data-version="es5" data-features="">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="description" content="A TodoMVC workload app for Speedometer!" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
|
||||
<title>TodoMVC: JavaScript Es5</title>
|
||||
<link rel="stylesheet" href="base.css" />
|
||||
<link rel="stylesheet" href="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>
|
||||
<main class="main">
|
||||
<div class="toggle-all-container">
|
||||
<input class="toggle-all" type="checkbox" />
|
||||
<label class="toggle-all-label" for="toggle-all">Mark all as complete</label>
|
||||
</div>
|
||||
<ul class="todo-list show-priority"></ul>
|
||||
</main>
|
||||
<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>Click on input field to write your todo.</p>
|
||||
<p>At least two characters are needed to be a valid entry.</p>
|
||||
<p>Press 'enter' to add the todo.</p>
|
||||
<p>Double-click to edit a todo</p>
|
||||
</footer>
|
||||
<script src="helpers.js"></script>
|
||||
<script src="store.js"></script>
|
||||
<script src="model.js"></script>
|
||||
<script src="template.js"></script>
|
||||
<script src="view.js"></script>
|
||||
<script src="controller.js"></script>
|
||||
<script src="app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
119
experimental/todomvc-localstorage/dist/model.js
vendored
Normal file
119
experimental/todomvc-localstorage/dist/model.js
vendored
Normal file
|
|
@ -0,0 +1,119 @@
|
|||
(function (window) {
|
||||
"use strict";
|
||||
|
||||
/**
|
||||
* Creates a new Model instance and hooks up the storage.
|
||||
*
|
||||
* @constructor
|
||||
* @param {object} storage A reference to the client side storage class
|
||||
*/
|
||||
function Model(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
|
||||
*/
|
||||
Model.prototype.create = function (title, callback) {
|
||||
title = title || "";
|
||||
callback = callback || function () {};
|
||||
|
||||
var 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' });
|
||||
*/
|
||||
Model.prototype.read = function (query, callback) {
|
||||
var queryType = typeof query;
|
||||
callback = callback || function () {};
|
||||
|
||||
if (queryType === "function") {
|
||||
callback = query;
|
||||
return this.storage.findAll(callback);
|
||||
} else if (queryType === "string" || queryType === "number") {
|
||||
query = parseInt(query, 10);
|
||||
return this.storage.find({ id: query }, callback);
|
||||
} else {
|
||||
return 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.
|
||||
*/
|
||||
Model.prototype.update = function (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.
|
||||
*/
|
||||
Model.prototype.remove = function (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.
|
||||
*/
|
||||
Model.prototype.removeAll = function (callback) {
|
||||
this.storage.drop(callback);
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns a count of all todos
|
||||
*/
|
||||
Model.prototype.getCount = function (callback) {
|
||||
var todos = {
|
||||
active: 0,
|
||||
completed: 0,
|
||||
total: 0,
|
||||
};
|
||||
|
||||
this.storage.findAll(function (data) {
|
||||
data.forEach(function (todo) {
|
||||
if (todo.completed)
|
||||
todos.completed++;
|
||||
else
|
||||
todos.active++;
|
||||
|
||||
todos.total++;
|
||||
});
|
||||
callback(todos);
|
||||
});
|
||||
};
|
||||
|
||||
// Export to window
|
||||
window.app = window.app || {};
|
||||
window.app.Model = Model;
|
||||
})(window);
|
||||
177
experimental/todomvc-localstorage/dist/store.js
vendored
Normal file
177
experimental/todomvc-localstorage/dist/store.js
vendored
Normal file
|
|
@ -0,0 +1,177 @@
|
|||
/* jshint eqeqeq:false */
|
||||
(function (window) {
|
||||
"use strict";
|
||||
|
||||
var MemoryStorage = {
|
||||
_data: {},
|
||||
_name: "",
|
||||
init: function (name, data = {}) {
|
||||
this._name = name;
|
||||
if (!this.getData())
|
||||
this.setData(data);
|
||||
},
|
||||
getData: function () {
|
||||
const data = this._data[this._name];
|
||||
if (!data)
|
||||
return null;
|
||||
|
||||
return JSON.parse(data);
|
||||
},
|
||||
setData: function (value) {
|
||||
this._data[this._name] = JSON.stringify(value);
|
||||
},
|
||||
};
|
||||
|
||||
var LocalStorage = {
|
||||
_name: "",
|
||||
init: function (name, data = {}) {
|
||||
this._name = name;
|
||||
if (!this.getData())
|
||||
this.setData(data);
|
||||
},
|
||||
getData: function () {
|
||||
const data = window.localStorage.getItem(this._name);
|
||||
if (!data)
|
||||
return null;
|
||||
|
||||
return JSON.parse(data);
|
||||
},
|
||||
setData: function (value) {
|
||||
window.localStorage.setItem(this._name, JSON.stringify(value));
|
||||
},
|
||||
};
|
||||
|
||||
var ID = 1;
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
function Store(name, callback, type = "disk") {
|
||||
callback = callback || function () {};
|
||||
|
||||
this._dbName = name;
|
||||
this.storage = type === "memory" ? MemoryStorage : LocalStorage;
|
||||
|
||||
this.storage.init(name, []);
|
||||
|
||||
callback.call(this, this.storage.getData());
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
* });
|
||||
*/
|
||||
Store.prototype.find = function (query, callback) {
|
||||
if (!callback)
|
||||
return;
|
||||
|
||||
var todos = this.storage.getData();
|
||||
|
||||
callback.call(
|
||||
this,
|
||||
todos.filter(function (todo) {
|
||||
for (var 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
|
||||
*/
|
||||
Store.prototype.findAll = function (callback) {
|
||||
callback = callback || function () {};
|
||||
callback.call(this, this.storage.getData());
|
||||
};
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
Store.prototype.save = function (updateData, callback, id) {
|
||||
var todos = this.storage.getData();
|
||||
|
||||
callback = callback || function () {};
|
||||
|
||||
// If an ID was actually given, find the item and update each property
|
||||
if (id) {
|
||||
for (var i = 0; i < todos.length; i++) {
|
||||
if (todos[i].id === id) {
|
||||
for (var key in updateData)
|
||||
todos[i][key] = updateData[key];
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
this.storage.setData(todos);
|
||||
callback.call(this, todos);
|
||||
} else {
|
||||
// Generate an ID
|
||||
updateData.id = ID++;
|
||||
|
||||
todos.push(updateData);
|
||||
this.storage.setData(todos);
|
||||
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
|
||||
*/
|
||||
Store.prototype.remove = function (id, callback) {
|
||||
var todos = this.storage.getData();
|
||||
|
||||
for (var i = 0; i < todos.length; i++) {
|
||||
if (todos[i].id === id) {
|
||||
todos.splice(i, 1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
this.storage.setData(todos);
|
||||
callback.call(this, todos);
|
||||
};
|
||||
|
||||
/**
|
||||
* Will drop all storage and start fresh
|
||||
*
|
||||
* @param {function} callback The callback to fire after dropping the data
|
||||
*/
|
||||
Store.prototype.drop = function (callback) {
|
||||
var data = [];
|
||||
this.storage.setData(data);
|
||||
callback.call(this, data);
|
||||
};
|
||||
|
||||
// Export to window
|
||||
window.app = window.app || {};
|
||||
window.app.Store = Store;
|
||||
})(window);
|
||||
112
experimental/todomvc-localstorage/dist/template.js
vendored
Normal file
112
experimental/todomvc-localstorage/dist/template.js
vendored
Normal file
|
|
@ -0,0 +1,112 @@
|
|||
/* jshint laxbreak:true */
|
||||
(function (window) {
|
||||
"use strict";
|
||||
|
||||
var htmlEscapes = {
|
||||
"&": "&",
|
||||
"<": "<",
|
||||
">": ">",
|
||||
'"': """,
|
||||
"'": "'",
|
||||
"`": "`",
|
||||
};
|
||||
|
||||
var escapeHtmlChar = function (chr) {
|
||||
return htmlEscapes[chr];
|
||||
};
|
||||
|
||||
var reUnescapedHtml = /[&<>"'`]/g;
|
||||
var reHasUnescapedHtml = new RegExp(reUnescapedHtml.source);
|
||||
|
||||
var escape = function (string) {
|
||||
return string && reHasUnescapedHtml.test(string) ? string.replace(reUnescapedHtml, escapeHtmlChar) : string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Sets up defaults for all the Template methods such as a default template
|
||||
*
|
||||
* @constructor
|
||||
*/
|
||||
function Template() {
|
||||
this.defaultTemplate
|
||||
= '<li data-id="{{id}}" class="{{completed}}" data-priority="{{priority}}">'
|
||||
+ '<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,
|
||||
* });
|
||||
*/
|
||||
Template.prototype.show = function (data) {
|
||||
var i, l;
|
||||
var view = "";
|
||||
|
||||
for (i = 0, l = data.length; i < l; i++) {
|
||||
var template = this.defaultTemplate;
|
||||
var completed = "";
|
||||
var checked = "";
|
||||
|
||||
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);
|
||||
template = template.replace("{{priority}}", 4 - (i % 5));
|
||||
|
||||
view = 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
|
||||
*/
|
||||
Template.prototype.itemCounter = function (activeTodos) {
|
||||
var 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
|
||||
*/
|
||||
Template.prototype.clearCompletedButton = function (completedTodos) {
|
||||
if (completedTodos > 0)
|
||||
return "Clear completed";
|
||||
else
|
||||
return "";
|
||||
};
|
||||
|
||||
// Export to window
|
||||
window.app = window.app || {};
|
||||
window.app.Template = Template;
|
||||
})(window);
|
||||
215
experimental/todomvc-localstorage/dist/view.js
vendored
Normal file
215
experimental/todomvc-localstorage/dist/view.js
vendored
Normal file
|
|
@ -0,0 +1,215 @@
|
|||
/* global qs, qsa, $on, $parent, $delegate */
|
||||
|
||||
(function (window) {
|
||||
"use strict";
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
function View(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.$toggleAllInput = qs(".toggle-all");
|
||||
this.$toggleAll = qs(".toggle-all-label");
|
||||
this.$newTodo = qs(".new-todo");
|
||||
}
|
||||
|
||||
View.prototype._removeItem = function (id) {
|
||||
var elem = qs(`[data-id="${id}"]`);
|
||||
|
||||
if (elem)
|
||||
this.$todoList.removeChild(elem);
|
||||
};
|
||||
|
||||
View.prototype._clearCompletedButton = function (completedCount, visible) {
|
||||
this.$clearCompleted.textContent = this.template.clearCompletedButton(completedCount);
|
||||
this.$clearCompleted.style.display = visible ? "block" : "none";
|
||||
};
|
||||
|
||||
View.prototype._setFilter = function (currentPage) {
|
||||
qs(".filters .selected").className = "";
|
||||
qs(`.filters [href="#/${currentPage}"]`).className = "selected";
|
||||
};
|
||||
|
||||
View.prototype._elementComplete = function (id, completed) {
|
||||
var 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;
|
||||
};
|
||||
|
||||
View.prototype._editItem = function (id, title) {
|
||||
var listItem = qs(`[data-id="${id}"]`);
|
||||
|
||||
if (!listItem)
|
||||
return;
|
||||
|
||||
listItem.className = `${listItem.className} editing`;
|
||||
|
||||
var input = document.createElement("input");
|
||||
input.className = "edit";
|
||||
|
||||
listItem.appendChild(input);
|
||||
input.focus();
|
||||
input.value = title;
|
||||
};
|
||||
|
||||
View.prototype._editItemDone = function (id, title) {
|
||||
var listItem = qs(`[data-id="${id}"]`);
|
||||
|
||||
if (!listItem)
|
||||
return;
|
||||
|
||||
var input = qs("input.edit", listItem);
|
||||
listItem.removeChild(input);
|
||||
|
||||
listItem.className = listItem.className.replace("editing", "");
|
||||
|
||||
qsa("label", listItem).forEach(function (label) {
|
||||
label.textContent = title;
|
||||
});
|
||||
};
|
||||
|
||||
View.prototype._replaceContentWithHtml = function (element, html) {
|
||||
const parsedDocument = new DOMParser().parseFromString(html, "text/html");
|
||||
element.replaceChildren(...parsedDocument.body.childNodes);
|
||||
};
|
||||
|
||||
View.prototype.render = function (viewCmd, parameter) {
|
||||
var self = this;
|
||||
var viewCommands = {
|
||||
showEntries: function () {
|
||||
self._replaceContentWithHtml(self.$todoList, self.template.show(parameter));
|
||||
},
|
||||
removeItem: function () {
|
||||
self._removeItem(parameter);
|
||||
},
|
||||
updateElementCount: function () {
|
||||
self._replaceContentWithHtml(self.$todoItemCounter, self.template.itemCounter(parameter));
|
||||
},
|
||||
clearCompletedButton: function () {
|
||||
self._clearCompletedButton(parameter.completed, parameter.visible);
|
||||
},
|
||||
contentBlockVisibility: function () {
|
||||
self.$main.style.display = self.$footer.style.display = parameter.visible ? "block" : "none";
|
||||
},
|
||||
toggleAll: function () {
|
||||
self.$toggleAll.checked = parameter.checked;
|
||||
},
|
||||
setFilter: function () {
|
||||
self._setFilter(parameter);
|
||||
},
|
||||
clearNewTodo: function () {
|
||||
self.$newTodo.value = "";
|
||||
},
|
||||
elementComplete: function () {
|
||||
self._elementComplete(parameter.id, parameter.completed);
|
||||
},
|
||||
editItem: function () {
|
||||
self._editItem(parameter.id, parameter.title);
|
||||
},
|
||||
editItemDone: function () {
|
||||
self._editItemDone(parameter.id, parameter.title);
|
||||
},
|
||||
};
|
||||
|
||||
viewCommands[viewCmd]();
|
||||
};
|
||||
|
||||
View.prototype._itemId = function (element) {
|
||||
var li = $parent(element, "li");
|
||||
return parseInt(li.dataset.id, 10);
|
||||
};
|
||||
|
||||
View.prototype._bindItemEditDone = function (handler) {
|
||||
var self = this;
|
||||
$delegate(self.$todoList, "li .edit", "blur", function () {
|
||||
if (!this.dataset.iscanceled) {
|
||||
handler({
|
||||
id: self._itemId(this),
|
||||
title: this.value,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
$delegate(self.$todoList, "li .edit", "keypress", function (event) {
|
||||
if (event.keyCode === self.ENTER_KEY) {
|
||||
// Remove the cursor from the input when you hit enter just like if it
|
||||
// were a real form
|
||||
this.blur();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
View.prototype._bindItemEditCancel = function (handler) {
|
||||
var self = this;
|
||||
$delegate(self.$todoList, "li .edit", "keyup", function (event) {
|
||||
if (event.keyCode === self.ESCAPE_KEY) {
|
||||
this.dataset.iscanceled = true;
|
||||
this.blur();
|
||||
|
||||
handler({ id: self._itemId(this) });
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
View.prototype.bind = function (event, handler) {
|
||||
var self = this;
|
||||
if (event === "newTodo") {
|
||||
$on(self.$newTodo, "change", function () {
|
||||
handler(self.$newTodo.value);
|
||||
});
|
||||
} else if (event === "removeCompleted") {
|
||||
$on(self.$clearCompleted, "click", function () {
|
||||
handler();
|
||||
});
|
||||
} else if (event === "toggleAll") {
|
||||
$on(self.$toggleAll, "click", function () {
|
||||
self.$toggleAllInput.click();
|
||||
handler({ completed: self.$toggleAllInput.checked });
|
||||
});
|
||||
} else if (event === "itemEdit") {
|
||||
$delegate(self.$todoList, "li label", "dblclick", function () {
|
||||
handler({ id: self._itemId(this) });
|
||||
});
|
||||
} else if (event === "itemRemove") {
|
||||
$delegate(self.$todoList, ".destroy", "click", function () {
|
||||
handler({ id: self._itemId(this) });
|
||||
});
|
||||
} else if (event === "itemToggle") {
|
||||
$delegate(self.$todoList, ".toggle", "click", function () {
|
||||
handler({
|
||||
id: self._itemId(this),
|
||||
completed: this.checked,
|
||||
});
|
||||
});
|
||||
} else if (event === "itemEditDone") {
|
||||
self._bindItemEditDone(handler);
|
||||
} else if (event === "itemEditCancel") {
|
||||
self._bindItemEditCancel(handler);
|
||||
}
|
||||
};
|
||||
|
||||
// Export to window
|
||||
window.app = window.app || {};
|
||||
window.app.View = View;
|
||||
})(window);
|
||||
55
experimental/todomvc-localstorage/index.html
Normal file
55
experimental/todomvc-localstorage/index.html
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en" data-framework="javascript" data-version="es5" data-features="">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="description" content="A TodoMVC workload app for Speedometer!" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
|
||||
<title>TodoMVC: JavaScript Es5</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>
|
||||
<main class="main">
|
||||
<div class="toggle-all-container">
|
||||
<input class="toggle-all" type="checkbox" />
|
||||
<label class="toggle-all-label" for="toggle-all">Mark all as complete</label>
|
||||
</div>
|
||||
<ul class="todo-list show-priority"></ul>
|
||||
</main>
|
||||
<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>Click on input field to write your todo.</p>
|
||||
<p>At least two characters are needed to be a valid entry.</p>
|
||||
<p>Press 'enter' to add the todo.</p>
|
||||
<p>Double-click to edit a todo</p>
|
||||
</footer>
|
||||
<script src="src/helpers.js"></script>
|
||||
<script src="src/store.js"></script>
|
||||
<script src="src/model.js"></script>
|
||||
<script src="src/template.js"></script>
|
||||
<script src="src/view.js"></script>
|
||||
<script src="src/controller.js"></script>
|
||||
<script src="src/app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
787
experimental/todomvc-localstorage/package-lock.json
generated
Normal file
787
experimental/todomvc-localstorage/package-lock.json
generated
Normal file
|
|
@ -0,0 +1,787 @@
|
|||
{
|
||||
"name": "todomvc-javascript-es5",
|
||||
"version": "1.0.0",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "todomvc-javascript-es5",
|
||||
"version": "1.0.0",
|
||||
"dependencies": {
|
||||
"todomvc-app-css": "^2.4.2",
|
||||
"todomvc-common": "^1.0.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"http-server": "^14.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.13.0",
|
||||
"npm": ">=8.19.3"
|
||||
}
|
||||
},
|
||||
"node_modules/ansi-styles": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
|
||||
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"color-convert": "^2.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/async": {
|
||||
"version": "2.6.4",
|
||||
"resolved": "https://registry.npmjs.org/async/-/async-2.6.4.tgz",
|
||||
"integrity": "sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"lodash": "^4.17.14"
|
||||
}
|
||||
},
|
||||
"node_modules/basic-auth": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz",
|
||||
"integrity": "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"safe-buffer": "5.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/call-bind": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz",
|
||||
"integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"function-bind": "^1.1.1",
|
||||
"get-intrinsic": "^1.0.2"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/chalk": {
|
||||
"version": "4.1.2",
|
||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
|
||||
"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"ansi-styles": "^4.1.0",
|
||||
"supports-color": "^7.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/chalk?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/color-convert": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"color-name": "~1.1.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=7.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/color-name": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
|
||||
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/corser": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/corser/-/corser-2.0.1.tgz",
|
||||
"integrity": "sha512-utCYNzRSQIZNPIcGZdQc92UVJYAhtGAteCFg0yRaFm8f0P+CPtyGyHXJcGXnffjCybUCEx3FQ2G7U3/o9eIkVQ==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">= 0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/debug": {
|
||||
"version": "3.2.7",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz",
|
||||
"integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"ms": "^2.1.1"
|
||||
}
|
||||
},
|
||||
"node_modules/eventemitter3": {
|
||||
"version": "4.0.7",
|
||||
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz",
|
||||
"integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/follow-redirects": {
|
||||
"version": "1.15.5",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.5.tgz",
|
||||
"integrity": "sha512-vSFWUON1B+yAw1VN4xMfxgn5fTUiaOzAJCKBwIIgT/+7CuGy9+r+5gITvP62j3RmaD5Ph65UaERdOSRGUzZtgw==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "individual",
|
||||
"url": "https://github.com/sponsors/RubenVerborgh"
|
||||
}
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=4.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"debug": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/function-bind": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
|
||||
"integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/get-intrinsic": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.0.tgz",
|
||||
"integrity": "sha512-L049y6nFOuom5wGyRc3/gdTLO94dySVKRACj1RmJZBQXlbTMhtNIgkWkUHq+jYmZvKf14EW1EoJnnjbmoHij0Q==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"function-bind": "^1.1.1",
|
||||
"has": "^1.0.3",
|
||||
"has-symbols": "^1.0.3"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/has": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz",
|
||||
"integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"function-bind": "^1.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/has-flag": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
|
||||
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/has-symbols": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz",
|
||||
"integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/he": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz",
|
||||
"integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==",
|
||||
"dev": true,
|
||||
"bin": {
|
||||
"he": "bin/he"
|
||||
}
|
||||
},
|
||||
"node_modules/html-encoding-sniffer": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-3.0.0.tgz",
|
||||
"integrity": "sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"whatwg-encoding": "^2.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/http-proxy": {
|
||||
"version": "1.18.1",
|
||||
"resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz",
|
||||
"integrity": "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"eventemitter3": "^4.0.0",
|
||||
"follow-redirects": "^1.0.0",
|
||||
"requires-port": "^1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/http-server": {
|
||||
"version": "14.1.1",
|
||||
"resolved": "https://registry.npmjs.org/http-server/-/http-server-14.1.1.tgz",
|
||||
"integrity": "sha512-+cbxadF40UXd9T01zUHgA+rlo2Bg1Srer4+B4NwIHdaGxAGGv59nYRnGGDJ9LBk7alpS0US+J+bLLdQOOkJq4A==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"basic-auth": "^2.0.1",
|
||||
"chalk": "^4.1.2",
|
||||
"corser": "^2.0.1",
|
||||
"he": "^1.2.0",
|
||||
"html-encoding-sniffer": "^3.0.0",
|
||||
"http-proxy": "^1.18.1",
|
||||
"mime": "^1.6.0",
|
||||
"minimist": "^1.2.6",
|
||||
"opener": "^1.5.1",
|
||||
"portfinder": "^1.0.28",
|
||||
"secure-compare": "3.0.1",
|
||||
"union": "~0.5.0",
|
||||
"url-join": "^4.0.1"
|
||||
},
|
||||
"bin": {
|
||||
"http-server": "bin/http-server"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/iconv-lite": {
|
||||
"version": "0.6.3",
|
||||
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
|
||||
"integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"safer-buffer": ">= 2.1.2 < 3.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/lodash": {
|
||||
"version": "4.17.21",
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
|
||||
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/mime": {
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
|
||||
"integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
|
||||
"dev": true,
|
||||
"bin": {
|
||||
"mime": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/minimist": {
|
||||
"version": "1.2.8",
|
||||
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
|
||||
"integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
|
||||
"dev": true,
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/mkdirp": {
|
||||
"version": "0.5.6",
|
||||
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz",
|
||||
"integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"minimist": "^1.2.6"
|
||||
},
|
||||
"bin": {
|
||||
"mkdirp": "bin/cmd.js"
|
||||
}
|
||||
},
|
||||
"node_modules/ms": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/object-inspect": {
|
||||
"version": "1.12.3",
|
||||
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.3.tgz",
|
||||
"integrity": "sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==",
|
||||
"dev": true,
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/opener": {
|
||||
"version": "1.5.2",
|
||||
"resolved": "https://registry.npmjs.org/opener/-/opener-1.5.2.tgz",
|
||||
"integrity": "sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A==",
|
||||
"dev": true,
|
||||
"bin": {
|
||||
"opener": "bin/opener-bin.js"
|
||||
}
|
||||
},
|
||||
"node_modules/portfinder": {
|
||||
"version": "1.0.32",
|
||||
"resolved": "https://registry.npmjs.org/portfinder/-/portfinder-1.0.32.tgz",
|
||||
"integrity": "sha512-on2ZJVVDXRADWE6jnQaX0ioEylzgBpQk8r55NE4wjXW1ZxO+BgDlY6DXwj20i0V8eB4SenDQ00WEaxfiIQPcxg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"async": "^2.6.4",
|
||||
"debug": "^3.2.7",
|
||||
"mkdirp": "^0.5.6"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/qs": {
|
||||
"version": "6.11.0",
|
||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz",
|
||||
"integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"side-channel": "^1.0.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.6"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/requires-port": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz",
|
||||
"integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/safe-buffer": {
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
|
||||
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/safer-buffer": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
|
||||
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/secure-compare": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/secure-compare/-/secure-compare-3.0.1.tgz",
|
||||
"integrity": "sha512-AckIIV90rPDcBcglUwXPF3kg0P0qmPsPXAj6BBEENQE1p5yA1xfmDJzfi1Tappj37Pv2mVbKpL3Z1T+Nn7k1Qw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/side-channel": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz",
|
||||
"integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"call-bind": "^1.0.0",
|
||||
"get-intrinsic": "^1.0.2",
|
||||
"object-inspect": "^1.9.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/supports-color": {
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
|
||||
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"has-flag": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/todomvc-app-css": {
|
||||
"version": "2.4.2",
|
||||
"resolved": "https://registry.npmjs.org/todomvc-app-css/-/todomvc-app-css-2.4.2.tgz",
|
||||
"integrity": "sha512-ViAkQ7ed89rmhFIGRsT36njN+97z8+s3XsJnB8E2IKOq+/SLD/6PtSvmTtiwUcVk39qPcjAc/OyeDys4LoJUVg=="
|
||||
},
|
||||
"node_modules/todomvc-common": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/todomvc-common/-/todomvc-common-1.0.5.tgz",
|
||||
"integrity": "sha512-D8kEJmxVMQIWwztEdH+WeiAfXRbbSCpgXq4NkYi+gduJ2tr8CNq7sYLfJvjpQ10KD9QxJwig57rvMbV2QAESwQ=="
|
||||
},
|
||||
"node_modules/union": {
|
||||
"version": "0.5.0",
|
||||
"resolved": "https://registry.npmjs.org/union/-/union-0.5.0.tgz",
|
||||
"integrity": "sha512-N6uOhuW6zO95P3Mel2I2zMsbsanvvtgn6jVqJv4vbVcz/JN0OkL9suomjQGmWtxJQXOCqUJvquc1sMeNz/IwlA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"qs": "^6.4.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/url-join": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/url-join/-/url-join-4.0.1.tgz",
|
||||
"integrity": "sha512-jk1+QP6ZJqyOiuEI9AEWQfju/nB2Pw466kbA0LEZljHwKeMgd9WrAEgEGxjPDD2+TNbbb37rTyhEfrCXfuKXnA==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/whatwg-encoding": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-2.0.0.tgz",
|
||||
"integrity": "sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"iconv-lite": "0.6.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"ansi-styles": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
|
||||
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"color-convert": "^2.0.1"
|
||||
}
|
||||
},
|
||||
"async": {
|
||||
"version": "2.6.4",
|
||||
"resolved": "https://registry.npmjs.org/async/-/async-2.6.4.tgz",
|
||||
"integrity": "sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"lodash": "^4.17.14"
|
||||
}
|
||||
},
|
||||
"basic-auth": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz",
|
||||
"integrity": "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"safe-buffer": "5.1.2"
|
||||
}
|
||||
},
|
||||
"call-bind": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz",
|
||||
"integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"function-bind": "^1.1.1",
|
||||
"get-intrinsic": "^1.0.2"
|
||||
}
|
||||
},
|
||||
"chalk": {
|
||||
"version": "4.1.2",
|
||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
|
||||
"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"ansi-styles": "^4.1.0",
|
||||
"supports-color": "^7.1.0"
|
||||
}
|
||||
},
|
||||
"color-convert": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"color-name": "~1.1.4"
|
||||
}
|
||||
},
|
||||
"color-name": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
|
||||
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
|
||||
"dev": true
|
||||
},
|
||||
"corser": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/corser/-/corser-2.0.1.tgz",
|
||||
"integrity": "sha512-utCYNzRSQIZNPIcGZdQc92UVJYAhtGAteCFg0yRaFm8f0P+CPtyGyHXJcGXnffjCybUCEx3FQ2G7U3/o9eIkVQ==",
|
||||
"dev": true
|
||||
},
|
||||
"debug": {
|
||||
"version": "3.2.7",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz",
|
||||
"integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"ms": "^2.1.1"
|
||||
}
|
||||
},
|
||||
"eventemitter3": {
|
||||
"version": "4.0.7",
|
||||
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz",
|
||||
"integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==",
|
||||
"dev": true
|
||||
},
|
||||
"follow-redirects": {
|
||||
"version": "1.15.5",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.5.tgz",
|
||||
"integrity": "sha512-vSFWUON1B+yAw1VN4xMfxgn5fTUiaOzAJCKBwIIgT/+7CuGy9+r+5gITvP62j3RmaD5Ph65UaERdOSRGUzZtgw==",
|
||||
"dev": true
|
||||
},
|
||||
"function-bind": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
|
||||
"integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==",
|
||||
"dev": true
|
||||
},
|
||||
"get-intrinsic": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.0.tgz",
|
||||
"integrity": "sha512-L049y6nFOuom5wGyRc3/gdTLO94dySVKRACj1RmJZBQXlbTMhtNIgkWkUHq+jYmZvKf14EW1EoJnnjbmoHij0Q==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"function-bind": "^1.1.1",
|
||||
"has": "^1.0.3",
|
||||
"has-symbols": "^1.0.3"
|
||||
}
|
||||
},
|
||||
"has": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz",
|
||||
"integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"function-bind": "^1.1.1"
|
||||
}
|
||||
},
|
||||
"has-flag": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
|
||||
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
|
||||
"dev": true
|
||||
},
|
||||
"has-symbols": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz",
|
||||
"integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==",
|
||||
"dev": true
|
||||
},
|
||||
"he": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz",
|
||||
"integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==",
|
||||
"dev": true
|
||||
},
|
||||
"html-encoding-sniffer": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-3.0.0.tgz",
|
||||
"integrity": "sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"whatwg-encoding": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"http-proxy": {
|
||||
"version": "1.18.1",
|
||||
"resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz",
|
||||
"integrity": "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"eventemitter3": "^4.0.0",
|
||||
"follow-redirects": "^1.0.0",
|
||||
"requires-port": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"http-server": {
|
||||
"version": "14.1.1",
|
||||
"resolved": "https://registry.npmjs.org/http-server/-/http-server-14.1.1.tgz",
|
||||
"integrity": "sha512-+cbxadF40UXd9T01zUHgA+rlo2Bg1Srer4+B4NwIHdaGxAGGv59nYRnGGDJ9LBk7alpS0US+J+bLLdQOOkJq4A==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"basic-auth": "^2.0.1",
|
||||
"chalk": "^4.1.2",
|
||||
"corser": "^2.0.1",
|
||||
"he": "^1.2.0",
|
||||
"html-encoding-sniffer": "^3.0.0",
|
||||
"http-proxy": "^1.18.1",
|
||||
"mime": "^1.6.0",
|
||||
"minimist": "^1.2.6",
|
||||
"opener": "^1.5.1",
|
||||
"portfinder": "^1.0.28",
|
||||
"secure-compare": "3.0.1",
|
||||
"union": "~0.5.0",
|
||||
"url-join": "^4.0.1"
|
||||
}
|
||||
},
|
||||
"iconv-lite": {
|
||||
"version": "0.6.3",
|
||||
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
|
||||
"integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"safer-buffer": ">= 2.1.2 < 3.0.0"
|
||||
}
|
||||
},
|
||||
"lodash": {
|
||||
"version": "4.17.21",
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
|
||||
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
|
||||
"dev": true
|
||||
},
|
||||
"mime": {
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
|
||||
"integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
|
||||
"dev": true
|
||||
},
|
||||
"minimist": {
|
||||
"version": "1.2.8",
|
||||
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
|
||||
"integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
|
||||
"dev": true
|
||||
},
|
||||
"mkdirp": {
|
||||
"version": "0.5.6",
|
||||
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz",
|
||||
"integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"minimist": "^1.2.6"
|
||||
}
|
||||
},
|
||||
"ms": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||
"dev": true
|
||||
},
|
||||
"object-inspect": {
|
||||
"version": "1.12.3",
|
||||
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.3.tgz",
|
||||
"integrity": "sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==",
|
||||
"dev": true
|
||||
},
|
||||
"opener": {
|
||||
"version": "1.5.2",
|
||||
"resolved": "https://registry.npmjs.org/opener/-/opener-1.5.2.tgz",
|
||||
"integrity": "sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A==",
|
||||
"dev": true
|
||||
},
|
||||
"portfinder": {
|
||||
"version": "1.0.32",
|
||||
"resolved": "https://registry.npmjs.org/portfinder/-/portfinder-1.0.32.tgz",
|
||||
"integrity": "sha512-on2ZJVVDXRADWE6jnQaX0ioEylzgBpQk8r55NE4wjXW1ZxO+BgDlY6DXwj20i0V8eB4SenDQ00WEaxfiIQPcxg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"async": "^2.6.4",
|
||||
"debug": "^3.2.7",
|
||||
"mkdirp": "^0.5.6"
|
||||
}
|
||||
},
|
||||
"qs": {
|
||||
"version": "6.11.0",
|
||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz",
|
||||
"integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"side-channel": "^1.0.4"
|
||||
}
|
||||
},
|
||||
"requires-port": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz",
|
||||
"integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==",
|
||||
"dev": true
|
||||
},
|
||||
"safe-buffer": {
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
|
||||
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
|
||||
"dev": true
|
||||
},
|
||||
"safer-buffer": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
|
||||
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
|
||||
"dev": true
|
||||
},
|
||||
"secure-compare": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/secure-compare/-/secure-compare-3.0.1.tgz",
|
||||
"integrity": "sha512-AckIIV90rPDcBcglUwXPF3kg0P0qmPsPXAj6BBEENQE1p5yA1xfmDJzfi1Tappj37Pv2mVbKpL3Z1T+Nn7k1Qw==",
|
||||
"dev": true
|
||||
},
|
||||
"side-channel": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz",
|
||||
"integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"call-bind": "^1.0.0",
|
||||
"get-intrinsic": "^1.0.2",
|
||||
"object-inspect": "^1.9.0"
|
||||
}
|
||||
},
|
||||
"supports-color": {
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
|
||||
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"has-flag": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"todomvc-app-css": {
|
||||
"version": "2.4.2",
|
||||
"resolved": "https://registry.npmjs.org/todomvc-app-css/-/todomvc-app-css-2.4.2.tgz",
|
||||
"integrity": "sha512-ViAkQ7ed89rmhFIGRsT36njN+97z8+s3XsJnB8E2IKOq+/SLD/6PtSvmTtiwUcVk39qPcjAc/OyeDys4LoJUVg=="
|
||||
},
|
||||
"todomvc-common": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/todomvc-common/-/todomvc-common-1.0.5.tgz",
|
||||
"integrity": "sha512-D8kEJmxVMQIWwztEdH+WeiAfXRbbSCpgXq4NkYi+gduJ2tr8CNq7sYLfJvjpQ10KD9QxJwig57rvMbV2QAESwQ=="
|
||||
},
|
||||
"union": {
|
||||
"version": "0.5.0",
|
||||
"resolved": "https://registry.npmjs.org/union/-/union-0.5.0.tgz",
|
||||
"integrity": "sha512-N6uOhuW6zO95P3Mel2I2zMsbsanvvtgn6jVqJv4vbVcz/JN0OkL9suomjQGmWtxJQXOCqUJvquc1sMeNz/IwlA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"qs": "^6.4.0"
|
||||
}
|
||||
},
|
||||
"url-join": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/url-join/-/url-join-4.0.1.tgz",
|
||||
"integrity": "sha512-jk1+QP6ZJqyOiuEI9AEWQfju/nB2Pw466kbA0LEZljHwKeMgd9WrAEgEGxjPDD2+TNbbb37rTyhEfrCXfuKXnA==",
|
||||
"dev": true
|
||||
},
|
||||
"whatwg-encoding": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-2.0.0.tgz",
|
||||
"integrity": "sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"iconv-lite": "0.6.3"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
22
experimental/todomvc-localstorage/package.json
Normal file
22
experimental/todomvc-localstorage/package.json
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
{
|
||||
"name": "todomvc-localstorage",
|
||||
"version": "1.0.0",
|
||||
"description": "TodoMVC app written with JavaScript es5 features.",
|
||||
"engines": {
|
||||
"node": ">=18.13.0",
|
||||
"npm": ">=8.19.3"
|
||||
},
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "http-server ./ -p 7001 -c-1 --cors",
|
||||
"build": "node scripts/build.js",
|
||||
"serve": "http-server ./dist -p 7002 -c-1 --cors"
|
||||
},
|
||||
"dependencies": {
|
||||
"todomvc-app-css": "^2.4.2",
|
||||
"todomvc-common": "^1.0.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"http-server": "^14.1.1"
|
||||
}
|
||||
}
|
||||
56
experimental/todomvc-localstorage/scripts/build.js
Normal file
56
experimental/todomvc-localstorage/scripts/build.js
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
const fs = require("fs").promises;
|
||||
const path = require("path");
|
||||
|
||||
const rootDirectory = "./";
|
||||
const sourceDirectory = "./src";
|
||||
const targetDirectory = "./dist";
|
||||
|
||||
const htmlFile = "index.html";
|
||||
|
||||
const filesToMove = ["node_modules/todomvc-common/base.css", "node_modules/todomvc-app-css/index.css"];
|
||||
|
||||
const copy = async (src, dest) => {
|
||||
await fs.copyFile(src, dest);
|
||||
};
|
||||
|
||||
const build = async () => {
|
||||
// remove dist directory if it exists
|
||||
await fs.rm(targetDirectory, { recursive: true, force: true });
|
||||
|
||||
// re-create the directory.
|
||||
await fs.mkdir(targetDirectory);
|
||||
|
||||
// copy src folder
|
||||
await fs.cp(sourceDirectory, targetDirectory, { recursive: true }, (err) => {
|
||||
if (err)
|
||||
console.error(err);
|
||||
});
|
||||
|
||||
// copy html file
|
||||
await fs.copyFile(path.join(rootDirectory, htmlFile), path.join(targetDirectory, htmlFile));
|
||||
|
||||
// copy files to move
|
||||
for (let i = 0; i < filesToMove.length; i++)
|
||||
await copy(filesToMove[i], path.join(targetDirectory, path.basename(filesToMove[i])));
|
||||
|
||||
// read html file
|
||||
let html = await fs.readFile(path.join(targetDirectory, htmlFile), "utf8");
|
||||
|
||||
// remove base paths from files to move
|
||||
for (let i = 0; i < filesToMove.length; i++) {
|
||||
const basePath = `${filesToMove[i].split("/").slice(0, -1).join("/")}/`;
|
||||
html = html.replace(basePath, "");
|
||||
}
|
||||
|
||||
// remove basePath from source directory
|
||||
const basePath = `${path.basename(sourceDirectory)}/`;
|
||||
const re = new RegExp(basePath, "g");
|
||||
html = html.replace(re, "");
|
||||
|
||||
// write html files
|
||||
await fs.writeFile(`${targetDirectory}/${htmlFile}`, html);
|
||||
|
||||
console.log("done!!");
|
||||
};
|
||||
|
||||
build();
|
||||
25
experimental/todomvc-localstorage/src/app.js
Normal file
25
experimental/todomvc-localstorage/src/app.js
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
/* global app, $on */
|
||||
(function () {
|
||||
"use strict";
|
||||
|
||||
/**
|
||||
* Sets up a brand new Todo list.
|
||||
*
|
||||
* @param {string} name The name of your new to do list.
|
||||
*/
|
||||
function Todo(name) {
|
||||
this.storage = new app.Store(name);
|
||||
this.model = new app.Model(this.storage);
|
||||
this.template = new app.Template();
|
||||
this.view = new app.View(this.template);
|
||||
this.controller = new app.Controller(this.model, this.view);
|
||||
}
|
||||
|
||||
var todo = new Todo("javascript-es5");
|
||||
|
||||
function setView() {
|
||||
todo.controller.setView(document.location.hash);
|
||||
}
|
||||
$on(window, "load", setView);
|
||||
$on(window, "hashchange", setView);
|
||||
})();
|
||||
266
experimental/todomvc-localstorage/src/controller.js
Normal file
266
experimental/todomvc-localstorage/src/controller.js
Normal file
|
|
@ -0,0 +1,266 @@
|
|||
(function (window) {
|
||||
"use strict";
|
||||
|
||||
/**
|
||||
* Takes a model and view and acts as the controller between them
|
||||
*
|
||||
* @constructor
|
||||
* @param {object} model The model instance
|
||||
* @param {object} view The view instance
|
||||
*/
|
||||
function Controller(model, view) {
|
||||
var self = this;
|
||||
self.model = model;
|
||||
self.view = view;
|
||||
|
||||
self.view.bind("newTodo", function (title) {
|
||||
self.addItem(title);
|
||||
});
|
||||
|
||||
self.view.bind("itemEdit", function (item) {
|
||||
self.editItem(item.id);
|
||||
});
|
||||
|
||||
self.view.bind("itemEditDone", function (item) {
|
||||
self.editItemSave(item.id, item.title);
|
||||
});
|
||||
|
||||
self.view.bind("itemEditCancel", function (item) {
|
||||
self.editItemCancel(item.id);
|
||||
});
|
||||
|
||||
self.view.bind("itemRemove", function (item) {
|
||||
self.removeItem(item.id);
|
||||
});
|
||||
|
||||
self.view.bind("itemToggle", function (item) {
|
||||
self.toggleComplete(item.id, item.completed);
|
||||
});
|
||||
|
||||
self.view.bind("removeCompleted", function () {
|
||||
self.removeCompletedItems();
|
||||
});
|
||||
|
||||
self.view.bind("toggleAll", function (status) {
|
||||
self.toggleAll(status.completed);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads and initialises the view
|
||||
*
|
||||
* @param {string} '' | 'active' | 'completed'
|
||||
*/
|
||||
Controller.prototype.setView = function (locationHash) {
|
||||
var route = locationHash.split("/")[1];
|
||||
var page = route || "";
|
||||
this._updateFilterState(page);
|
||||
};
|
||||
|
||||
/**
|
||||
* An event to fire on load. Will get all items and display them in the
|
||||
* todo-list
|
||||
*/
|
||||
Controller.prototype.showAll = function () {
|
||||
var self = this;
|
||||
self.model.read(function (data) {
|
||||
self.view.render("showEntries", data);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Renders all active tasks
|
||||
*/
|
||||
Controller.prototype.showActive = function () {
|
||||
var self = this;
|
||||
self.model.read({ completed: false }, function (data) {
|
||||
self.view.render("showEntries", data);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Renders all completed tasks
|
||||
*/
|
||||
Controller.prototype.showCompleted = function () {
|
||||
var self = this;
|
||||
self.model.read({ completed: true }, function (data) {
|
||||
self.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.
|
||||
*/
|
||||
Controller.prototype.addItem = function (title) {
|
||||
var self = this;
|
||||
|
||||
if (title.trim() === "")
|
||||
return;
|
||||
|
||||
self.model.create(title, function () {
|
||||
self.view.render("clearNewTodo");
|
||||
self._filter(true);
|
||||
});
|
||||
};
|
||||
|
||||
/*
|
||||
* Triggers the item editing mode.
|
||||
*/
|
||||
Controller.prototype.editItem = function (id) {
|
||||
var self = this;
|
||||
self.model.read(id, function (data) {
|
||||
self.view.render("editItem", { id: id, title: data[0].title });
|
||||
});
|
||||
};
|
||||
|
||||
/*
|
||||
* Finishes the item editing mode successfully.
|
||||
*/
|
||||
Controller.prototype.editItemSave = function (id, title) {
|
||||
var self = this;
|
||||
title = title.trim();
|
||||
|
||||
if (title.length !== 0) {
|
||||
self.model.update(id, { title: title }, function () {
|
||||
self.view.render("editItemDone", { id: id, title: title });
|
||||
});
|
||||
} else {
|
||||
self.removeItem(id);
|
||||
}
|
||||
};
|
||||
|
||||
/*
|
||||
* Cancels the item editing mode.
|
||||
*/
|
||||
Controller.prototype.editItemCancel = function (id) {
|
||||
var self = this;
|
||||
self.model.read(id, function (data) {
|
||||
self.view.render("editItemDone", { id: id, title: data[0].title });
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* By giving it an ID it'll find the DOM element matching that ID,
|
||||
* remove it from the DOM and also remove it from storage.
|
||||
*
|
||||
* @param {number} id The ID of the item to remove from the DOM and
|
||||
* storage
|
||||
*/
|
||||
Controller.prototype.removeItem = function (id) {
|
||||
var self = this;
|
||||
self.model.remove(id, function () {
|
||||
self.view.render("removeItem", id);
|
||||
});
|
||||
|
||||
self._filter();
|
||||
};
|
||||
|
||||
/**
|
||||
* Will remove all completed items from the DOM and storage.
|
||||
*/
|
||||
Controller.prototype.removeCompletedItems = function () {
|
||||
var self = this;
|
||||
self.model.read({ completed: true }, function (data) {
|
||||
data.forEach(function (item) {
|
||||
self.removeItem(item.id);
|
||||
});
|
||||
});
|
||||
|
||||
self._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
|
||||
*/
|
||||
Controller.prototype.toggleComplete = function (id, completed, silent) {
|
||||
var self = this;
|
||||
self.model.update(id, { completed: completed }, function () {
|
||||
self.view.render("elementComplete", {
|
||||
id: id,
|
||||
completed: completed,
|
||||
});
|
||||
});
|
||||
|
||||
if (!silent)
|
||||
self._filter();
|
||||
};
|
||||
|
||||
/**
|
||||
* Will toggle ALL checkboxes' on/off state and completeness of models.
|
||||
* Just pass in the event object.
|
||||
*/
|
||||
Controller.prototype.toggleAll = function (completed) {
|
||||
var self = this;
|
||||
self.model.read({ completed: !completed }, function (data) {
|
||||
data.forEach(function (item) {
|
||||
self.toggleComplete(item.id, completed, true);
|
||||
});
|
||||
});
|
||||
|
||||
self._filter();
|
||||
};
|
||||
|
||||
/**
|
||||
* Updates the pieces of the page which change depending on the remaining
|
||||
* number of todos.
|
||||
*/
|
||||
Controller.prototype._updateCount = function () {
|
||||
var self = this;
|
||||
self.model.getCount(function (todos) {
|
||||
self.view.render("updateElementCount", todos.active);
|
||||
self.view.render("clearCompletedButton", {
|
||||
completed: todos.completed,
|
||||
visible: todos.completed > 0,
|
||||
});
|
||||
|
||||
self.view.render("toggleAll", { checked: todos.completed === todos.total });
|
||||
self.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.
|
||||
*/
|
||||
Controller.prototype._filter = function (force) {
|
||||
var activeRoute = this._activeRoute.charAt(0).toUpperCase() + this._activeRoute.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
|
||||
*/
|
||||
Controller.prototype._updateFilterState = function (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);
|
||||
};
|
||||
|
||||
// Export to window
|
||||
window.app = window.app || {};
|
||||
window.app.Controller = Controller;
|
||||
})(window);
|
||||
51
experimental/todomvc-localstorage/src/helpers.js
Normal file
51
experimental/todomvc-localstorage/src/helpers.js
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
/* global NodeList */
|
||||
(function (window) {
|
||||
"use strict";
|
||||
|
||||
// Get element(s) by CSS selector:
|
||||
window.qs = function (selector, scope) {
|
||||
return (scope || document).querySelector(selector);
|
||||
};
|
||||
window.qsa = function (selector, scope) {
|
||||
return (scope || document).querySelectorAll(selector);
|
||||
};
|
||||
|
||||
// addEventListener wrapper:
|
||||
window.$on = function (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
|
||||
window.$delegate = function (target, selector, type, handler) {
|
||||
function dispatchEvent(event) {
|
||||
var targetElement = event.target;
|
||||
var potentialElements = window.qsa(selector, target);
|
||||
var hasMatch = Array.prototype.indexOf.call(potentialElements, targetElement) >= 0;
|
||||
|
||||
if (hasMatch)
|
||||
handler.call(targetElement, event);
|
||||
}
|
||||
|
||||
// https://developer.mozilla.org/en-US/docs/Web/Events/blur
|
||||
var useCapture = type === "blur" || type === "focus";
|
||||
|
||||
window.$on(target, type, dispatchEvent, useCapture);
|
||||
};
|
||||
|
||||
// Find the element's parent with the given tag name:
|
||||
// $parent(qs('a'), 'div');
|
||||
window.$parent = function (element, tagName) {
|
||||
if (!element.parentNode)
|
||||
return undefined;
|
||||
|
||||
if (element.parentNode.tagName.toLowerCase() === tagName.toLowerCase())
|
||||
return element.parentNode;
|
||||
|
||||
return window.$parent(element.parentNode, tagName);
|
||||
};
|
||||
|
||||
// Allow for looping on nodes by chaining:
|
||||
// qsa('.foo').forEach(function () {})
|
||||
NodeList.prototype.forEach = Array.prototype.forEach;
|
||||
})(window);
|
||||
119
experimental/todomvc-localstorage/src/model.js
Normal file
119
experimental/todomvc-localstorage/src/model.js
Normal file
|
|
@ -0,0 +1,119 @@
|
|||
(function (window) {
|
||||
"use strict";
|
||||
|
||||
/**
|
||||
* Creates a new Model instance and hooks up the storage.
|
||||
*
|
||||
* @constructor
|
||||
* @param {object} storage A reference to the client side storage class
|
||||
*/
|
||||
function Model(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
|
||||
*/
|
||||
Model.prototype.create = function (title, callback) {
|
||||
title = title || "";
|
||||
callback = callback || function () {};
|
||||
|
||||
var 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' });
|
||||
*/
|
||||
Model.prototype.read = function (query, callback) {
|
||||
var queryType = typeof query;
|
||||
callback = callback || function () {};
|
||||
|
||||
if (queryType === "function") {
|
||||
callback = query;
|
||||
return this.storage.findAll(callback);
|
||||
} else if (queryType === "string" || queryType === "number") {
|
||||
query = parseInt(query, 10);
|
||||
return this.storage.find({ id: query }, callback);
|
||||
} else {
|
||||
return 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.
|
||||
*/
|
||||
Model.prototype.update = function (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.
|
||||
*/
|
||||
Model.prototype.remove = function (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.
|
||||
*/
|
||||
Model.prototype.removeAll = function (callback) {
|
||||
this.storage.drop(callback);
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns a count of all todos
|
||||
*/
|
||||
Model.prototype.getCount = function (callback) {
|
||||
var todos = {
|
||||
active: 0,
|
||||
completed: 0,
|
||||
total: 0,
|
||||
};
|
||||
|
||||
this.storage.findAll(function (data) {
|
||||
data.forEach(function (todo) {
|
||||
if (todo.completed)
|
||||
todos.completed++;
|
||||
else
|
||||
todos.active++;
|
||||
|
||||
todos.total++;
|
||||
});
|
||||
callback(todos);
|
||||
});
|
||||
};
|
||||
|
||||
// Export to window
|
||||
window.app = window.app || {};
|
||||
window.app.Model = Model;
|
||||
})(window);
|
||||
177
experimental/todomvc-localstorage/src/store.js
Normal file
177
experimental/todomvc-localstorage/src/store.js
Normal file
|
|
@ -0,0 +1,177 @@
|
|||
/* jshint eqeqeq:false */
|
||||
(function (window) {
|
||||
"use strict";
|
||||
|
||||
var MemoryStorage = {
|
||||
_data: {},
|
||||
_name: "",
|
||||
init: function (name, data = {}) {
|
||||
this._name = name;
|
||||
if (!this.getData())
|
||||
this.setData(data);
|
||||
},
|
||||
getData: function () {
|
||||
const data = this._data[this._name];
|
||||
if (!data)
|
||||
return null;
|
||||
|
||||
return JSON.parse(data);
|
||||
},
|
||||
setData: function (value) {
|
||||
this._data[this._name] = JSON.stringify(value);
|
||||
},
|
||||
};
|
||||
|
||||
var LocalStorage = {
|
||||
_name: "",
|
||||
init: function (name, data = {}) {
|
||||
this._name = name;
|
||||
if (!this.getData())
|
||||
this.setData(data);
|
||||
},
|
||||
getData: function () {
|
||||
const data = window.localStorage.getItem(this._name);
|
||||
if (!data)
|
||||
return null;
|
||||
|
||||
return JSON.parse(data);
|
||||
},
|
||||
setData: function (value) {
|
||||
window.localStorage.setItem(this._name, JSON.stringify(value));
|
||||
},
|
||||
};
|
||||
|
||||
var ID = 1;
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
function Store(name, callback, type = "disk") {
|
||||
callback = callback || function () {};
|
||||
|
||||
this._dbName = name;
|
||||
this.storage = type === "memory" ? MemoryStorage : LocalStorage;
|
||||
|
||||
this.storage.init(name, []);
|
||||
|
||||
callback.call(this, this.storage.getData());
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
* });
|
||||
*/
|
||||
Store.prototype.find = function (query, callback) {
|
||||
if (!callback)
|
||||
return;
|
||||
|
||||
var todos = this.storage.getData();
|
||||
|
||||
callback.call(
|
||||
this,
|
||||
todos.filter(function (todo) {
|
||||
for (var 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
|
||||
*/
|
||||
Store.prototype.findAll = function (callback) {
|
||||
callback = callback || function () {};
|
||||
callback.call(this, this.storage.getData());
|
||||
};
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
Store.prototype.save = function (updateData, callback, id) {
|
||||
var todos = this.storage.getData();
|
||||
|
||||
callback = callback || function () {};
|
||||
|
||||
// If an ID was actually given, find the item and update each property
|
||||
if (id) {
|
||||
for (var i = 0; i < todos.length; i++) {
|
||||
if (todos[i].id === id) {
|
||||
for (var key in updateData)
|
||||
todos[i][key] = updateData[key];
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
this.storage.setData(todos);
|
||||
callback.call(this, todos);
|
||||
} else {
|
||||
// Generate an ID
|
||||
updateData.id = ID++;
|
||||
|
||||
todos.push(updateData);
|
||||
this.storage.setData(todos);
|
||||
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
|
||||
*/
|
||||
Store.prototype.remove = function (id, callback) {
|
||||
var todos = this.storage.getData();
|
||||
|
||||
for (var i = 0; i < todos.length; i++) {
|
||||
if (todos[i].id === id) {
|
||||
todos.splice(i, 1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
this.storage.setData(todos);
|
||||
callback.call(this, todos);
|
||||
};
|
||||
|
||||
/**
|
||||
* Will drop all storage and start fresh
|
||||
*
|
||||
* @param {function} callback The callback to fire after dropping the data
|
||||
*/
|
||||
Store.prototype.drop = function (callback) {
|
||||
var data = [];
|
||||
this.storage.setData(data);
|
||||
callback.call(this, data);
|
||||
};
|
||||
|
||||
// Export to window
|
||||
window.app = window.app || {};
|
||||
window.app.Store = Store;
|
||||
})(window);
|
||||
112
experimental/todomvc-localstorage/src/template.js
Normal file
112
experimental/todomvc-localstorage/src/template.js
Normal file
|
|
@ -0,0 +1,112 @@
|
|||
/* jshint laxbreak:true */
|
||||
(function (window) {
|
||||
"use strict";
|
||||
|
||||
var htmlEscapes = {
|
||||
"&": "&",
|
||||
"<": "<",
|
||||
">": ">",
|
||||
'"': """,
|
||||
"'": "'",
|
||||
"`": "`",
|
||||
};
|
||||
|
||||
var escapeHtmlChar = function (chr) {
|
||||
return htmlEscapes[chr];
|
||||
};
|
||||
|
||||
var reUnescapedHtml = /[&<>"'`]/g;
|
||||
var reHasUnescapedHtml = new RegExp(reUnescapedHtml.source);
|
||||
|
||||
var escape = function (string) {
|
||||
return string && reHasUnescapedHtml.test(string) ? string.replace(reUnescapedHtml, escapeHtmlChar) : string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Sets up defaults for all the Template methods such as a default template
|
||||
*
|
||||
* @constructor
|
||||
*/
|
||||
function Template() {
|
||||
this.defaultTemplate
|
||||
= '<li data-id="{{id}}" class="{{completed}}" data-priority="{{priority}}">'
|
||||
+ '<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,
|
||||
* });
|
||||
*/
|
||||
Template.prototype.show = function (data) {
|
||||
var i, l;
|
||||
var view = "";
|
||||
|
||||
for (i = 0, l = data.length; i < l; i++) {
|
||||
var template = this.defaultTemplate;
|
||||
var completed = "";
|
||||
var checked = "";
|
||||
|
||||
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);
|
||||
template = template.replace("{{priority}}", 4 - (i % 5));
|
||||
|
||||
view = 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
|
||||
*/
|
||||
Template.prototype.itemCounter = function (activeTodos) {
|
||||
var 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
|
||||
*/
|
||||
Template.prototype.clearCompletedButton = function (completedTodos) {
|
||||
if (completedTodos > 0)
|
||||
return "Clear completed";
|
||||
else
|
||||
return "";
|
||||
};
|
||||
|
||||
// Export to window
|
||||
window.app = window.app || {};
|
||||
window.app.Template = Template;
|
||||
})(window);
|
||||
215
experimental/todomvc-localstorage/src/view.js
Normal file
215
experimental/todomvc-localstorage/src/view.js
Normal file
|
|
@ -0,0 +1,215 @@
|
|||
/* global qs, qsa, $on, $parent, $delegate */
|
||||
|
||||
(function (window) {
|
||||
"use strict";
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
function View(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.$toggleAllInput = qs(".toggle-all");
|
||||
this.$toggleAll = qs(".toggle-all-label");
|
||||
this.$newTodo = qs(".new-todo");
|
||||
}
|
||||
|
||||
View.prototype._removeItem = function (id) {
|
||||
var elem = qs(`[data-id="${id}"]`);
|
||||
|
||||
if (elem)
|
||||
this.$todoList.removeChild(elem);
|
||||
};
|
||||
|
||||
View.prototype._clearCompletedButton = function (completedCount, visible) {
|
||||
this.$clearCompleted.textContent = this.template.clearCompletedButton(completedCount);
|
||||
this.$clearCompleted.style.display = visible ? "block" : "none";
|
||||
};
|
||||
|
||||
View.prototype._setFilter = function (currentPage) {
|
||||
qs(".filters .selected").className = "";
|
||||
qs(`.filters [href="#/${currentPage}"]`).className = "selected";
|
||||
};
|
||||
|
||||
View.prototype._elementComplete = function (id, completed) {
|
||||
var 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;
|
||||
};
|
||||
|
||||
View.prototype._editItem = function (id, title) {
|
||||
var listItem = qs(`[data-id="${id}"]`);
|
||||
|
||||
if (!listItem)
|
||||
return;
|
||||
|
||||
listItem.className = `${listItem.className} editing`;
|
||||
|
||||
var input = document.createElement("input");
|
||||
input.className = "edit";
|
||||
|
||||
listItem.appendChild(input);
|
||||
input.focus();
|
||||
input.value = title;
|
||||
};
|
||||
|
||||
View.prototype._editItemDone = function (id, title) {
|
||||
var listItem = qs(`[data-id="${id}"]`);
|
||||
|
||||
if (!listItem)
|
||||
return;
|
||||
|
||||
var input = qs("input.edit", listItem);
|
||||
listItem.removeChild(input);
|
||||
|
||||
listItem.className = listItem.className.replace("editing", "");
|
||||
|
||||
qsa("label", listItem).forEach(function (label) {
|
||||
label.textContent = title;
|
||||
});
|
||||
};
|
||||
|
||||
View.prototype._replaceContentWithHtml = function (element, html) {
|
||||
const parsedDocument = new DOMParser().parseFromString(html, "text/html");
|
||||
element.replaceChildren(...parsedDocument.body.childNodes);
|
||||
};
|
||||
|
||||
View.prototype.render = function (viewCmd, parameter) {
|
||||
var self = this;
|
||||
var viewCommands = {
|
||||
showEntries: function () {
|
||||
self._replaceContentWithHtml(self.$todoList, self.template.show(parameter));
|
||||
},
|
||||
removeItem: function () {
|
||||
self._removeItem(parameter);
|
||||
},
|
||||
updateElementCount: function () {
|
||||
self._replaceContentWithHtml(self.$todoItemCounter, self.template.itemCounter(parameter));
|
||||
},
|
||||
clearCompletedButton: function () {
|
||||
self._clearCompletedButton(parameter.completed, parameter.visible);
|
||||
},
|
||||
contentBlockVisibility: function () {
|
||||
self.$main.style.display = self.$footer.style.display = parameter.visible ? "block" : "none";
|
||||
},
|
||||
toggleAll: function () {
|
||||
self.$toggleAll.checked = parameter.checked;
|
||||
},
|
||||
setFilter: function () {
|
||||
self._setFilter(parameter);
|
||||
},
|
||||
clearNewTodo: function () {
|
||||
self.$newTodo.value = "";
|
||||
},
|
||||
elementComplete: function () {
|
||||
self._elementComplete(parameter.id, parameter.completed);
|
||||
},
|
||||
editItem: function () {
|
||||
self._editItem(parameter.id, parameter.title);
|
||||
},
|
||||
editItemDone: function () {
|
||||
self._editItemDone(parameter.id, parameter.title);
|
||||
},
|
||||
};
|
||||
|
||||
viewCommands[viewCmd]();
|
||||
};
|
||||
|
||||
View.prototype._itemId = function (element) {
|
||||
var li = $parent(element, "li");
|
||||
return parseInt(li.dataset.id, 10);
|
||||
};
|
||||
|
||||
View.prototype._bindItemEditDone = function (handler) {
|
||||
var self = this;
|
||||
$delegate(self.$todoList, "li .edit", "blur", function () {
|
||||
if (!this.dataset.iscanceled) {
|
||||
handler({
|
||||
id: self._itemId(this),
|
||||
title: this.value,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
$delegate(self.$todoList, "li .edit", "keypress", function (event) {
|
||||
if (event.keyCode === self.ENTER_KEY) {
|
||||
// Remove the cursor from the input when you hit enter just like if it
|
||||
// were a real form
|
||||
this.blur();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
View.prototype._bindItemEditCancel = function (handler) {
|
||||
var self = this;
|
||||
$delegate(self.$todoList, "li .edit", "keyup", function (event) {
|
||||
if (event.keyCode === self.ESCAPE_KEY) {
|
||||
this.dataset.iscanceled = true;
|
||||
this.blur();
|
||||
|
||||
handler({ id: self._itemId(this) });
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
View.prototype.bind = function (event, handler) {
|
||||
var self = this;
|
||||
if (event === "newTodo") {
|
||||
$on(self.$newTodo, "change", function () {
|
||||
handler(self.$newTodo.value);
|
||||
});
|
||||
} else if (event === "removeCompleted") {
|
||||
$on(self.$clearCompleted, "click", function () {
|
||||
handler();
|
||||
});
|
||||
} else if (event === "toggleAll") {
|
||||
$on(self.$toggleAll, "click", function () {
|
||||
self.$toggleAllInput.click();
|
||||
handler({ completed: self.$toggleAllInput.checked });
|
||||
});
|
||||
} else if (event === "itemEdit") {
|
||||
$delegate(self.$todoList, "li label", "dblclick", function () {
|
||||
handler({ id: self._itemId(this) });
|
||||
});
|
||||
} else if (event === "itemRemove") {
|
||||
$delegate(self.$todoList, ".destroy", "click", function () {
|
||||
handler({ id: self._itemId(this) });
|
||||
});
|
||||
} else if (event === "itemToggle") {
|
||||
$delegate(self.$todoList, ".toggle", "click", function () {
|
||||
handler({
|
||||
id: self._itemId(this),
|
||||
completed: this.checked,
|
||||
});
|
||||
});
|
||||
} else if (event === "itemEditDone") {
|
||||
self._bindItemEditDone(handler);
|
||||
} else if (event === "itemEditCancel") {
|
||||
self._bindItemEditCancel(handler);
|
||||
}
|
||||
};
|
||||
|
||||
// Export to window
|
||||
window.app = window.app || {};
|
||||
window.app.View = View;
|
||||
})(window);
|
||||
128
index.html
Normal file
128
index.html
Normal file
|
|
@ -0,0 +1,128 @@
|
|||
<!DOCTYPE html>
|
||||
<html data-visible-section="home">
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
|
||||
<meta name="viewport" content="width=850" />
|
||||
<title>Speedometer 3.0</title>
|
||||
<link rel="stylesheet" href="resources/main.css" />
|
||||
<link rel="icon" href="resources/favicon.png" />
|
||||
<script src="resources/main.mjs" type="module"></script>
|
||||
</head>
|
||||
<body>
|
||||
<main>
|
||||
<section id="home">
|
||||
<a href="#home" class="logo">
|
||||
<img srcset="resources/logo@2x.png 2x" src="resources/logo.png" alt="Speedometer" />
|
||||
<div class="version next">next</div>
|
||||
</a>
|
||||
<div class="content">
|
||||
<p>Speedometer is a browser benchmark that measures the responsiveness of Web applications. It uses demo web applications to simulate user actions such as adding to-do items.</p>
|
||||
<p id="screen-size-warning">
|
||||
<strong>
|
||||
Your browser window is too small. For most accurate results, please make the view port size at least <span id="min-screen-width">850px</span> by <span id="min-screen-height">650px</span>.<br />
|
||||
It's currently <span id="screen-size"></span>.
|
||||
</strong>
|
||||
</p>
|
||||
</div>
|
||||
<div class="buttons">
|
||||
<div class="button-row">
|
||||
<button class="start-tests-button">Start Test</button>
|
||||
</div>
|
||||
<div class="button-row">
|
||||
<a href="about.html">About Speedometer</a>
|
||||
<a href="instructions.html">Test Instructions</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="running">
|
||||
<a href="#home" class="logo">
|
||||
<img srcset="resources/logo@2x.png 2x" src="resources/logo.png" alt="Speedometer" />
|
||||
<div class="version">3.0</div>
|
||||
</a>
|
||||
<div id="testContainer"></div>
|
||||
<div id="leakedIframes"></div>
|
||||
<div id="progress">
|
||||
<progress aria-label="Progress" id="progress-completed"></progress>
|
||||
</div>
|
||||
<div id="info">
|
||||
<div id="info-label"></div>
|
||||
<div id="info-progress"></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="summary" data-title="Results Summary" class="valid">
|
||||
<a href="#home" class="logo">
|
||||
<img srcset="resources/logo@2x.png 2x" src="resources/logo.png" alt="Speedometer" />
|
||||
<div class="version">3.0</div>
|
||||
</a>
|
||||
<h1>Score</h1>
|
||||
<div class="gauge">
|
||||
<div class="window"><div class="needle"></div></div>
|
||||
</div>
|
||||
<hr />
|
||||
<div id="result-number"></div>
|
||||
<div id="confidence-number"></div>
|
||||
<div id="invalid-score-text">
|
||||
One or more subtests produced no duration.<br />
|
||||
Please check your <a href="./instructions.html" target="_blank">browser settings</a> and re-run the benchmark.<br />
|
||||
</div>
|
||||
<div class="buttons">
|
||||
<div class="button-row">
|
||||
<a class="button" href="#details" id="show-details" title="Show detailed results data.">Details</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="details" data-title="Results Details">
|
||||
<a href="#home" class="logo">
|
||||
<img srcset="resources/logo@2x.png 2x" src="resources/logo.png" alt="Speedometer" />
|
||||
<div class="version">3.0</div>
|
||||
</a>
|
||||
<div class="section-grid">
|
||||
<h1 class="section-header">Detailed Results</h1>
|
||||
<div class="section-content all-metric-results">
|
||||
<div class="non-standard-params">
|
||||
<h2>Non-standard Parameters</h2>
|
||||
<p>
|
||||
Speedometer ran with non-standard parameters.<br />
|
||||
The results are likely not comparable to default runs.
|
||||
</p>
|
||||
<table id="non-standard-params-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Param</th>
|
||||
<th>Value</th>
|
||||
<th>Default</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody></tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="aggregated-metric-result">
|
||||
<h2>Aggregate Metric</h2>
|
||||
<div id="geomean-chart"></div>
|
||||
<h2>Test Metrics Overview</h2>
|
||||
<div id="tests-chart"></div>
|
||||
</div>
|
||||
<br />
|
||||
<h2>Test Metrics Details</h2>
|
||||
<div id="metrics-results"></div>
|
||||
</div>
|
||||
<div class="buttons section-footer">
|
||||
<div class="button-row">
|
||||
<a class="button" href="#summary" title="Go back to the simplified summary view.">Summary</a>
|
||||
</div>
|
||||
<div class="button-row export-buttons">
|
||||
<button id="copy-full-json" title="Copy full result metrics as json string.">Copy JSON</button>
|
||||
<a class="button" id="download-full-json" title="Download full result metrics as json string.">Download JSON</a>
|
||||
<a class="button" id="download-classic-json" title="Download backwards-compatible result metrics as json string.">Download Classic JSON</a>
|
||||
<a class="button" id="download-csv" title="Download all result metrics as CSV string.">Download CSV</a>
|
||||
<button id="copy-csv" title="Copy all result metrics as CSV string.">Copy CSV</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
42
instructions.html
Normal file
42
instructions.html
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
|
||||
<meta name="viewport" content="width=850" />
|
||||
<title>Speedometer 3.0 Test Instructions</title>
|
||||
<link rel="stylesheet" href="resources/main.css" />
|
||||
<link rel="icon" href="resources/favicon.png" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<main>
|
||||
<section id="instructions" class="visible" data-title="Test Instructions">
|
||||
<a href="./" class="logo">
|
||||
<img srcset="resources/logo@2x.png 2x" src="resources/logo.png" alt="Speedometer" />
|
||||
<div class="version">3.0</div>
|
||||
</a>
|
||||
<div class="section-grid">
|
||||
<h1 class="section-header">Speedometer 3 General Instructions</h1>
|
||||
<div class="section-content">
|
||||
<p>To get reasonably accurate results your device and browser need to be focused on the benchmark. Here are some basic instructions to allow that:</p>
|
||||
<ul class="compact">
|
||||
<li>Ensure you are running the latest stable browser version.</li>
|
||||
<li>Prefer using a separate clean browser profile — extensions and non-default browser settings can have a large impact on the score.</li>
|
||||
<li>Don't run any programs in the background.</li>
|
||||
<li>Restart the browser and close all other tabs and windows.</li>
|
||||
<li>Keep the Speedometer page focused and do not interact with the device while running the benchmark.</li>
|
||||
<li>Keep your battery-powered device plugged into a charger.</li>
|
||||
<li>Some devices might need time to cool down between consecutive runs.</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="buttons section-footer">
|
||||
<div class="button-row">
|
||||
<a class="button" href="./" title="Show main section.">Home</a>
|
||||
<a class="button" href="about.html" title="Show about section.">About</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
7
netlify.toml
Normal file
7
netlify.toml
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
[[headers]]
|
||||
for = "/*"
|
||||
[headers.values]
|
||||
cross-origin-embedder-policy = "require-corp"
|
||||
cross-origin-opener-policy = "same-origin"
|
||||
X-Frame-Options = "SAMEORIGIN"
|
||||
x-content-type-options = "nosniff"
|
||||
11542
package-lock.json
generated
Normal file
11542
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
56
package.json
Normal file
56
package.json
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
{
|
||||
"name": "speedometer",
|
||||
"version": "3.0.0-alpha",
|
||||
"description": "An open source repository for the Speedometer benchmark.",
|
||||
"engines": {
|
||||
"node": ">=22.0.0",
|
||||
"npm": ">=8.19.3"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/WebKit/Speedometer.git"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/WebKit/Speedometer/issues"
|
||||
},
|
||||
"license": "SEE LICENSE IN LICENSE",
|
||||
"scripts": {
|
||||
"dev": "http-server ./ -c-1 --cors",
|
||||
"server": "http-server ./ --cors",
|
||||
"lint:check": "eslint **/*.{js,mjs,jsx,ts,tsx}",
|
||||
"lint:fix": "eslint \"**/*.{js,mjs,jsx,ts,tsx}\" --fix",
|
||||
"pretty:check": "prettier --check ./",
|
||||
"pretty:fix": "prettier --write ./",
|
||||
"format": "npm run pretty:fix && npm run lint:fix",
|
||||
"test:chrome": "node tests/run-unittests.mjs --browser chrome",
|
||||
"test:firefox": "node tests/run-unittests.mjs --browser firefox",
|
||||
"test:safari": "node tests/run-unittests.mjs --browser safari",
|
||||
"test:edge": "node tests/run-unittests.mjs --browser edge",
|
||||
"test-e2e:chrome": "node tests/run-end2end.mjs --browser chrome",
|
||||
"test-e2e:firefox": "node tests/run-end2end.mjs --browser firefox",
|
||||
"test-e2e:safari": "node tests/run-end2end.mjs --browser safari",
|
||||
"test-e2e:edge": "node tests/run-end2end.mjs --browser edge"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.21.3",
|
||||
"@babel/eslint-parser": "^7.21.3",
|
||||
"@babel/plugin-proposal-decorators": "^7.21.0",
|
||||
"@nuxt/eslint-config": "^0.1.1",
|
||||
"@next/eslint-plugin-next": "^13.4.5",
|
||||
"@typescript-eslint/eslint-plugin": "^5.59.0",
|
||||
"@typescript-eslint/parser": "^5.59.0",
|
||||
"command-line-args": "^5.2.1",
|
||||
"command-line-usage": "^6.1.3",
|
||||
"eslint": "^8.38.0",
|
||||
"eslint-plugin-ember": "^11.4.8",
|
||||
"eslint-plugin-react": "^7.32.2",
|
||||
"eslint-plugin-vue": "^9.10.0",
|
||||
"expect.js": "^0.3.1",
|
||||
"http-server": "^14.1.1",
|
||||
"mocha": "^10.2.0",
|
||||
"prettier": "^2.8.3",
|
||||
"selenium-webdriver": "^4.27.0",
|
||||
"sinon": "^17.0.1",
|
||||
"typescript": "^5.0.4"
|
||||
}
|
||||
}
|
||||
518
resources/benchmark-runner.mjs
Normal file
518
resources/benchmark-runner.mjs
Normal file
|
|
@ -0,0 +1,518 @@
|
|||
import { Metric } from "./metric.mjs";
|
||||
import { params } from "./shared/params.mjs";
|
||||
import { SUITE_RUNNER_LOOKUP } from "./suite-runner.mjs";
|
||||
|
||||
const performance = globalThis.performance;
|
||||
|
||||
export class BenchmarkTestStep {
|
||||
constructor(testName, testFunction) {
|
||||
this.name = testName;
|
||||
this.run = testFunction;
|
||||
}
|
||||
}
|
||||
|
||||
function getParent(lookupStartNode, path) {
|
||||
const parent = path.reduce((root, selector) => {
|
||||
const node = root.querySelector(selector);
|
||||
return node.shadowRoot ?? node;
|
||||
}, lookupStartNode);
|
||||
|
||||
return parent;
|
||||
}
|
||||
|
||||
class Page {
|
||||
constructor(frame) {
|
||||
this._frame = frame;
|
||||
}
|
||||
|
||||
getLocalStorage() {
|
||||
return this._frame.contentWindow.localStorage;
|
||||
}
|
||||
|
||||
layout() {
|
||||
const body = this._frame.contentDocument.body.getBoundingClientRect();
|
||||
this.layout.e = document.elementFromPoint((body.width / 2) | 0, (body.height / 2) | 0);
|
||||
}
|
||||
|
||||
async waitForElement(selector) {
|
||||
return new Promise((resolve) => {
|
||||
const resolveIfReady = () => {
|
||||
const element = this.querySelector(selector);
|
||||
let callback = resolveIfReady;
|
||||
if (element)
|
||||
callback = () => resolve(element);
|
||||
window.requestAnimationFrame(callback);
|
||||
};
|
||||
resolveIfReady();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the first element within the document that matches the specified selector, or group of selectors.
|
||||
* If no matches are found, null is returned.
|
||||
*
|
||||
* An optional path param is added to be able to target elements within a shadow DOM or nested shadow DOMs.
|
||||
*
|
||||
* @example
|
||||
* // DOM structure: <todo-app> -> #shadow-root -> <todo-list> -> #shadow-root -> <todo-item>
|
||||
* // return PageElement(<todo-item>)
|
||||
* querySelector("todo-item", ["todo-app", "todo-list"]);
|
||||
*
|
||||
* @param {string} selector A string containing one or more selectors to match.
|
||||
* @param {string[]} [path] An array containing a path to the parent element.
|
||||
* @returns PageElement | null
|
||||
*/
|
||||
querySelector(selector, path = []) {
|
||||
const lookupStartNode = this._frame.contentDocument;
|
||||
const element = getParent(lookupStartNode, path).querySelector(selector);
|
||||
|
||||
if (element === null)
|
||||
return null;
|
||||
return this._wrapElement(element);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all elements within the document that matches the specified selector, or group of selectors.
|
||||
* If no matches are found, null is returned.
|
||||
*
|
||||
* An optional path param is added to be able to target elements within a shadow DOM or nested shadow DOMs.
|
||||
*
|
||||
* @example
|
||||
* // DOM structure: <todo-app> -> #shadow-root -> <todo-list> -> #shadow-root -> <todo-item>
|
||||
* // return [PageElement(<todo-item>), PageElement(<todo-item>)]
|
||||
* querySelectorAll("todo-item", ["todo-app", "todo-list"]);
|
||||
*
|
||||
* @param {string} selector A string containing one or more selectors to match.
|
||||
* @param {string[]} [path] An array containing a path to the parent element.
|
||||
* @returns array
|
||||
*/
|
||||
querySelectorAll(selector, path = []) {
|
||||
const lookupStartNode = this._frame.contentDocument;
|
||||
const elements = Array.from(getParent(lookupStartNode, path).querySelectorAll(selector));
|
||||
for (let i = 0; i < elements.length; i++)
|
||||
elements[i] = this._wrapElement(elements[i]);
|
||||
return elements;
|
||||
}
|
||||
|
||||
getElementById(id) {
|
||||
const element = this._frame.contentDocument.getElementById(id);
|
||||
if (element === null)
|
||||
return null;
|
||||
return this._wrapElement(element);
|
||||
}
|
||||
|
||||
call(functionName) {
|
||||
this._frame.contentWindow[functionName]();
|
||||
return null;
|
||||
}
|
||||
|
||||
callAsync(functionName) {
|
||||
setTimeout(() => {
|
||||
this._frame.contentWindow[functionName]();
|
||||
}, 0);
|
||||
}
|
||||
|
||||
callToGetElement(functionName) {
|
||||
return this._wrapElement(this._frame.contentWindow[functionName]());
|
||||
}
|
||||
|
||||
_wrapElement(element) {
|
||||
return new PageElement(element);
|
||||
}
|
||||
}
|
||||
|
||||
const NATIVE_OPTIONS = {
|
||||
bubbles: true,
|
||||
cancellable: true,
|
||||
};
|
||||
|
||||
class PageElement {
|
||||
#node;
|
||||
|
||||
constructor(node) {
|
||||
this.#node = node;
|
||||
}
|
||||
|
||||
setValue(value) {
|
||||
this.#node.value = value;
|
||||
}
|
||||
|
||||
click() {
|
||||
this.#node.click();
|
||||
}
|
||||
|
||||
focus() {
|
||||
this.#node.focus();
|
||||
}
|
||||
|
||||
getElementByMethod(name) {
|
||||
return new PageElement(this.#node[name]());
|
||||
}
|
||||
|
||||
dispatchEvent(eventName, options = NATIVE_OPTIONS, eventType = Event) {
|
||||
if (eventName === "submit")
|
||||
// FIXME FireFox doesn't like `new Event('submit')
|
||||
this._dispatchSubmitEvent();
|
||||
else
|
||||
this.#node.dispatchEvent(new eventType(eventName, options));
|
||||
}
|
||||
|
||||
_dispatchSubmitEvent() {
|
||||
const submitEvent = document.createEvent("Event");
|
||||
submitEvent.initEvent("submit", true, true);
|
||||
this.#node.dispatchEvent(submitEvent);
|
||||
}
|
||||
|
||||
enter(type, options = undefined) {
|
||||
const ENTER_KEY_CODE = 13;
|
||||
return this.dispatchKeyEvent(type, ENTER_KEY_CODE, "Enter", options);
|
||||
}
|
||||
|
||||
dispatchKeyEvent(type, keyCode, key, options) {
|
||||
let eventOptions = { bubbles: true, cancelable: true, keyCode, which: keyCode, key };
|
||||
if (options !== undefined)
|
||||
eventOptions = Object.assign(eventOptions, options);
|
||||
const event = new KeyboardEvent(type, eventOptions);
|
||||
this.#node.dispatchEvent(event);
|
||||
}
|
||||
|
||||
dispatchMouseEvent(type, offsetX, offsetY, options) {
|
||||
const boundingRect = this.#node.getBoundingClientRect();
|
||||
const clientX = offsetX + boundingRect.x;
|
||||
const clientY = offsetY + boundingRect.y;
|
||||
const contentWindow = this.#node.ownerDocument.defaultView;
|
||||
const screenX = clientX + contentWindow.screenX;
|
||||
const screenY = clientY + contentWindow.screenY;
|
||||
let eventOptions = { bubbles: true, cancelable: true, clientX, clientY, screenX, screenY };
|
||||
if (options !== undefined)
|
||||
eventOptions = Object.assign(eventOptions, options);
|
||||
const event = new contentWindow.MouseEvent(type, eventOptions);
|
||||
this.#node.dispatchEvent(event);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the first element found in a node of a PageElement that matches the specified selector, or group of selectors. If a shadow DOM is present in the node, the shadow DOM is used to query.
|
||||
* If no matches are found, null is returned.
|
||||
*
|
||||
* @param {string} selector A string containing one or more selectors to match.
|
||||
* @param {string[]} [path] An array containing a path to the parent element.
|
||||
* @returns PageElement | null
|
||||
*/
|
||||
querySelectorInShadowRoot(selector, path = []) {
|
||||
const lookupStartNode = this.#node.shadowRoot ?? this.#node;
|
||||
const element = getParent(lookupStartNode, path).querySelector(selector);
|
||||
|
||||
if (element === null)
|
||||
return null;
|
||||
return new PageElement(element);
|
||||
}
|
||||
|
||||
querySelector(selector) {
|
||||
const element = this.#node.querySelector(selector);
|
||||
|
||||
if (element === null)
|
||||
return null;
|
||||
return new PageElement(element);
|
||||
}
|
||||
}
|
||||
|
||||
function geomeanToScore(geomean) {
|
||||
return 1000 / geomean;
|
||||
}
|
||||
|
||||
// The WarmupSuite is used to make sure all runner helper functions and
|
||||
// classes are compiled, to avoid unnecessary pauses due to delayed
|
||||
// compilation of runner methods in the middle of the measuring cycle.
|
||||
export const WarmupSuite = {
|
||||
name: "Warmup",
|
||||
url: "warmup/index.html",
|
||||
async prepare(page) {
|
||||
await page.waitForElement("#testItem");
|
||||
},
|
||||
tests: [
|
||||
// Make sure to run ever page.method once at least
|
||||
new BenchmarkTestStep("WarmingUpPageMethods", (page) => {
|
||||
let results = [];
|
||||
results.push(page.querySelector(".testItem"));
|
||||
results.push(page.querySelectorAll(".item"));
|
||||
results.push(page.getElementById("testItem"));
|
||||
}),
|
||||
new BenchmarkTestStep("WarmingUpPageElementMethods", (page) => {
|
||||
const item = page.getElementById("testItem");
|
||||
item.setValue("value");
|
||||
item.click();
|
||||
item.focus();
|
||||
item.dispatchEvent("change");
|
||||
item.enter("keypress");
|
||||
item.dispatchEvent("input");
|
||||
item.enter("keyup");
|
||||
}),
|
||||
new BenchmarkTestStep("WarmingUpPageElementMouseMethods", (page) => {
|
||||
const item = page.getElementById("testItem");
|
||||
const mouseEventOptions = { clientX: 100, clientY: 100, bubbles: true, cancelable: true };
|
||||
const wheelEventOptions = {
|
||||
clientX: 200,
|
||||
clientY: 200,
|
||||
deltaMode: 0,
|
||||
delta: -10,
|
||||
deltaY: -10,
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
};
|
||||
item.dispatchEvent("mousedown", mouseEventOptions, MouseEvent);
|
||||
item.dispatchEvent("mousemove", mouseEventOptions, MouseEvent);
|
||||
item.dispatchEvent("mouseup", mouseEventOptions, MouseEvent);
|
||||
item.dispatchEvent("wheel", wheelEventOptions, WheelEvent);
|
||||
}),
|
||||
],
|
||||
};
|
||||
|
||||
// https://stackoverflow.com/a/47593316
|
||||
function seededHashRandomNumberGenerator(a) {
|
||||
return function () {
|
||||
var t = a += 0x6d2b79f5;
|
||||
t = Math.imul(t ^ (t >>> 15), t | 1);
|
||||
t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
|
||||
return (t ^ (t >>> 14)) >>> 0;
|
||||
};
|
||||
}
|
||||
|
||||
class WakeLock {
|
||||
#wakeLockSentinel = undefined;
|
||||
async request() {
|
||||
if (!navigator.wakeLock)
|
||||
return;
|
||||
try {
|
||||
this.#wakeLockSentinel = await navigator.wakeLock.request("screen");
|
||||
} catch (err) {
|
||||
console.error(`${err.name}, ${err.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
async release() {
|
||||
if (!this.#wakeLockSentinel)
|
||||
return;
|
||||
try {
|
||||
await this.#wakeLockSentinel.release();
|
||||
} catch (err) {
|
||||
console.error(`${err.name}, ${err.message}`);
|
||||
} finally {
|
||||
this.#wakeLockSentinel = undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class BenchmarkRunner {
|
||||
constructor(suites, client) {
|
||||
this._suites = suites;
|
||||
if (params.useWarmupSuite)
|
||||
this._suites = [WarmupSuite, ...suites];
|
||||
this._client = client;
|
||||
this._page = null;
|
||||
this._metrics = null;
|
||||
this._iterationCount = params.iterationCount;
|
||||
if (params.shuffleSeed !== "off")
|
||||
this._suiteOrderRandomNumberGenerator = seededHashRandomNumberGenerator(params.shuffleSeed);
|
||||
this._wakeLock = new WakeLock();
|
||||
}
|
||||
|
||||
async runMultipleIterations(iterationCount) {
|
||||
this._iterationCount = iterationCount;
|
||||
if (this._client?.willStartFirstIteration)
|
||||
await this._client.willStartFirstIteration(iterationCount);
|
||||
|
||||
try {
|
||||
await this._runMultipleIterations();
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
if (this._client?.handleError) {
|
||||
await this._client.handleError(error);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (this._client?.didFinishLastIteration)
|
||||
await this._client.didFinishLastIteration(this._metrics);
|
||||
}
|
||||
|
||||
async _runMultipleIterations() {
|
||||
const iterationStartLabel = "iteration-start";
|
||||
const iterationEndLabel = "iteration-end";
|
||||
for (let i = 0; i < this._iterationCount; i++) {
|
||||
performance.mark(iterationStartLabel);
|
||||
await this.runAllSuites();
|
||||
performance.mark(iterationEndLabel);
|
||||
performance.measure(`iteration-${i}`, iterationStartLabel, iterationEndLabel);
|
||||
}
|
||||
}
|
||||
|
||||
_removeFrame() {
|
||||
if (this._frame) {
|
||||
if (params.leakyIframes)
|
||||
document.getElementById("leakedIframes").append(this._frame);
|
||||
else
|
||||
this._frame.parentNode.removeChild(this._frame);
|
||||
this._frame = null;
|
||||
}
|
||||
}
|
||||
|
||||
async _appendFrame() {
|
||||
const frame = document.createElement("iframe");
|
||||
const style = frame.style;
|
||||
style.width = `${params.viewport.width}px`;
|
||||
style.height = `${params.viewport.height}px`;
|
||||
style.border = "0px none";
|
||||
style.position = "absolute";
|
||||
frame.setAttribute("scrolling", "no");
|
||||
frame.className = "test-runner";
|
||||
style.left = "50%";
|
||||
style.top = "50%";
|
||||
style.transform = "translate(-50%, -50%)";
|
||||
|
||||
if (this._client?.willAddTestFrame)
|
||||
await this._client.willAddTestFrame(frame);
|
||||
|
||||
document.body.insertBefore(frame, document.body.firstChild);
|
||||
this._frame = frame;
|
||||
return frame;
|
||||
}
|
||||
|
||||
async _prepareAllSuites() {
|
||||
this._measuredValues = { tests: {}, total: 0, mean: NaN, geomean: NaN, score: NaN };
|
||||
await this._wakeLock.request();
|
||||
|
||||
const prepareStartLabel = "runner-prepare-start";
|
||||
const prepareEndLabel = "runner-prepare-end";
|
||||
|
||||
performance.mark(prepareStartLabel);
|
||||
let suites = [...this._suites];
|
||||
if (this._suiteOrderRandomNumberGenerator)
|
||||
this._shuffleSuites(suites);
|
||||
|
||||
performance.mark(prepareEndLabel);
|
||||
performance.measure("runner-prepare", prepareStartLabel, prepareEndLabel);
|
||||
|
||||
return suites;
|
||||
}
|
||||
|
||||
_shuffleSuites(suites) {
|
||||
// We just do a simple Fisher-Yates shuffle based on the repeated hash of the
|
||||
// seed. This is not a high quality RNG, but it's plenty good enough.
|
||||
for (let i = 0; i < suites.length - 1; i++) {
|
||||
const j = i + (this._suiteOrderRandomNumberGenerator() % (suites.length - i));
|
||||
const tmp = suites[i];
|
||||
suites[i] = suites[j];
|
||||
suites[j] = tmp;
|
||||
}
|
||||
}
|
||||
|
||||
async runAllSuites() {
|
||||
const suites = await this._prepareAllSuites();
|
||||
try {
|
||||
for (const suite of suites) {
|
||||
if (suite.disabled)
|
||||
continue;
|
||||
|
||||
try {
|
||||
await this._appendFrame();
|
||||
this._page = new Page(this._frame);
|
||||
await this.runSuite(suite);
|
||||
} finally {
|
||||
this._removeFrame();
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
await this._finishRunAllSuites();
|
||||
}
|
||||
}
|
||||
|
||||
async _finishRunAllSuites() {
|
||||
const finalizeStartLabel = "runner-finalize-start";
|
||||
const finalizeEndLabel = "runner-finalize-end";
|
||||
|
||||
performance.mark(finalizeStartLabel);
|
||||
await this._finalize();
|
||||
performance.mark(finalizeEndLabel);
|
||||
performance.measure("runner-finalize", finalizeStartLabel, finalizeEndLabel);
|
||||
await this._wakeLock.release();
|
||||
}
|
||||
|
||||
async runSuite(suite) {
|
||||
// FIXME: Encapsulate more state in the SuiteRunner.
|
||||
// FIXME: Return and use measured values from SuiteRunner.
|
||||
const type = suite.type ?? ((params.useAsyncSteps && "async") || "default");
|
||||
const suiteRunnerClass = SUITE_RUNNER_LOOKUP[type];
|
||||
const suiteRunner = new suiteRunnerClass(this._frame, this._page, params, suite, this._client, this._measuredValues);
|
||||
await suiteRunner.run();
|
||||
}
|
||||
|
||||
async _finalize() {
|
||||
this._appendIterationMetrics();
|
||||
if (this._client?.didRunSuites) {
|
||||
let product = 1;
|
||||
const values = [];
|
||||
for (const suiteName in this._measuredValues.tests) {
|
||||
const suiteTotal = this._measuredValues.tests[suiteName].total;
|
||||
product *= suiteTotal;
|
||||
values.push(suiteTotal);
|
||||
}
|
||||
|
||||
values.sort((a, b) => a - b); // Avoid the loss of significance for the sum.
|
||||
const total = values.reduce((a, b) => a + b);
|
||||
const geomean = Math.pow(product, 1 / values.length);
|
||||
|
||||
this._measuredValues.total = total;
|
||||
this._measuredValues.mean = total / values.length;
|
||||
this._measuredValues.geomean = geomean;
|
||||
this._measuredValues.score = geomeanToScore(geomean);
|
||||
await this._client.didRunSuites(this._measuredValues);
|
||||
}
|
||||
}
|
||||
|
||||
_appendIterationMetrics() {
|
||||
const getMetric = (name, unit = "ms") => this._metrics[name] || (this._metrics[name] = new Metric(name, unit));
|
||||
const iterationTotalMetric = (i) => {
|
||||
if (i >= params.iterationCount)
|
||||
throw new Error(`Requested iteration=${i} does not exist.`);
|
||||
return getMetric(`Iteration-${i}-Total`);
|
||||
};
|
||||
|
||||
const collectSubMetrics = (prefix, items, parent) => {
|
||||
for (let name in items) {
|
||||
const results = items[name];
|
||||
const metric = getMetric(prefix + name);
|
||||
metric.add(results.total ?? results);
|
||||
if (metric.parent !== parent)
|
||||
parent.addChild(metric);
|
||||
if (results.tests)
|
||||
collectSubMetrics(`${metric.name}${Metric.separator}`, results.tests, metric);
|
||||
}
|
||||
};
|
||||
const initializeMetrics = this._metrics === null;
|
||||
if (initializeMetrics)
|
||||
this._metrics = { __proto__: null };
|
||||
|
||||
const iterationResults = this._measuredValues.tests;
|
||||
collectSubMetrics("", iterationResults);
|
||||
|
||||
if (initializeMetrics) {
|
||||
// Prepare all iteration metrics so they are listed at the end of
|
||||
// of the _metrics object, before "Total" and "Score".
|
||||
for (let i = 0; i < this._iterationCount; i++)
|
||||
iterationTotalMetric(i).description = `Test totals for iteration ${i}`;
|
||||
getMetric("Geomean", "ms").description = "Geomean of test totals";
|
||||
getMetric("Score", "score").description = "Scaled inverse of the Geomean";
|
||||
}
|
||||
|
||||
const geomean = getMetric("Geomean");
|
||||
const iterationTotal = iterationTotalMetric(geomean.length);
|
||||
for (const results of Object.values(iterationResults))
|
||||
iterationTotal.add(results.total);
|
||||
iterationTotal.computeAggregatedMetrics();
|
||||
geomean.add(iterationTotal.geomean);
|
||||
getMetric("Score").add(geomeanToScore(iterationTotal.geomean));
|
||||
|
||||
for (const metric of Object.values(this._metrics))
|
||||
metric.computeAggregatedMetrics();
|
||||
}
|
||||
}
|
||||
78
resources/charts/README.md
Normal file
78
resources/charts/README.md
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
# Chart benchmarks
|
||||
|
||||
## How to run locally
|
||||
|
||||
This project uses `npm` to manage dependencies and run scripts. It has been
|
||||
tested with node v18 but should work with other versions of node.
|
||||
|
||||
This uses [vite](https://vitejs.dev/) as builder but this should be fairly
|
||||
transparent.
|
||||
|
||||
### Install dependencies
|
||||
|
||||
```
|
||||
npm i
|
||||
```
|
||||
|
||||
### Run the development server
|
||||
|
||||
```
|
||||
npm run dev
|
||||
```
|
||||
|
||||
### Build the app
|
||||
|
||||
```
|
||||
npm run build
|
||||
```
|
||||
|
||||
### Preview the production build
|
||||
|
||||
```
|
||||
npm run preview
|
||||
```
|
||||
|
||||
## Included benchmarks
|
||||
|
||||
All the benchmarks are included in iframes in the index page. But it may be more
|
||||
convenient to open these benchmarks with their specific files.
|
||||
|
||||
### [Observable Plot](https://github.com/observablehq/plot)
|
||||
|
||||
You can load this benchmark with the `/observable-plot.html` page, for example
|
||||
http://localhost:5173/observable-plot.html if you run it locally or
|
||||
http://localhost:8900/resources/charts/dist/observable-plot.html in the
|
||||
context of the speedometer.
|
||||
|
||||
Observable Plot is D3-based and outputs SVG.
|
||||
|
||||
When run in development mode, the page will automatically execute the 3
|
||||
included graphs:
|
||||
|
||||
- a stacked bar graph
|
||||
- a grouped bar graph
|
||||
- a graph using dots
|
||||
|
||||
In production mode nothing executes by default, the user needs to push the
|
||||
buttons to run any code. That's how the benchmark exercizes this code.
|
||||
|
||||
To build these graphs, we use datasets representing flight information in the
|
||||
US. You can consult them in the [datasets directory](./datasets).
|
||||
|
||||
### [ChartJS](https://github.com/chartjs/Chart.js)
|
||||
|
||||
You can load this benchmark with the `/chartjs.html` page, for example
|
||||
http://localhost:5173/chartjs.html if you run it locally or
|
||||
http://localhost:8900/resources/charts/dist/chartjs.html in the
|
||||
context of speedometer.
|
||||
|
||||
ChartJS is canvas-based.
|
||||
|
||||
When run in development mode, the page will automatically execute the scatter
|
||||
graph.
|
||||
|
||||
In production mode nothing executes by default, the user needs to push the
|
||||
buttons to run any code. That's how the benchmark exercizes this code.
|
||||
|
||||
To build these graphs, we use datasets representing flight information in the
|
||||
US. You can consult them in the [datasets directory](./datasets).
|
||||
33
resources/charts/chartjs.html
Normal file
33
resources/charts/chartjs.html
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Chart.js</title>
|
||||
<style type="text/css" media="all">
|
||||
label {
|
||||
user-select: none;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app">
|
||||
<button id="run-all" type="button">Run all scenario (development only)</button>
|
||||
<button id="prepare" type="button">Prepare data</button>
|
||||
<button id="add-scatter-chart-button" type="button">Draw a scatter chart</button>
|
||||
<button id="open-tooltip" type="button">Open tooltips</button>
|
||||
<button id="reset" type="button">Reset</button>
|
||||
<label><input type="checkbox" id="opaque-color" />Use opaque color for next draw</label>
|
||||
</div>
|
||||
<canvas id="chart"></canvas>
|
||||
<script>
|
||||
// This hack allows to capture the work normally happening in a rAF. We
|
||||
// may be able to remove it if the runner improves.
|
||||
window.requestAnimationFrame = (cb) => window.setTimeout(cb, 0);
|
||||
window.cancelAnimationFrame = window.clearTimeout;
|
||||
</script>
|
||||
<script type="module" src="/chartjs.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
174
resources/charts/chartjs.js
Normal file
174
resources/charts/chartjs.js
Normal file
|
|
@ -0,0 +1,174 @@
|
|||
import Chart from "chart.js/auto";
|
||||
import { csvParse } from "d3-dsv";
|
||||
|
||||
// These 2 imports use some vite machinery to directly import these files as
|
||||
// strings. Then they are constants and this reduces the load on the GC.
|
||||
import airportsString from "./datasets/airports.csv?raw";
|
||||
import flightsString from "./datasets/flights-airports.csv?raw";
|
||||
|
||||
/*
|
||||
* From https://www.omnicalculator.com/other/latitude-longitude-distance:
|
||||
* d = 2R × sin⁻¹(√[sin²((θ₂ - θ₁)/2) + cosθ₁ × cosθ₂ × sin²((φ₂ - φ₁)/2)])
|
||||
*
|
||||
* where:
|
||||
* θ₁, φ₁ – First point latitude and longitude coordinates;
|
||||
* θ₂, φ₂ – Second point latitude and longitude coordinates;
|
||||
* R – Earth's radius (R = 6371 km); and
|
||||
* d – Distance between them along Earth's surface.
|
||||
*/
|
||||
const R = 6371;
|
||||
function computeDistance(coords1, coords2) {
|
||||
const long1 = toRadian(coords1.longitude);
|
||||
const lat1 = toRadian(coords1.latitude);
|
||||
const long2 = toRadian(coords2.longitude);
|
||||
const lat2 = toRadian(coords2.latitude);
|
||||
const longSquareSin = Math.sin((long2 - long1) / 2) ** 2;
|
||||
const latSquareSin = Math.sin((lat2 - lat1) / 2) ** 2;
|
||||
const d = 2 * R * Math.asin(Math.sqrt(latSquareSin + Math.cos(lat1) * Math.cos(lat2) * longSquareSin));
|
||||
return d;
|
||||
}
|
||||
|
||||
const RAD = Math.PI / 180;
|
||||
function toRadian(deg) {
|
||||
return deg * RAD;
|
||||
}
|
||||
|
||||
let preparedData = null;
|
||||
function prepare() {
|
||||
/**
|
||||
* AirportInformation: { state, iata, name, city, country, latitude, longitude }
|
||||
* airports: Array<AirportInformation>
|
||||
* flights: Array<{ origin, destination, count }>
|
||||
*/
|
||||
const airports = csvParse(airportsString);
|
||||
const flights = csvParse(flightsString);
|
||||
const airportMap = new Map(airports.map((airport) => [airport.iata, airport]));
|
||||
|
||||
for (const flight of flights) {
|
||||
const origAirport = airportMap.get(flight.origin);
|
||||
const destAirport = airportMap.get(flight.destination);
|
||||
flight.distance = computeDistance(origAirport, destAirport);
|
||||
}
|
||||
|
||||
preparedData = { flights, airportMap };
|
||||
}
|
||||
|
||||
const ROOT = document.getElementById("chart");
|
||||
const opaqueCheckBox = document.getElementById("opaque-color");
|
||||
|
||||
let currentChart = null;
|
||||
function drawScattered(data) {
|
||||
if (!preparedData)
|
||||
throw new Error("Please prepare the data first.");
|
||||
|
||||
reset();
|
||||
|
||||
currentChart = new Chart(ROOT, {
|
||||
type: "scatter",
|
||||
data: {
|
||||
datasets: [
|
||||
{
|
||||
data: preparedData.flights,
|
||||
backgroundColor: opaqueCheckBox.checked ? "rgb(0, 125, 255)" : "rgba(0, 125, 255, .20)",
|
||||
},
|
||||
],
|
||||
},
|
||||
options: {
|
||||
animation: false,
|
||||
parsing: {
|
||||
xAxisKey: "distance",
|
||||
yAxisKey: "count",
|
||||
},
|
||||
plugins: {
|
||||
legend: {
|
||||
display: false,
|
||||
},
|
||||
title: {
|
||||
display: true,
|
||||
text: "Number of flights for a distance",
|
||||
position: "bottom",
|
||||
},
|
||||
tooltip: {
|
||||
displayColors: false,
|
||||
callbacks: {
|
||||
label: (item) => {
|
||||
const orig = preparedData.airportMap.get(item.raw.origin);
|
||||
const dest = preparedData.airportMap.get(item.raw.destination);
|
||||
const result = `✈ ${orig.name} (${orig.iata}) → ${dest.name} (${dest.iata}): ${Math.round(item.raw.distance)} km`;
|
||||
return result;
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
title: {
|
||||
text: "distance →",
|
||||
align: "end",
|
||||
display: true,
|
||||
},
|
||||
ticks: {
|
||||
format: {
|
||||
style: "unit",
|
||||
unit: "kilometer",
|
||||
},
|
||||
},
|
||||
},
|
||||
y: {
|
||||
type: "logarithmic",
|
||||
title: {
|
||||
text: "# of flights →",
|
||||
align: "end",
|
||||
display: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function openTooltip() {
|
||||
if (!currentChart)
|
||||
throw new Error("No chart is present, please draw a chart first");
|
||||
|
||||
const renderedDataset = currentChart.getDatasetMeta(0);
|
||||
const node = currentChart.canvas;
|
||||
const rect = node.getBoundingClientRect();
|
||||
|
||||
// Index 2426 is carefully chosen to display a lot of lines (which depends
|
||||
// on the zoom level).
|
||||
const point = renderedDataset.data[2426];
|
||||
const event = new MouseEvent("mousemove", {
|
||||
clientX: rect.left + point.x,
|
||||
clientY: rect.top + point.y,
|
||||
cancelable: true,
|
||||
bubbles: true,
|
||||
});
|
||||
|
||||
node.dispatchEvent(event);
|
||||
}
|
||||
|
||||
function reset() {
|
||||
if (currentChart) {
|
||||
currentChart.destroy();
|
||||
currentChart = null;
|
||||
}
|
||||
}
|
||||
|
||||
async function runAllTheThings() {
|
||||
[
|
||||
// Force prettier to use a multiline formatting
|
||||
"prepare",
|
||||
"add-scatter-chart-button",
|
||||
"open-tooltip",
|
||||
].forEach((id) => document.getElementById(id).click());
|
||||
}
|
||||
|
||||
document.getElementById("prepare").addEventListener("click", prepare);
|
||||
document.getElementById("add-scatter-chart-button").addEventListener("click", drawScattered);
|
||||
document.getElementById("open-tooltip").addEventListener("click", openTooltip);
|
||||
document.getElementById("reset").addEventListener("click", reset);
|
||||
document.getElementById("run-all").addEventListener("click", runAllTheThings);
|
||||
|
||||
if (import.meta.env.DEV)
|
||||
runAllTheThings();
|
||||
4
resources/charts/datasets/README
Normal file
4
resources/charts/datasets/README
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
Origin of the datasets:
|
||||
|
||||
airports.csv: https://github.com/d3/d3/blob/gh-pages-old/talk/20111116/airports.csv
|
||||
flights-airports.csv: https://github.com/d3/d3/blob/gh-pages-old/talk/20111116/flights-airport.csv
|
||||
3377
resources/charts/datasets/airports.csv
Normal file
3377
resources/charts/datasets/airports.csv
Normal file
File diff suppressed because it is too large
Load diff
5367
resources/charts/datasets/flights-airports.csv
Normal file
5367
resources/charts/datasets/flights-airports.csv
Normal file
File diff suppressed because it is too large
Load diff
33
resources/charts/developer.html
Normal file
33
resources/charts/developer.html
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Development UI</title>
|
||||
<style>
|
||||
iframe {
|
||||
border: 0;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
width: 48%;
|
||||
height: 800px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app">
|
||||
<h1>Demo page (not intended for workload)</h1>
|
||||
<p>This page shows the various workloads to help with local development. The runner should open only one of them.</p>
|
||||
<details open>
|
||||
<summary><strong>Observable Plot</strong> (<a href="./observable-plot.html">open in full page</a>)</summary>
|
||||
<iframe src="./observable-plot.html"></iframe>
|
||||
</details>
|
||||
<details open>
|
||||
<summary>
|
||||
<strong>Chart.js (<a href="./chartjs.html">open in full page</a>)</strong>
|
||||
</summary>
|
||||
<iframe src="./chartjs.html"></iframe>
|
||||
</details>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
14248
resources/charts/dist/assets/chartjs-7fd89fd7.js
vendored
Normal file
14248
resources/charts/dist/assets/chartjs-7fd89fd7.js
vendored
Normal file
File diff suppressed because it is too large
Load diff
1
resources/charts/dist/assets/chartjs-7fd89fd7.js.map
vendored
Normal file
1
resources/charts/dist/assets/chartjs-7fd89fd7.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
1
resources/charts/dist/assets/developer-4ed993c7.js.map
vendored
Normal file
1
resources/charts/dist/assets/developer-4ed993c7.js.map
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
{"version":3,"file":"developer-4ed993c7.js","sources":[],"sourcesContent":[],"names":[],"mappings":""}
|
||||
3517
resources/charts/dist/assets/flights-airports-9a9e6422.js
vendored
Normal file
3517
resources/charts/dist/assets/flights-airports-9a9e6422.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
resources/charts/dist/assets/flights-airports-9a9e6422.js.map
vendored
Normal file
1
resources/charts/dist/assets/flights-airports-9a9e6422.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
1
resources/charts/dist/assets/index-4ed993c7.js.map
vendored
Normal file
1
resources/charts/dist/assets/index-4ed993c7.js.map
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
{"version":3,"file":"index-4ed993c7.js","sources":[],"sourcesContent":[],"names":[],"mappings":""}
|
||||
12316
resources/charts/dist/assets/plot-562fbdb6.js
vendored
Normal file
12316
resources/charts/dist/assets/plot-562fbdb6.js
vendored
Normal file
File diff suppressed because it is too large
Load diff
1
resources/charts/dist/assets/plot-562fbdb6.js.map
vendored
Normal file
1
resources/charts/dist/assets/plot-562fbdb6.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
35
resources/charts/dist/chartjs.html
vendored
Normal file
35
resources/charts/dist/chartjs.html
vendored
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Chart.js</title>
|
||||
<style type="text/css" media="all">
|
||||
label {
|
||||
user-select: none;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
}
|
||||
</style>
|
||||
<script type="module" crossorigin src="./assets/chartjs-7fd89fd7.js"></script>
|
||||
<link rel="modulepreload" crossorigin href="./assets/flights-airports-9a9e6422.js">
|
||||
</head>
|
||||
<body>
|
||||
<div id="app">
|
||||
<button id="run-all" type="button">Run all scenario (development only)</button>
|
||||
<button id="prepare" type="button">Prepare data</button>
|
||||
<button id="add-scatter-chart-button" type="button">Draw a scatter chart</button>
|
||||
<button id="open-tooltip" type="button">Open tooltips</button>
|
||||
<button id="reset" type="button">Reset</button>
|
||||
<label><input type="checkbox" id="opaque-color" />Use opaque color for next draw</label>
|
||||
</div>
|
||||
<canvas id="chart"></canvas>
|
||||
<script>
|
||||
// This hack allows to capture the work normally happening in a rAF. We
|
||||
// may be able to remove it if the runner improves.
|
||||
window.requestAnimationFrame = (cb) => window.setTimeout(cb, 0);
|
||||
window.cancelAnimationFrame = window.clearTimeout;
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
33
resources/charts/dist/developer.html
vendored
Normal file
33
resources/charts/dist/developer.html
vendored
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Development UI</title>
|
||||
<style>
|
||||
iframe {
|
||||
border: 0;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
width: 48%;
|
||||
height: 800px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app">
|
||||
<h1>Demo page (not intended for workload)</h1>
|
||||
<p>This page shows the various workloads to help with local development. The runner should open only one of them.</p>
|
||||
<details open>
|
||||
<summary><strong>Observable Plot</strong> (<a href="./observable-plot.html">open in full page</a>)</summary>
|
||||
<iframe src="./observable-plot.html"></iframe>
|
||||
</details>
|
||||
<details open>
|
||||
<summary>
|
||||
<strong>Chart.js (<a href="./chartjs.html">open in full page</a>)</strong>
|
||||
</summary>
|
||||
<iframe src="./chartjs.html"></iframe>
|
||||
</details>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
15
resources/charts/dist/index.html
vendored
Normal file
15
resources/charts/dist/index.html
vendored
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="initial-scale=1" />
|
||||
</head>
|
||||
<body>
|
||||
<h1>Charting workloads</h1>
|
||||
<ul>
|
||||
<li><a href="./observable-plot.html">Observable Plot</a></li>
|
||||
<li><a href="./chartjs.html">Chart.js</a></li>
|
||||
</ul>
|
||||
<p>You can also find the <a href="./developer.html">developer page</a> that includes all workloads in iframes.</p>
|
||||
</body>
|
||||
</html>
|
||||
37
resources/charts/dist/observable-plot.html
vendored
Normal file
37
resources/charts/dist/observable-plot.html
vendored
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Observable Plot</title>
|
||||
<style>
|
||||
label {
|
||||
display: inline-flex;
|
||||
align-content: center;
|
||||
gap: 3px;
|
||||
margin: 0 3px;
|
||||
}
|
||||
#airport-group-size {
|
||||
min-width: 30px;
|
||||
}
|
||||
</style>
|
||||
<script type="module" crossorigin src="./assets/plot-562fbdb6.js"></script>
|
||||
<link rel="modulepreload" crossorigin href="./assets/flights-airports-9a9e6422.js">
|
||||
</head>
|
||||
<body>
|
||||
<div id="app">
|
||||
<button id="run-all" type="button">Run all scenario (development only)</button>
|
||||
<button id="prepare" type="button">Prepare data</button>
|
||||
<button id="add-stacked-chart-button" type="button">Add a stacked chart</button>
|
||||
<button id="add-dotted-chart-button" type="button">Add a dotted chart</button>
|
||||
<button id="reset" type="button">Reset</button>
|
||||
<label>
|
||||
Number of airports to keep in each group:
|
||||
<span id="airport-group-size"></span>
|
||||
<input type="range" id="airport-group-size-input" min="0" max="20" value="6" />
|
||||
</label>
|
||||
</div>
|
||||
<div id="chart"></div>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
15
resources/charts/index.html
Normal file
15
resources/charts/index.html
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="initial-scale=1" />
|
||||
</head>
|
||||
<body>
|
||||
<h1>Charting workloads</h1>
|
||||
<ul>
|
||||
<li><a href="./observable-plot.html">Observable Plot</a></li>
|
||||
<li><a href="./chartjs.html">Chart.js</a></li>
|
||||
</ul>
|
||||
<p>You can also find the <a href="./developer.html">developer page</a> that includes all workloads in iframes.</p>
|
||||
</body>
|
||||
</html>
|
||||
35
resources/charts/observable-plot.html
Normal file
35
resources/charts/observable-plot.html
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Observable Plot</title>
|
||||
<style>
|
||||
label {
|
||||
display: inline-flex;
|
||||
align-content: center;
|
||||
gap: 3px;
|
||||
margin: 0 3px;
|
||||
}
|
||||
#airport-group-size {
|
||||
min-width: 30px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app">
|
||||
<button id="run-all" type="button">Run all scenario (development only)</button>
|
||||
<button id="prepare" type="button">Prepare data</button>
|
||||
<button id="add-stacked-chart-button" type="button">Add a stacked chart</button>
|
||||
<button id="add-dotted-chart-button" type="button">Add a dotted chart</button>
|
||||
<button id="reset" type="button">Reset</button>
|
||||
<label>
|
||||
Number of airports to keep in each group:
|
||||
<span id="airport-group-size"></span>
|
||||
<input type="range" id="airport-group-size-input" min="0" max="20" value="6" />
|
||||
</label>
|
||||
</div>
|
||||
<div id="chart"></div>
|
||||
<script type="module" src="/observable-plot.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
233
resources/charts/observable-plot.js
Normal file
233
resources/charts/observable-plot.js
Normal file
|
|
@ -0,0 +1,233 @@
|
|||
import * as Plot from "@observablehq/plot";
|
||||
import { csvParse } from "d3-dsv";
|
||||
import * as d3Array from "d3-array";
|
||||
import { format as d3Format } from "d3-format";
|
||||
|
||||
// These 2 imports use some vite machinery to directly import these files as
|
||||
// strings. Then they are constants and this reduces the load on the GC.
|
||||
import airportsString from "./datasets/airports.csv?raw";
|
||||
import flightsString from "./datasets/flights-airports.csv?raw";
|
||||
|
||||
const ROOT = document.getElementById("chart");
|
||||
const DEFAULT_OPTIONS = {
|
||||
// This width is really a max-width: the plot adjusts using the available
|
||||
// width as well.
|
||||
width: 2000,
|
||||
// The height is especially used for the ratio.
|
||||
height: 1000,
|
||||
};
|
||||
|
||||
let preparedData;
|
||||
function prepare() {
|
||||
/**
|
||||
* AirportInformation: { state, iata, name, city, country, latitude, longitude }
|
||||
* airports: Array<AirportInformation>
|
||||
* flights: Array<{ origin, destination, count }>
|
||||
*/
|
||||
const { airports, flights } = parseAirportsInformation();
|
||||
/**
|
||||
* flightsByAirport: Map<iata: string, { origin: number, destination: number, total: number}>
|
||||
*/
|
||||
const flightsByAirport = groupFlightsByAirports(flights);
|
||||
/**
|
||||
* byAirport: Map<iata: string, AirportInformation>
|
||||
*/
|
||||
const byAirport = d3Array.index(airports, (d) => d.iata);
|
||||
|
||||
/* Array<[state, AirportInformation[]]> */
|
||||
const airportsGroupedByStateArray = d3Array.groups(airports, (d) => d.state);
|
||||
|
||||
/* DescSortedArray<[{ state: string, total: number, mostUsedAirportsInState: AirportInformation[] }]> */
|
||||
const stateInformationSortedArray = airportsGroupedByStateArray
|
||||
.map(([state, airportsInState]) => {
|
||||
const totalFlightsInState = d3Array.sum(airportsInState, ({ iata }) => flightsByAirport.get(iata)?.total);
|
||||
const sorted = d3Array.sort(airportsInState, ({ iata }) => -flightsByAirport.get(iata)?.total);
|
||||
const mostUsedAirportsInState = sorted.slice(0, airportCountPerGroup());
|
||||
return {
|
||||
state,
|
||||
total: totalFlightsInState,
|
||||
mostUsedAirports: mostUsedAirportsInState,
|
||||
};
|
||||
})
|
||||
.sort((stateA, stateB) => stateB.total - stateA.total);
|
||||
|
||||
/* Array<state: string> */
|
||||
const stateSortedArray = stateInformationSortedArray.map(({ state }) => state);
|
||||
|
||||
// Flatten the information in preparedData.stateInformationSortedArray, so that we
|
||||
// have one item == one airport information.
|
||||
/* Array<{state, iata, name, city, index, origin, destination, total}> */
|
||||
const plotData = stateInformationSortedArray.flatMap(({ mostUsedAirports, total, state }) => {
|
||||
const enrichedMostUsedAirports = mostUsedAirports.map(({ iata, name, city }, index) => ({
|
||||
state,
|
||||
iata,
|
||||
name,
|
||||
city,
|
||||
index, // This will be used to have consistent colors.
|
||||
...flightsByAirport.get(iata),
|
||||
}));
|
||||
const otherTotal = total - d3Array.sum(mostUsedAirports, ({ iata }) => flightsByAirport.get(iata)?.total);
|
||||
|
||||
if (otherTotal > 0) {
|
||||
enrichedMostUsedAirports.push({
|
||||
state,
|
||||
iata: "Other",
|
||||
total: otherTotal,
|
||||
index: enrichedMostUsedAirports.length,
|
||||
});
|
||||
}
|
||||
return enrichedMostUsedAirports;
|
||||
});
|
||||
|
||||
preparedData = {
|
||||
airports,
|
||||
flights,
|
||||
flightsByAirport,
|
||||
byAirport,
|
||||
stateInformationSortedArray,
|
||||
stateSortedArray,
|
||||
plotData,
|
||||
};
|
||||
}
|
||||
|
||||
function parseAirportsInformation() {
|
||||
return {
|
||||
airports: csvParse(airportsString),
|
||||
flights: csvParse(flightsString),
|
||||
};
|
||||
}
|
||||
|
||||
function groupFlightsByAirports(flights) {
|
||||
const flightsByAirport = new Map();
|
||||
|
||||
for (const { origin, destination, count } of flights) {
|
||||
const infoForOriginAirport = flightsByAirport.get(origin) ?? { origin: 0, destination: 0, total: 0 };
|
||||
const intCount = Number(count);
|
||||
if (Number.isNaN(intCount)) {
|
||||
console.error(`Couldn't convert ${count} to number.`);
|
||||
continue;
|
||||
}
|
||||
infoForOriginAirport.origin += intCount;
|
||||
infoForOriginAirport.total += intCount;
|
||||
flightsByAirport.set(origin, infoForOriginAirport);
|
||||
const infoForDestinationAirport = flightsByAirport.get(destination) ?? { origin: 0, destination: 0, total: 0 };
|
||||
infoForDestinationAirport.destination += intCount;
|
||||
infoForDestinationAirport.total += intCount;
|
||||
flightsByAirport.set(destination, infoForDestinationAirport);
|
||||
}
|
||||
|
||||
return flightsByAirport;
|
||||
}
|
||||
|
||||
function isReady() {
|
||||
return !!preparedData;
|
||||
}
|
||||
|
||||
function addStackedBars() {
|
||||
if (!isReady())
|
||||
throw new Error("Please prepare the data first.");
|
||||
|
||||
const options = {
|
||||
...DEFAULT_OPTIONS,
|
||||
color: { type: "categorical" },
|
||||
x: {
|
||||
domain: preparedData.stateSortedArray,
|
||||
},
|
||||
y: { grid: true, tickFormat: "~s" },
|
||||
marks: [
|
||||
Plot.barY(preparedData.plotData, {
|
||||
x: "state",
|
||||
y: "total",
|
||||
fill: "index",
|
||||
title: (d) => `${d.iata === "Other" ? "Other" : `${d.name}, ${d.city} (${d.iata})`}\n${d3Format(",")(d.total)} flights`,
|
||||
}),
|
||||
Plot.text(preparedData.stateInformationSortedArray, { x: "state", y: "total", text: (d) => d3Format(".2~s")(d.total), dy: -10 }),
|
||||
Plot.ruleY([0]),
|
||||
],
|
||||
};
|
||||
ROOT.append(Plot.plot(options));
|
||||
}
|
||||
|
||||
function addDottedBars() {
|
||||
if (!isReady())
|
||||
throw new Error("Please prepare the data first.");
|
||||
|
||||
const data = [...preparedData.flightsByAirport]
|
||||
.flatMap(([iata, { origin, destination }]) => {
|
||||
const airportInformation = preparedData.byAirport.get(iata);
|
||||
return [
|
||||
{ ...airportInformation, value: -origin },
|
||||
{ ...airportInformation, value: destination },
|
||||
];
|
||||
})
|
||||
.filter((d) => d.value);
|
||||
|
||||
const options = {
|
||||
...DEFAULT_OPTIONS,
|
||||
color: { type: "threshold", domain: [0] },
|
||||
x: {
|
||||
domain: preparedData.stateSortedArray,
|
||||
},
|
||||
y: {
|
||||
grid: true,
|
||||
label: "← outward Number of flights inward →",
|
||||
labelAnchor: "center",
|
||||
tickFormat: (v) => d3Format("~s")(Math.abs(v)),
|
||||
type: "pow",
|
||||
exponent: 0.2,
|
||||
},
|
||||
marks: [
|
||||
Plot.dot(data, {
|
||||
x: "state",
|
||||
y: "value",
|
||||
r: 4,
|
||||
stroke: "value",
|
||||
strokeWidth: 3,
|
||||
title: (d) => `${d.iata === "Other" ? "Other" : `${d.name}, ${d.city} (${d.iata})`}\n${d3Format(",")(Math.abs(d.value))} ${d.value > 0 ? "inward" : "outward"} flights`,
|
||||
}),
|
||||
Plot.ruleY([0]),
|
||||
],
|
||||
};
|
||||
|
||||
ROOT.append(Plot.plot(options));
|
||||
}
|
||||
|
||||
function reset() {
|
||||
ROOT.textContent = "";
|
||||
}
|
||||
|
||||
async function runAllTheThings() {
|
||||
reset();
|
||||
[
|
||||
// Force prettier to use a multiline formatting
|
||||
"prepare",
|
||||
"add-stacked-chart-button",
|
||||
"add-dotted-chart-button",
|
||||
].forEach((id) => document.getElementById(id).click());
|
||||
}
|
||||
|
||||
// This is the number of airports we keep for each state in the stacked bar
|
||||
// graph. One additional group will be added, that will sum all airports that
|
||||
// haven't been kept. It's retrieved from the input directly.
|
||||
function airportCountPerGroup() {
|
||||
return document.querySelector("#airport-group-size-input").value;
|
||||
}
|
||||
|
||||
function onGroupSizeInputChange() {
|
||||
document.querySelector("#airport-group-size").textContent = airportCountPerGroup();
|
||||
if (import.meta.env.DEV) {
|
||||
// In dev mode, redraw everything
|
||||
runAllTheThings();
|
||||
}
|
||||
}
|
||||
|
||||
document.getElementById("prepare").addEventListener("click", prepare);
|
||||
document.getElementById("add-stacked-chart-button").addEventListener("click", addStackedBars);
|
||||
document.getElementById("add-dotted-chart-button").addEventListener("click", addDottedBars);
|
||||
document.getElementById("reset").addEventListener("click", reset);
|
||||
document.getElementById("run-all").addEventListener("click", runAllTheThings);
|
||||
document.getElementById("airport-group-size-input").addEventListener("input", onGroupSizeInputChange);
|
||||
onGroupSizeInputChange();
|
||||
|
||||
if (import.meta.env.DEV)
|
||||
runAllTheThings();
|
||||
1022
resources/charts/package-lock.json
generated
Normal file
1022
resources/charts/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
19
resources/charts/package.json
Normal file
19
resources/charts/package.json
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
{
|
||||
"name": "charts-benchmark",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"devDependencies": {
|
||||
"vite": "^4.1.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@observablehq/plot": "^0.6.4",
|
||||
"chart.js": "^4.2.1",
|
||||
"d3-dsv": "^3.0.1"
|
||||
}
|
||||
}
|
||||
18
resources/charts/vite.config.js
Normal file
18
resources/charts/vite.config.js
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
import { resolve } from "path";
|
||||
import { defineConfig } from "vite";
|
||||
export default defineConfig({
|
||||
base: "./", // Since this will be loaded from the project root
|
||||
build: {
|
||||
modulePreload: { polyfill: false },
|
||||
minify: false,
|
||||
sourcemap: true,
|
||||
rollupOptions: {
|
||||
input: {
|
||||
index: resolve(__dirname, "index.html"),
|
||||
developer: resolve(__dirname, "developer.html"),
|
||||
plot: resolve(__dirname, "observable-plot.html"),
|
||||
chartjs: resolve(__dirname, "chartjs.html"),
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
286
resources/developer-mode.mjs
Normal file
286
resources/developer-mode.mjs
Normal file
|
|
@ -0,0 +1,286 @@
|
|||
import { Suites, Tags } from "./tests.mjs";
|
||||
import { params, defaultParams } from "./shared/params.mjs";
|
||||
|
||||
export function createDeveloperModeContainer() {
|
||||
const container = document.createElement("div");
|
||||
container.className = "developer-mode";
|
||||
|
||||
const details = document.createElement("details");
|
||||
const summary = document.createElement("summary");
|
||||
summary.textContent = "Developer Mode";
|
||||
details.append(summary);
|
||||
|
||||
const content = document.createElement("div");
|
||||
content.className = "developer-mode-content";
|
||||
|
||||
content.append(createUIForSuites());
|
||||
|
||||
const settings = document.createElement("div");
|
||||
settings.className = "settings";
|
||||
settings.append(createUIForIterationCount());
|
||||
settings.append(createUIForWarmupSuite());
|
||||
settings.append(createUIForWarmupBeforeSync());
|
||||
settings.append(createUIForSyncStepDelay());
|
||||
settings.append(createUIForAsyncSteps());
|
||||
settings.append(createUIForLeakyIframes());
|
||||
|
||||
content.append(document.createElement("hr"));
|
||||
content.append(settings);
|
||||
|
||||
content.append(document.createElement("hr"));
|
||||
content.append(createUIForRun());
|
||||
|
||||
details.append(content);
|
||||
container.append(details);
|
||||
return container;
|
||||
}
|
||||
|
||||
function span(text) {
|
||||
const span = document.createElement("span");
|
||||
span.textContent = text;
|
||||
return span;
|
||||
}
|
||||
|
||||
function createUIForWarmupSuite() {
|
||||
return createCheckboxUI("Use Warmup Suite", params.useWarmupSuite, (isChecked) => {
|
||||
params.useWarmupSuite = isChecked;
|
||||
});
|
||||
}
|
||||
|
||||
function createUIForAsyncSteps() {
|
||||
return createCheckboxUI("Use Async Steps", params.useAsyncSteps, (isChecked) => {
|
||||
params.useAsyncSteps = isChecked;
|
||||
});
|
||||
}
|
||||
|
||||
function createUIForLeakyIframes() {
|
||||
return createCheckboxUI("Leak IFrames", params.leakyIframes, (isChecked) => {
|
||||
params.leakyIframes = isChecked;
|
||||
});
|
||||
}
|
||||
|
||||
function createCheckboxUI(labelValue, initialValue, paramsUpdateCallback) {
|
||||
const checkbox = document.createElement("input");
|
||||
checkbox.type = "checkbox";
|
||||
checkbox.checked = !!initialValue;
|
||||
checkbox.onchange = () => {
|
||||
paramsUpdateCallback(checkbox.checked);
|
||||
updateURL();
|
||||
};
|
||||
|
||||
const label = document.createElement("label");
|
||||
label.append(checkbox, " ", span(labelValue));
|
||||
|
||||
return label;
|
||||
}
|
||||
|
||||
function createUIForIterationCount() {
|
||||
return createTimeRangeUI("Iterations: ", "iterationCount", "#", 1, 200);
|
||||
}
|
||||
|
||||
function createUIForWarmupBeforeSync() {
|
||||
return createTimeRangeUI("Warmup time: ", "warmupBeforeSync");
|
||||
}
|
||||
|
||||
function createUIForSyncStepDelay() {
|
||||
return createTimeRangeUI("Sync step delay: ", "waitBeforeSync");
|
||||
}
|
||||
|
||||
function createTimeRangeUI(labelText, paramKey, unit = "ms", min = 0, max = 1000) {
|
||||
const range = document.createElement("input");
|
||||
range.type = "range";
|
||||
range.min = min;
|
||||
range.max = max;
|
||||
range.value = params[paramKey];
|
||||
|
||||
const rangeValueAndUnit = document.createElement("span");
|
||||
rangeValueAndUnit.className = "range-label-data";
|
||||
|
||||
const rangeValue = document.createElement("span");
|
||||
rangeValue.textContent = params[paramKey];
|
||||
rangeValueAndUnit.append(rangeValue, " ", unit);
|
||||
|
||||
const label = document.createElement("label");
|
||||
label.append(span(labelText), range, rangeValueAndUnit);
|
||||
|
||||
range.oninput = () => {
|
||||
rangeValue.textContent = range.value;
|
||||
};
|
||||
range.onchange = () => {
|
||||
params[paramKey] = parseInt(range.value);
|
||||
updateURL();
|
||||
};
|
||||
|
||||
return label;
|
||||
}
|
||||
|
||||
function createUIForSuites() {
|
||||
const control = document.createElement("nav");
|
||||
control.className = "suites";
|
||||
const checkboxes = [];
|
||||
const setSuiteEnabled = (suiteIndex, enabled) => {
|
||||
Suites[suiteIndex].disabled = !enabled;
|
||||
checkboxes[suiteIndex].checked = enabled;
|
||||
};
|
||||
|
||||
control.appendChild(createSuitesGlobalSelectButtons(setSuiteEnabled));
|
||||
|
||||
const ol = document.createElement("ol");
|
||||
for (const suite of Suites) {
|
||||
const li = document.createElement("li");
|
||||
const checkbox = document.createElement("input");
|
||||
checkbox.id = suite.name;
|
||||
checkbox.type = "checkbox";
|
||||
checkbox.checked = !suite.disabled;
|
||||
checkbox.onchange = () => {
|
||||
suite.disabled = !checkbox.checked;
|
||||
updateURL();
|
||||
};
|
||||
checkboxes.push(checkbox);
|
||||
|
||||
const label = document.createElement("label");
|
||||
label.append(checkbox, " ", suite.name);
|
||||
li.appendChild(label);
|
||||
label.onclick = (event) => {
|
||||
if (event?.ctrlKey || event?.metaKey) {
|
||||
for (let suiteIndex = 0; suiteIndex < Suites.length; suiteIndex++) {
|
||||
if (Suites[suiteIndex] !== suite)
|
||||
setSuiteEnabled(suiteIndex, false);
|
||||
else
|
||||
setSuiteEnabled(suiteIndex, true);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
ol.appendChild(li);
|
||||
}
|
||||
control.appendChild(ol);
|
||||
control.appendChild(createSuitesTagsButton(setSuiteEnabled));
|
||||
return control;
|
||||
}
|
||||
|
||||
function createSuitesGlobalSelectButtons(setSuiteEnabled) {
|
||||
const buttons = document.createElement("div");
|
||||
buttons.className = "button-bar";
|
||||
|
||||
let button = document.createElement("button");
|
||||
button.className = "select-all";
|
||||
button.textContent = "Select all";
|
||||
button.onclick = () => {
|
||||
for (let suiteIndex = 0; suiteIndex < Suites.length; suiteIndex++)
|
||||
setSuiteEnabled(suiteIndex, true);
|
||||
|
||||
updateURL();
|
||||
};
|
||||
buttons.appendChild(button);
|
||||
|
||||
button = document.createElement("button");
|
||||
button.textContent = "Unselect all";
|
||||
button.className = "unselect-all";
|
||||
button.onclick = () => {
|
||||
for (let suiteIndex = 0; suiteIndex < Suites.length; suiteIndex++)
|
||||
setSuiteEnabled(suiteIndex, false);
|
||||
|
||||
updateURL();
|
||||
};
|
||||
buttons.appendChild(button);
|
||||
return buttons;
|
||||
}
|
||||
|
||||
function createSuitesTagsButton(setSuiteEnabled) {
|
||||
let tags = document.createElement("div");
|
||||
let buttons = tags.appendChild(document.createElement("div"));
|
||||
buttons.className = "button-bar";
|
||||
let i = 0;
|
||||
const kTagsPerLine = 3;
|
||||
for (const tag of Tags) {
|
||||
if (tag === "all")
|
||||
continue;
|
||||
if (!(i % kTagsPerLine)) {
|
||||
buttons = tags.appendChild(document.createElement("div"));
|
||||
buttons.className = "button-bar";
|
||||
}
|
||||
i++;
|
||||
const button = document.createElement("button");
|
||||
button.className = "tag";
|
||||
button.textContent = `#${tag}`;
|
||||
button.dataTag = tag;
|
||||
button.onclick = (event) => {
|
||||
const extendSelection = event?.shiftKey;
|
||||
const invertSelection = event?.ctrlKey || event?.metaKey;
|
||||
const selectedTag = event.target.dataTag;
|
||||
for (let suiteIndex = 0; suiteIndex < Suites.length; suiteIndex++) {
|
||||
let enabled = Suites[suiteIndex].tags.includes(selectedTag);
|
||||
if (invertSelection)
|
||||
enabled = !enabled;
|
||||
if (extendSelection && !enabled)
|
||||
continue;
|
||||
setSuiteEnabled(suiteIndex, enabled);
|
||||
}
|
||||
updateURL();
|
||||
};
|
||||
buttons.appendChild(button);
|
||||
}
|
||||
return tags;
|
||||
}
|
||||
|
||||
function createUIForRun() {
|
||||
const stepTestButton = document.createElement("button");
|
||||
stepTestButton.textContent = "Step Test \u23EF";
|
||||
stepTestButton.onclick = (event) => {
|
||||
globalThis.benchmarkClient.step();
|
||||
};
|
||||
const startTestButton = document.createElement("button");
|
||||
startTestButton.textContent = "Start Test \u23F5";
|
||||
startTestButton.onclick = (event) => {
|
||||
globalThis.benchmarkClient.start();
|
||||
};
|
||||
const buttons = document.createElement("div");
|
||||
buttons.className = "button-bar";
|
||||
buttons.appendChild(stepTestButton);
|
||||
buttons.appendChild(startTestButton);
|
||||
return buttons;
|
||||
}
|
||||
|
||||
function updateURL() {
|
||||
const url = new URL(window.location.href);
|
||||
|
||||
// If less than all suites are selected then change the URL "Suites" GET parameter
|
||||
// to comma separate only the selected
|
||||
const selectedSuites = Suites.filter((suite) => !suite.disabled);
|
||||
|
||||
url.searchParams.delete("tags");
|
||||
url.searchParams.delete("suites");
|
||||
url.searchParams.delete("suite");
|
||||
if (selectedSuites.length) {
|
||||
// Try finding common tags that would result in the current suite selection.
|
||||
let commonTags = new Set(selectedSuites[0].tags);
|
||||
for (const suite of Suites) {
|
||||
if (suite.disabled)
|
||||
suite.tags.forEach((tag) => commonTags.delete(tag));
|
||||
else
|
||||
commonTags = new Set(suite.tags.filter((tag) => commonTags.has(tag)));
|
||||
}
|
||||
if (selectedSuites.length > 1 && commonTags.size) {
|
||||
const tags = [...commonTags][0];
|
||||
if (tags !== "default")
|
||||
url.searchParams.set("tags", tags);
|
||||
url.searchParams.delete("suites");
|
||||
} else {
|
||||
url.searchParams.set("suites", selectedSuites.map((suite) => suite.name).join(","));
|
||||
}
|
||||
}
|
||||
|
||||
const defaultParamKeys = ["iterationCount", "useWarmupSuite", "warmupBeforeSync", "waitBeforeSync", "useAsyncSteps", "leakyIframes"];
|
||||
for (const paramKey of defaultParamKeys) {
|
||||
if (params[paramKey] !== defaultParams[paramKey])
|
||||
url.searchParams.set(paramKey, params[paramKey]);
|
||||
else
|
||||
url.searchParams.delete(paramKey);
|
||||
}
|
||||
|
||||
// Only push state if changed
|
||||
url.search = decodeURIComponent(url.search);
|
||||
if (url.href !== window.location.href)
|
||||
window.history.pushState({}, "", url);
|
||||
}
|
||||
26
resources/editors/.gitignore
vendored
Normal file
26
resources/editors/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
|
||||
node_modules
|
||||
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
# dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
27
resources/editors/README.md
Normal file
27
resources/editors/README.md
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
## Description
|
||||
|
||||
Lots of people edit text content in the browser. Lots of that content, like WYSIWYG content or code, is too rich or complex to represent well with a `<textarea>`. Sites typically rely on advanced editor libraries for this, and we should make sure browsers perform well at common patterns used by them.
|
||||
|
||||
## Screenshot
|
||||
|
||||

|
||||
|
||||
## What are we testing
|
||||
|
||||
- Basic DOM and editing
|
||||
- Virtualization (DOM content changing during scroll)
|
||||
- Basic flex layout with SVG icons
|
||||
|
||||
## How are we testing
|
||||
|
||||
The test simulates a real-world user flow by loading a number of popular editor libraries. After the initial load is complete, the following steps are timed:
|
||||
|
||||
- Setting to a fairly large value
|
||||
- "Formatting" the text - in code editors this means turning on syntax highlighting, and in WYSISWYG this means bolding all of the contents
|
||||
- Scrolling to the bottom of the editor
|
||||
|
||||
## Developer Documentation
|
||||
|
||||
The app was created with `npm create vite@latest editors`, and can be previewed with `npm run dev`. In order to update the files run in the harness you have to use `npm run build` which will recreate the `dist/` directory.
|
||||
|
||||
The built test can be loaded within the harness using i.e. http://localhost:8900/?suite=Editor-CodeMirror&startAutomatically=true, or the static versions at http://localhost:7000/resources/editors/dist/.
|
||||
2
resources/editors/assets/README.md
Normal file
2
resources/editors/assets/README.md
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
longtext.html is "Du côté de chez Swann" by Marcel Proust, downloaded from https://www.gutenberg.org/ebooks/2650
|
||||
longscript.js has been downloaded from https://unpkg.com/react-dom@18.2.0/umd/react-dom.development.js
|
||||
29869
resources/editors/assets/longscript.js
Normal file
29869
resources/editors/assets/longscript.js
Normal file
File diff suppressed because it is too large
Load diff
1560
resources/editors/assets/longtext.html
Normal file
1560
resources/editors/assets/longtext.html
Normal file
File diff suppressed because it is too large
Load diff
206
resources/editors/codemirror.html
Normal file
206
resources/editors/codemirror.html
Normal file
|
|
@ -0,0 +1,206 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<link rel="stylesheet" href="/style.css" />
|
||||
<title>CodeMirror Test</title>
|
||||
<script>
|
||||
window.requestIdleCallback = undefined;
|
||||
window.cancelIdleCallback = undefined;
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app">
|
||||
<div id="controls">
|
||||
<button id="create">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-square-plus" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
||||
<path d="M4 4m0 2a2 2 0 0 1 2 -2h12a2 2 0 0 1 2 2v12a2 2 0 0 1 -2 2h-12a2 2 0 0 1 -2 -2z"></path>
|
||||
<path d="M9 12l6 0"></path>
|
||||
<path d="M12 9l0 6"></path>
|
||||
</svg>
|
||||
Create
|
||||
</button>
|
||||
|
||||
<button id="long">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="icon icon-tabler icon-tabler-arrow-big-up-lines-filled"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="2"
|
||||
stroke="currentColor"
|
||||
fill="none"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
||||
<path
|
||||
d="M10.586 3l-6.586 6.586a2 2 0 0 0 -.434 2.18l.068 .145a2 2 0 0 0 1.78 1.089h2.586v2a1 1 0 0 0 1 1h6l.117 -.007a1 1 0 0 0 .883 -.993l-.001 -2h2.587a2 2 0 0 0 1.414 -3.414l-6.586 -6.586a2 2 0 0 0 -2.828 0z"
|
||||
stroke-width="0"
|
||||
fill="currentColor"
|
||||
></path>
|
||||
<path d="M15 20a1 1 0 0 1 .117 1.993l-.117 .007h-6a1 1 0 0 1 -.117 -1.993l.117 -.007h6z" stroke-width="0" fill="currentColor"></path>
|
||||
<path d="M15 17a1 1 0 0 1 .117 1.993l-.117 .007h-6a1 1 0 0 1 -.117 -1.993l.117 -.007h6z" stroke-width="0" fill="currentColor"></path>
|
||||
</svg>
|
||||
Long text
|
||||
</button>
|
||||
<button id="short">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="icon icon-tabler icon-tabler-arrow-big-down-lines-filled"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="2"
|
||||
stroke="currentColor"
|
||||
fill="none"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
||||
<path
|
||||
d="M9 8l-.117 .007a1 1 0 0 0 -.883 .993v1.999l-2.586 .001a2 2 0 0 0 -1.414 3.414l6.586 6.586a2 2 0 0 0 2.828 0l6.586 -6.586a2 2 0 0 0 .434 -2.18l-.068 -.145a2 2 0 0 0 -1.78 -1.089l-2.586 -.001v-1.999a1 1 0 0 0 -1 -1h-6z"
|
||||
stroke-width="0"
|
||||
fill="currentColor"
|
||||
></path>
|
||||
<path d="M15 2a1 1 0 0 1 .117 1.993l-.117 .007h-6a1 1 0 0 1 -.117 -1.993l.117 -.007h6z" stroke-width="0" fill="currentColor"></path>
|
||||
<path d="M15 5a1 1 0 0 1 .117 1.993l-.117 .007h-6a1 1 0 0 1 -.117 -1.993l.117 -.007h6z" stroke-width="0" fill="currentColor"></path>
|
||||
</svg>
|
||||
Short text
|
||||
</button>
|
||||
|
||||
<button id="highlight">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-highlight" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
||||
<path d="M3 19h4l10.5 -10.5a2.828 2.828 0 1 0 -4 -4l-10.5 10.5v4"></path>
|
||||
<path d="M12.5 5.5l4 4"></path>
|
||||
<path d="M4.5 13.5l4 4"></path>
|
||||
<path d="M21 15v4h-8l4 -4z"></path>
|
||||
</svg>
|
||||
Highlight
|
||||
</button>
|
||||
<button id="unhighlight">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="icon icon-tabler icon-tabler-highlight-off"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="2"
|
||||
stroke="currentColor"
|
||||
fill="none"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
||||
<path d="M9 9l-6 6v4h4l6 -6m2 -2l2.503 -2.503a2.828 2.828 0 1 0 -4 -4l-2.497 2.497"></path>
|
||||
<path d="M12.5 5.5l4 4"></path>
|
||||
<path d="M4.5 13.5l4 4"></path>
|
||||
<path d="M19 15h2v2m-2 2h-6l3 -3"></path>
|
||||
<path d="M3 3l18 18"></path>
|
||||
</svg>
|
||||
Unhighlight
|
||||
</button>
|
||||
|
||||
<button id="scroll">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-line-height" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
||||
<path d="M3 8l3 -3l3 3"></path>
|
||||
<path d="M3 16l3 3l3 -3"></path>
|
||||
<path d="M6 5l0 14"></path>
|
||||
<path d="M13 6l7 0"></path>
|
||||
<path d="M13 12l7 0"></path>
|
||||
<path d="M13 18l7 0"></path>
|
||||
</svg>
|
||||
Scroll
|
||||
</button>
|
||||
|
||||
<button id="layout">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-refresh-dot" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
||||
<path d="M20 11a8.1 8.1 0 0 0 -15.5 -2m-.5 -4v4h4"></path>
|
||||
<path d="M4 13a8.1 8.1 0 0 0 15.5 2m.5 4v-4h-4"></path>
|
||||
<path d="M12 12m-1 0a1 1 0 1 0 2 0a1 1 0 1 0 -2 0"></path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div id="editor"></div>
|
||||
</div>
|
||||
<script>
|
||||
// This hack allows to capture the work normally happening in a rAF. We
|
||||
// may be able to remove it if the runner improves.
|
||||
window.requestAnimationFrame = (cb) => window.setTimeout(cb, 0);
|
||||
window.cancelAnimationFrame = window.clearTimeout;
|
||||
</script>
|
||||
<script type="module">
|
||||
import { code as shorttext } from "./shorttext.js";
|
||||
import { code as longtext } from "./longtext.js";
|
||||
import editor from "./codemirror.js";
|
||||
|
||||
let editorContainer = document.querySelector("#editor");
|
||||
let editorInstance = null;
|
||||
|
||||
let buttons = {
|
||||
create: document.querySelector("#create"),
|
||||
highlight: document.querySelector("#highlight"),
|
||||
unhighlight: document.querySelector("#unhighlight"),
|
||||
long: document.querySelector("#long"),
|
||||
short: document.querySelector("#short"),
|
||||
scroll: document.querySelector("#scroll"),
|
||||
layout: document.querySelector("#layout"),
|
||||
};
|
||||
buttons.scroll.addEventListener("click", scroll);
|
||||
buttons.highlight.addEventListener("click", highlight);
|
||||
buttons.unhighlight.addEventListener("click", unhighlight);
|
||||
buttons.long.addEventListener("click", long);
|
||||
buttons.short.addEventListener("click", short);
|
||||
buttons.layout.addEventListener("click", layout);
|
||||
|
||||
buttons.create.addEventListener("click", (e) => {
|
||||
if (!editorInstance) {
|
||||
editorInstance = editor(editorContainer);
|
||||
editorInstance.ready.then(() => {
|
||||
buttons.unhighlight.classList.add("active", "true");
|
||||
buttons.create.setAttribute("disabled", "true");
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
function layout() {
|
||||
// Todo - is this necessary with the runner?
|
||||
const body = document.body.getBoundingClientRect();
|
||||
layout.e = document.elementFromPoint((body.width / 2) | 0, (body.height / 2) | 0);
|
||||
}
|
||||
|
||||
function highlight() {
|
||||
buttons.unhighlight.classList.toggle("active", false);
|
||||
buttons.highlight.classList.toggle("active", true);
|
||||
editorInstance.format(true);
|
||||
}
|
||||
function unhighlight() {
|
||||
buttons.unhighlight.classList.toggle("active", true);
|
||||
buttons.highlight.classList.toggle("active", false);
|
||||
editorInstance.format(false);
|
||||
}
|
||||
function long() {
|
||||
buttons.short.classList.toggle("active", false);
|
||||
buttons.long.classList.toggle("active", true);
|
||||
editorInstance.setValue(longtext);
|
||||
}
|
||||
function short() {
|
||||
buttons.short.classList.toggle("active", true);
|
||||
buttons.long.classList.toggle("active", false);
|
||||
editorInstance.setValue(shorttext);
|
||||
}
|
||||
|
||||
function scroll() {
|
||||
let isTop = editorInstance.getScrollTop() == 0;
|
||||
editorInstance.setScrollTop(isTop ? editorInstance.getScrollHeight() : 0);
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
47
resources/editors/codemirror.js
Normal file
47
resources/editors/codemirror.js
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
// https://codemirror.net/examples/bundle/
|
||||
import { EditorView, basicSetup } from "codemirror";
|
||||
import { StateEffect } from "@codemirror/state";
|
||||
import { javascript } from "@codemirror/lang-javascript";
|
||||
|
||||
let lang = javascript();
|
||||
let extensions = [basicSetup, EditorView.lineWrapping];
|
||||
|
||||
export default function (element, value) {
|
||||
let view = new EditorView({
|
||||
extensions,
|
||||
parent: element,
|
||||
doc: value,
|
||||
wordWrapColumn: 80,
|
||||
});
|
||||
|
||||
return {
|
||||
editor: view,
|
||||
// Anything before this promise resolves will happen before timing starts
|
||||
ready: Promise.resolve(),
|
||||
getScrollHeight() {
|
||||
return element.scrollHeight;
|
||||
},
|
||||
getScrollTop() {
|
||||
return element.scrollTop;
|
||||
},
|
||||
setScrollTop(value) {
|
||||
element.scrollTop = value;
|
||||
},
|
||||
setValue: (value) =>
|
||||
view.dispatch({
|
||||
changes: { from: 0, to: view.state.doc.length, insert: value },
|
||||
}),
|
||||
format(on) {
|
||||
// https://codemirror.net/examples/config/
|
||||
// https://discuss.codemirror.net/t/cm6-dynamically-switching-syntax-theme-w-reconfigure/2858/6
|
||||
if (on && extensions.length === 2)
|
||||
extensions.push(lang);
|
||||
else if (!on && extensions.length === 3)
|
||||
extensions.pop();
|
||||
|
||||
view.dispatch({
|
||||
effects: StateEffect.reconfigure.of(extensions),
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
||||
21908
resources/editors/dist/assets/codemirror-521de7ab.js
vendored
Normal file
21908
resources/editors/dist/assets/codemirror-521de7ab.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
resources/editors/dist/assets/codemirror-521de7ab.js.map
vendored
Normal file
1
resources/editors/dist/assets/codemirror-521de7ab.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
66
resources/editors/dist/assets/index-2feebe42.css
vendored
Normal file
66
resources/editors/dist/assets/index-2feebe42.css
vendored
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
:root {
|
||||
font-family: "Helvetica Neue", Helvetica, Verdana, sans-serif;
|
||||
line-height: 1.5;
|
||||
font-weight: 400;
|
||||
font-size: 13px;
|
||||
|
||||
color: #213547;
|
||||
background-color: #ffffff;
|
||||
|
||||
font-synthesis: none;
|
||||
text-rendering: optimizeLegibility;
|
||||
-webkit-text-size-adjust: none; /* Note that Firefox implements this with the webkit prefix. */
|
||||
text-size-adjust: none;
|
||||
}
|
||||
|
||||
a {
|
||||
font-weight: 500;
|
||||
color: #646cff;
|
||||
text-decoration: inherit;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
color: #747bff;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
display: flex;
|
||||
min-width: 320px;
|
||||
height: 100vh;
|
||||
padding: 5px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 3.2em;
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
#app {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
#editor {
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
padding: 2px 5px;
|
||||
}
|
||||
|
||||
#controls {
|
||||
display: flex;
|
||||
padding-bottom: 5px;
|
||||
}
|
||||
|
||||
#controls button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin: 0 2px;
|
||||
padding: 2px;
|
||||
}
|
||||
|
||||
#controls button.active {
|
||||
color: blue;
|
||||
}
|
||||
129
resources/editors/dist/assets/index.es-02a92ebc.js
vendored
Normal file
129
resources/editors/dist/assets/index.es-02a92ebc.js
vendored
Normal file
|
|
@ -0,0 +1,129 @@
|
|||
const style = "";
|
||||
const code$1 = `
|
||||
function x() {
|
||||
console.log("Hello world!");
|
||||
}
|
||||
`;
|
||||
const text = `
|
||||
<h1>MOBY-DICK;<br>or, THE WHALE.</h1><p>By Herman Melville</p><hr><h2>CHAPTER 1. Loomings.</h2><p>Call me Ishmael. Some years ago—never mind how long precisely—having little or no money in my purse, and nothing particular to interest me on shore, I thought I would sail about a little and see the watery part of the world. It is a way I have of driving off the spleen and regulating the circulation. Whenever I find myself growing grim about the mouth; whenever it is a damp, drizzly November in my soul; whenever I find myself involuntarily pausing before coffin warehouses, and bringing up the rear of every funeral I meet; and especially whenever my hypos get such an upper hand of me, that it requires a strong moral principle to prevent me from deliberately stepping into the street, and methodically knocking people’s hats off—then, I account it high time to get to sea as soon as I can. This is my substitute for pistol and ball. With a philosophical flourish Cato throws himself upon his sword; I quietly take to the ship. There is nothing surprising in this. If they but knew it, almost all men in their degree, some time or other, cherish very nearly the same feelings towards the ocean with me.</p>
|
||||
`;
|
||||
var base = {
|
||||
8: "Backspace",
|
||||
9: "Tab",
|
||||
10: "Enter",
|
||||
12: "NumLock",
|
||||
13: "Enter",
|
||||
16: "Shift",
|
||||
17: "Control",
|
||||
18: "Alt",
|
||||
20: "CapsLock",
|
||||
27: "Escape",
|
||||
32: " ",
|
||||
33: "PageUp",
|
||||
34: "PageDown",
|
||||
35: "End",
|
||||
36: "Home",
|
||||
37: "ArrowLeft",
|
||||
38: "ArrowUp",
|
||||
39: "ArrowRight",
|
||||
40: "ArrowDown",
|
||||
44: "PrintScreen",
|
||||
45: "Insert",
|
||||
46: "Delete",
|
||||
59: ";",
|
||||
61: "=",
|
||||
91: "Meta",
|
||||
92: "Meta",
|
||||
106: "*",
|
||||
107: "+",
|
||||
108: ",",
|
||||
109: "-",
|
||||
110: ".",
|
||||
111: "/",
|
||||
144: "NumLock",
|
||||
145: "ScrollLock",
|
||||
160: "Shift",
|
||||
161: "Shift",
|
||||
162: "Control",
|
||||
163: "Control",
|
||||
164: "Alt",
|
||||
165: "Alt",
|
||||
173: "-",
|
||||
186: ";",
|
||||
187: "=",
|
||||
188: ",",
|
||||
189: "-",
|
||||
190: ".",
|
||||
191: "/",
|
||||
192: "`",
|
||||
219: "[",
|
||||
220: "\\",
|
||||
221: "]",
|
||||
222: "'"
|
||||
};
|
||||
var shift = {
|
||||
48: ")",
|
||||
49: "!",
|
||||
50: "@",
|
||||
51: "#",
|
||||
52: "$",
|
||||
53: "%",
|
||||
54: "^",
|
||||
55: "&",
|
||||
56: "*",
|
||||
57: "(",
|
||||
59: ":",
|
||||
61: "+",
|
||||
173: "_",
|
||||
186: ":",
|
||||
187: "+",
|
||||
188: "<",
|
||||
189: "_",
|
||||
190: ">",
|
||||
191: "?",
|
||||
192: "~",
|
||||
219: "{",
|
||||
220: "|",
|
||||
221: "}",
|
||||
222: '"'
|
||||
};
|
||||
var chrome = typeof navigator != "undefined" && /Chrome\/(\d+)/.exec(navigator.userAgent);
|
||||
var mac = typeof navigator != "undefined" && /Mac/.test(navigator.platform);
|
||||
var ie = typeof navigator != "undefined" && /MSIE \d|Trident\/(?:[7-9]|\d{2,})\..*rv:(\d+)/.exec(navigator.userAgent);
|
||||
var brokenModifierNames = mac || chrome && +chrome[1] < 57;
|
||||
for (var i = 0; i < 10; i++)
|
||||
base[48 + i] = base[96 + i] = String(i);
|
||||
for (var i = 1; i <= 24; i++)
|
||||
base[i + 111] = "F" + i;
|
||||
for (var i = 65; i <= 90; i++) {
|
||||
base[i] = String.fromCharCode(i + 32);
|
||||
shift[i] = String.fromCharCode(i);
|
||||
}
|
||||
for (var code in base)
|
||||
if (!shift.hasOwnProperty(code))
|
||||
shift[code] = base[code];
|
||||
function keyName(event) {
|
||||
var ignoreKey = brokenModifierNames && (event.ctrlKey || event.altKey || event.metaKey) || ie && event.shiftKey && event.key && event.key.length == 1 || event.key == "Unidentified";
|
||||
var name = !ignoreKey && event.key || (event.shiftKey ? shift : base)[event.keyCode] || event.key || "Unidentified";
|
||||
if (name == "Esc")
|
||||
name = "Escape";
|
||||
if (name == "Del")
|
||||
name = "Delete";
|
||||
if (name == "Left")
|
||||
name = "ArrowLeft";
|
||||
if (name == "Up")
|
||||
name = "ArrowUp";
|
||||
if (name == "Right")
|
||||
name = "ArrowRight";
|
||||
if (name == "Down")
|
||||
name = "ArrowDown";
|
||||
return name;
|
||||
}
|
||||
export {
|
||||
base as b,
|
||||
code$1 as c,
|
||||
keyName as k,
|
||||
shift as s,
|
||||
text as t
|
||||
};
|
||||
//# sourceMappingURL=index.es-02a92ebc.js.map
|
||||
1
resources/editors/dist/assets/index.es-02a92ebc.js.map
vendored
Normal file
1
resources/editors/dist/assets/index.es-02a92ebc.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
1
resources/editors/dist/assets/main-4ed993c7.js.map
vendored
Normal file
1
resources/editors/dist/assets/main-4ed993c7.js.map
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
{"version":3,"file":"main-4ed993c7.js","sources":[],"sourcesContent":[],"names":[],"mappings":""}
|
||||
16358
resources/editors/dist/assets/tiptap-95a40ba8.js
vendored
Normal file
16358
resources/editors/dist/assets/tiptap-95a40ba8.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
resources/editors/dist/assets/tiptap-95a40ba8.js.map
vendored
Normal file
1
resources/editors/dist/assets/tiptap-95a40ba8.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
144
resources/editors/dist/codemirror.html
vendored
Normal file
144
resources/editors/dist/codemirror.html
vendored
Normal file
|
|
@ -0,0 +1,144 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="./vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
|
||||
<title>CodeMirror Test</title>
|
||||
<script>
|
||||
window.requestIdleCallback = undefined;
|
||||
window.cancelIdleCallback = undefined;
|
||||
</script>
|
||||
<script type="module" crossorigin src="./assets/codemirror-521de7ab.js"></script>
|
||||
<link rel="modulepreload" crossorigin href="./assets/index.es-02a92ebc.js">
|
||||
<link rel="stylesheet" href="./assets/index-2feebe42.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="app">
|
||||
<div id="controls">
|
||||
<button id="create">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-square-plus" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
||||
<path d="M4 4m0 2a2 2 0 0 1 2 -2h12a2 2 0 0 1 2 2v12a2 2 0 0 1 -2 2h-12a2 2 0 0 1 -2 -2z"></path>
|
||||
<path d="M9 12l6 0"></path>
|
||||
<path d="M12 9l0 6"></path>
|
||||
</svg>
|
||||
Create
|
||||
</button>
|
||||
|
||||
<button id="long">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="icon icon-tabler icon-tabler-arrow-big-up-lines-filled"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="2"
|
||||
stroke="currentColor"
|
||||
fill="none"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
||||
<path
|
||||
d="M10.586 3l-6.586 6.586a2 2 0 0 0 -.434 2.18l.068 .145a2 2 0 0 0 1.78 1.089h2.586v2a1 1 0 0 0 1 1h6l.117 -.007a1 1 0 0 0 .883 -.993l-.001 -2h2.587a2 2 0 0 0 1.414 -3.414l-6.586 -6.586a2 2 0 0 0 -2.828 0z"
|
||||
stroke-width="0"
|
||||
fill="currentColor"
|
||||
></path>
|
||||
<path d="M15 20a1 1 0 0 1 .117 1.993l-.117 .007h-6a1 1 0 0 1 -.117 -1.993l.117 -.007h6z" stroke-width="0" fill="currentColor"></path>
|
||||
<path d="M15 17a1 1 0 0 1 .117 1.993l-.117 .007h-6a1 1 0 0 1 -.117 -1.993l.117 -.007h6z" stroke-width="0" fill="currentColor"></path>
|
||||
</svg>
|
||||
Long text
|
||||
</button>
|
||||
<button id="short">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="icon icon-tabler icon-tabler-arrow-big-down-lines-filled"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="2"
|
||||
stroke="currentColor"
|
||||
fill="none"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
||||
<path
|
||||
d="M9 8l-.117 .007a1 1 0 0 0 -.883 .993v1.999l-2.586 .001a2 2 0 0 0 -1.414 3.414l6.586 6.586a2 2 0 0 0 2.828 0l6.586 -6.586a2 2 0 0 0 .434 -2.18l-.068 -.145a2 2 0 0 0 -1.78 -1.089l-2.586 -.001v-1.999a1 1 0 0 0 -1 -1h-6z"
|
||||
stroke-width="0"
|
||||
fill="currentColor"
|
||||
></path>
|
||||
<path d="M15 2a1 1 0 0 1 .117 1.993l-.117 .007h-6a1 1 0 0 1 -.117 -1.993l.117 -.007h6z" stroke-width="0" fill="currentColor"></path>
|
||||
<path d="M15 5a1 1 0 0 1 .117 1.993l-.117 .007h-6a1 1 0 0 1 -.117 -1.993l.117 -.007h6z" stroke-width="0" fill="currentColor"></path>
|
||||
</svg>
|
||||
Short text
|
||||
</button>
|
||||
|
||||
<button id="highlight">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-highlight" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
||||
<path d="M3 19h4l10.5 -10.5a2.828 2.828 0 1 0 -4 -4l-10.5 10.5v4"></path>
|
||||
<path d="M12.5 5.5l4 4"></path>
|
||||
<path d="M4.5 13.5l4 4"></path>
|
||||
<path d="M21 15v4h-8l4 -4z"></path>
|
||||
</svg>
|
||||
Highlight
|
||||
</button>
|
||||
<button id="unhighlight">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="icon icon-tabler icon-tabler-highlight-off"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="2"
|
||||
stroke="currentColor"
|
||||
fill="none"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
||||
<path d="M9 9l-6 6v4h4l6 -6m2 -2l2.503 -2.503a2.828 2.828 0 1 0 -4 -4l-2.497 2.497"></path>
|
||||
<path d="M12.5 5.5l4 4"></path>
|
||||
<path d="M4.5 13.5l4 4"></path>
|
||||
<path d="M19 15h2v2m-2 2h-6l3 -3"></path>
|
||||
<path d="M3 3l18 18"></path>
|
||||
</svg>
|
||||
Unhighlight
|
||||
</button>
|
||||
|
||||
<button id="scroll">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-line-height" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
||||
<path d="M3 8l3 -3l3 3"></path>
|
||||
<path d="M3 16l3 3l3 -3"></path>
|
||||
<path d="M6 5l0 14"></path>
|
||||
<path d="M13 6l7 0"></path>
|
||||
<path d="M13 12l7 0"></path>
|
||||
<path d="M13 18l7 0"></path>
|
||||
</svg>
|
||||
Scroll
|
||||
</button>
|
||||
|
||||
<button id="layout">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-refresh-dot" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
||||
<path d="M20 11a8.1 8.1 0 0 0 -15.5 -2m-.5 -4v4h4"></path>
|
||||
<path d="M4 13a8.1 8.1 0 0 0 15.5 2m.5 4v-4h-4"></path>
|
||||
<path d="M12 12m-1 0a1 1 0 1 0 2 0a1 1 0 1 0 -2 0"></path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div id="editor"></div>
|
||||
</div>
|
||||
<script>
|
||||
// This hack allows to capture the work normally happening in a rAF. We
|
||||
// may be able to remove it if the runner improves.
|
||||
window.requestAnimationFrame = (cb) => window.setTimeout(cb, 0);
|
||||
window.cancelAnimationFrame = window.clearTimeout;
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
32
resources/editors/dist/index.html
vendored
Normal file
32
resources/editors/dist/index.html
vendored
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="./vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Development UI</title>
|
||||
<style>
|
||||
iframe {
|
||||
border: 0;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
width: 48%;
|
||||
height: 800px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app">
|
||||
<h1>Demo page (not intended for workload)</h1>
|
||||
<p>This page shows the various workloads to help with local development. The runner should open only one of them.</p>
|
||||
<details open>
|
||||
<summary><strong>Code Editors</strong></summary>
|
||||
<iframe src="./codemirror.html"></iframe>
|
||||
</details>
|
||||
<details open>
|
||||
<summary><strong>WYSIWYG Editors</strong></summary>
|
||||
<iframe src="./tiptap.html"></iframe>
|
||||
</details>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
134
resources/editors/dist/tiptap.html
vendored
Normal file
134
resources/editors/dist/tiptap.html
vendored
Normal file
|
|
@ -0,0 +1,134 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="./vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
|
||||
<title>TipTap Test</title>
|
||||
<script type="module" crossorigin src="./assets/tiptap-95a40ba8.js"></script>
|
||||
<link rel="modulepreload" crossorigin href="./assets/index.es-02a92ebc.js">
|
||||
<link rel="stylesheet" href="./assets/index-2feebe42.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="app">
|
||||
<div id="controls">
|
||||
<button id="create">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-square-plus" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
||||
<path d="M4 4m0 2a2 2 0 0 1 2 -2h12a2 2 0 0 1 2 2v12a2 2 0 0 1 -2 2h-12a2 2 0 0 1 -2 -2z"></path>
|
||||
<path d="M9 12l6 0"></path>
|
||||
<path d="M12 9l0 6"></path>
|
||||
</svg>
|
||||
Create
|
||||
</button>
|
||||
|
||||
<button id="long">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="icon icon-tabler icon-tabler-arrow-big-up-lines-filled"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="2"
|
||||
stroke="currentColor"
|
||||
fill="none"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
||||
<path
|
||||
d="M10.586 3l-6.586 6.586a2 2 0 0 0 -.434 2.18l.068 .145a2 2 0 0 0 1.78 1.089h2.586v2a1 1 0 0 0 1 1h6l.117 -.007a1 1 0 0 0 .883 -.993l-.001 -2h2.587a2 2 0 0 0 1.414 -3.414l-6.586 -6.586a2 2 0 0 0 -2.828 0z"
|
||||
stroke-width="0"
|
||||
fill="currentColor"
|
||||
></path>
|
||||
<path d="M15 20a1 1 0 0 1 .117 1.993l-.117 .007h-6a1 1 0 0 1 -.117 -1.993l.117 -.007h6z" stroke-width="0" fill="currentColor"></path>
|
||||
<path d="M15 17a1 1 0 0 1 .117 1.993l-.117 .007h-6a1 1 0 0 1 -.117 -1.993l.117 -.007h6z" stroke-width="0" fill="currentColor"></path>
|
||||
</svg>
|
||||
Long text
|
||||
</button>
|
||||
<button id="short">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="icon icon-tabler icon-tabler-arrow-big-down-lines-filled"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="2"
|
||||
stroke="currentColor"
|
||||
fill="none"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
||||
<path
|
||||
d="M9 8l-.117 .007a1 1 0 0 0 -.883 .993v1.999l-2.586 .001a2 2 0 0 0 -1.414 3.414l6.586 6.586a2 2 0 0 0 2.828 0l6.586 -6.586a2 2 0 0 0 .434 -2.18l-.068 -.145a2 2 0 0 0 -1.78 -1.089l-2.586 -.001v-1.999a1 1 0 0 0 -1 -1h-6z"
|
||||
stroke-width="0"
|
||||
fill="currentColor"
|
||||
></path>
|
||||
<path d="M15 2a1 1 0 0 1 .117 1.993l-.117 .007h-6a1 1 0 0 1 -.117 -1.993l.117 -.007h6z" stroke-width="0" fill="currentColor"></path>
|
||||
<path d="M15 5a1 1 0 0 1 .117 1.993l-.117 .007h-6a1 1 0 0 1 -.117 -1.993l.117 -.007h6z" stroke-width="0" fill="currentColor"></path>
|
||||
</svg>
|
||||
Short text
|
||||
</button>
|
||||
|
||||
<button id="highlight">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-highlight" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
||||
<path d="M3 19h4l10.5 -10.5a2.828 2.828 0 1 0 -4 -4l-10.5 10.5v4"></path>
|
||||
<path d="M12.5 5.5l4 4"></path>
|
||||
<path d="M4.5 13.5l4 4"></path>
|
||||
<path d="M21 15v4h-8l4 -4z"></path>
|
||||
</svg>
|
||||
Highlight
|
||||
</button>
|
||||
<button id="unhighlight">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="icon icon-tabler icon-tabler-highlight-off"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="2"
|
||||
stroke="currentColor"
|
||||
fill="none"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
||||
<path d="M9 9l-6 6v4h4l6 -6m2 -2l2.503 -2.503a2.828 2.828 0 1 0 -4 -4l-2.497 2.497"></path>
|
||||
<path d="M12.5 5.5l4 4"></path>
|
||||
<path d="M4.5 13.5l4 4"></path>
|
||||
<path d="M19 15h2v2m-2 2h-6l3 -3"></path>
|
||||
<path d="M3 3l18 18"></path>
|
||||
</svg>
|
||||
Unhighlight
|
||||
</button>
|
||||
|
||||
<button id="scroll">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-line-height" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
||||
<path d="M3 8l3 -3l3 3"></path>
|
||||
<path d="M3 16l3 3l3 -3"></path>
|
||||
<path d="M6 5l0 14"></path>
|
||||
<path d="M13 6l7 0"></path>
|
||||
<path d="M13 12l7 0"></path>
|
||||
<path d="M13 18l7 0"></path>
|
||||
</svg>
|
||||
Scroll
|
||||
</button>
|
||||
|
||||
<button id="layout">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-refresh-dot" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
||||
<path d="M20 11a8.1 8.1 0 0 0 -15.5 -2m-.5 -4v4h4"></path>
|
||||
<path d="M4 13a8.1 8.1 0 0 0 15.5 2m.5 4v-4h-4"></path>
|
||||
<path d="M12 12m-1 0a1 1 0 1 0 2 0a1 1 0 1 0 -2 0"></path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div id="editor"></div>
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
1
resources/editors/dist/vite.svg
vendored
Normal file
1
resources/editors/dist/vite.svg
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
32
resources/editors/index.html
Normal file
32
resources/editors/index.html
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Development UI</title>
|
||||
<style>
|
||||
iframe {
|
||||
border: 0;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
width: 48%;
|
||||
height: 800px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app">
|
||||
<h1>Demo page (not intended for workload)</h1>
|
||||
<p>This page shows the various workloads to help with local development. The runner should open only one of them.</p>
|
||||
<details open>
|
||||
<summary><strong>Code Editors</strong></summary>
|
||||
<iframe src="./codemirror.html"></iframe>
|
||||
</details>
|
||||
<details open>
|
||||
<summary><strong>WYSIWYG Editors</strong></summary>
|
||||
<iframe src="./tiptap.html"></iframe>
|
||||
</details>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
2
resources/editors/longtext.js
Normal file
2
resources/editors/longtext.js
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
export { default as code } from "./assets/longscript.js?raw";
|
||||
export { default as text } from "./assets/longtext.html?raw";
|
||||
2615
resources/editors/package-lock.json
generated
Normal file
2615
resources/editors/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
18
resources/editors/package.json
Normal file
18
resources/editors/package.json
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
{
|
||||
"name": "editors",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@codemirror/lang-javascript": "^6.1.4",
|
||||
"@tiptap/core": "^2.0.0-beta.217",
|
||||
"@tiptap/starter-kit": "^2.0.0-beta.217",
|
||||
"codemirror": "^6.0.1",
|
||||
"vite": "^4.1.0"
|
||||
}
|
||||
}
|
||||
1
resources/editors/public/vite.svg
Normal file
1
resources/editors/public/vite.svg
Normal file
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
BIN
resources/editors/screenshot.jpg
Normal file
BIN
resources/editors/screenshot.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 37 KiB |
9
resources/editors/shorttext.js
Normal file
9
resources/editors/shorttext.js
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
export const code = `
|
||||
function x() {
|
||||
console.log("Hello world!");
|
||||
}
|
||||
`;
|
||||
|
||||
export const text = `
|
||||
<h1>MOBY-DICK;<br>or, THE WHALE.</h1><p>By Herman Melville</p><hr><h2>CHAPTER 1. Loomings.</h2><p>Call me Ishmael. Some years ago—never mind how long precisely—having little or no money in my purse, and nothing particular to interest me on shore, I thought I would sail about a little and see the watery part of the world. It is a way I have of driving off the spleen and regulating the circulation. Whenever I find myself growing grim about the mouth; whenever it is a damp, drizzly November in my soul; whenever I find myself involuntarily pausing before coffin warehouses, and bringing up the rear of every funeral I meet; and especially whenever my hypos get such an upper hand of me, that it requires a strong moral principle to prevent me from deliberately stepping into the street, and methodically knocking people’s hats off—then, I account it high time to get to sea as soon as I can. This is my substitute for pistol and ball. With a philosophical flourish Cato throws himself upon his sword; I quietly take to the ship. There is nothing surprising in this. If they but knew it, almost all men in their degree, some time or other, cherish very nearly the same feelings towards the ocean with me.</p>
|
||||
`;
|
||||
66
resources/editors/style.css
Normal file
66
resources/editors/style.css
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
:root {
|
||||
font-family: "Helvetica Neue", Helvetica, Verdana, sans-serif;
|
||||
line-height: 1.5;
|
||||
font-weight: 400;
|
||||
font-size: 13px;
|
||||
|
||||
color: #213547;
|
||||
background-color: #ffffff;
|
||||
|
||||
font-synthesis: none;
|
||||
text-rendering: optimizeLegibility;
|
||||
-webkit-text-size-adjust: none; /* Note that Firefox implements this with the webkit prefix. */
|
||||
text-size-adjust: none;
|
||||
}
|
||||
|
||||
a {
|
||||
font-weight: 500;
|
||||
color: #646cff;
|
||||
text-decoration: inherit;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
color: #747bff;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
display: flex;
|
||||
min-width: 320px;
|
||||
height: 100vh;
|
||||
padding: 5px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 3.2em;
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
#app {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
#editor {
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
padding: 2px 5px;
|
||||
}
|
||||
|
||||
#controls {
|
||||
display: flex;
|
||||
padding-bottom: 5px;
|
||||
}
|
||||
|
||||
#controls button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin: 0 2px;
|
||||
padding: 2px;
|
||||
}
|
||||
|
||||
#controls button.active {
|
||||
color: blue;
|
||||
}
|
||||
196
resources/editors/tiptap.html
Normal file
196
resources/editors/tiptap.html
Normal file
|
|
@ -0,0 +1,196 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<link rel="stylesheet" href="/style.css" />
|
||||
<title>TipTap Test</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app">
|
||||
<div id="controls">
|
||||
<button id="create">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-square-plus" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
||||
<path d="M4 4m0 2a2 2 0 0 1 2 -2h12a2 2 0 0 1 2 2v12a2 2 0 0 1 -2 2h-12a2 2 0 0 1 -2 -2z"></path>
|
||||
<path d="M9 12l6 0"></path>
|
||||
<path d="M12 9l0 6"></path>
|
||||
</svg>
|
||||
Create
|
||||
</button>
|
||||
|
||||
<button id="long">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="icon icon-tabler icon-tabler-arrow-big-up-lines-filled"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="2"
|
||||
stroke="currentColor"
|
||||
fill="none"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
||||
<path
|
||||
d="M10.586 3l-6.586 6.586a2 2 0 0 0 -.434 2.18l.068 .145a2 2 0 0 0 1.78 1.089h2.586v2a1 1 0 0 0 1 1h6l.117 -.007a1 1 0 0 0 .883 -.993l-.001 -2h2.587a2 2 0 0 0 1.414 -3.414l-6.586 -6.586a2 2 0 0 0 -2.828 0z"
|
||||
stroke-width="0"
|
||||
fill="currentColor"
|
||||
></path>
|
||||
<path d="M15 20a1 1 0 0 1 .117 1.993l-.117 .007h-6a1 1 0 0 1 -.117 -1.993l.117 -.007h6z" stroke-width="0" fill="currentColor"></path>
|
||||
<path d="M15 17a1 1 0 0 1 .117 1.993l-.117 .007h-6a1 1 0 0 1 -.117 -1.993l.117 -.007h6z" stroke-width="0" fill="currentColor"></path>
|
||||
</svg>
|
||||
Long text
|
||||
</button>
|
||||
<button id="short">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="icon icon-tabler icon-tabler-arrow-big-down-lines-filled"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="2"
|
||||
stroke="currentColor"
|
||||
fill="none"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
||||
<path
|
||||
d="M9 8l-.117 .007a1 1 0 0 0 -.883 .993v1.999l-2.586 .001a2 2 0 0 0 -1.414 3.414l6.586 6.586a2 2 0 0 0 2.828 0l6.586 -6.586a2 2 0 0 0 .434 -2.18l-.068 -.145a2 2 0 0 0 -1.78 -1.089l-2.586 -.001v-1.999a1 1 0 0 0 -1 -1h-6z"
|
||||
stroke-width="0"
|
||||
fill="currentColor"
|
||||
></path>
|
||||
<path d="M15 2a1 1 0 0 1 .117 1.993l-.117 .007h-6a1 1 0 0 1 -.117 -1.993l.117 -.007h6z" stroke-width="0" fill="currentColor"></path>
|
||||
<path d="M15 5a1 1 0 0 1 .117 1.993l-.117 .007h-6a1 1 0 0 1 -.117 -1.993l.117 -.007h6z" stroke-width="0" fill="currentColor"></path>
|
||||
</svg>
|
||||
Short text
|
||||
</button>
|
||||
|
||||
<button id="highlight">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-highlight" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
||||
<path d="M3 19h4l10.5 -10.5a2.828 2.828 0 1 0 -4 -4l-10.5 10.5v4"></path>
|
||||
<path d="M12.5 5.5l4 4"></path>
|
||||
<path d="M4.5 13.5l4 4"></path>
|
||||
<path d="M21 15v4h-8l4 -4z"></path>
|
||||
</svg>
|
||||
Highlight
|
||||
</button>
|
||||
<button id="unhighlight">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="icon icon-tabler icon-tabler-highlight-off"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="2"
|
||||
stroke="currentColor"
|
||||
fill="none"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
||||
<path d="M9 9l-6 6v4h4l6 -6m2 -2l2.503 -2.503a2.828 2.828 0 1 0 -4 -4l-2.497 2.497"></path>
|
||||
<path d="M12.5 5.5l4 4"></path>
|
||||
<path d="M4.5 13.5l4 4"></path>
|
||||
<path d="M19 15h2v2m-2 2h-6l3 -3"></path>
|
||||
<path d="M3 3l18 18"></path>
|
||||
</svg>
|
||||
Unhighlight
|
||||
</button>
|
||||
|
||||
<button id="scroll">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-line-height" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
||||
<path d="M3 8l3 -3l3 3"></path>
|
||||
<path d="M3 16l3 3l3 -3"></path>
|
||||
<path d="M6 5l0 14"></path>
|
||||
<path d="M13 6l7 0"></path>
|
||||
<path d="M13 12l7 0"></path>
|
||||
<path d="M13 18l7 0"></path>
|
||||
</svg>
|
||||
Scroll
|
||||
</button>
|
||||
|
||||
<button id="layout">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-refresh-dot" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
||||
<path d="M20 11a8.1 8.1 0 0 0 -15.5 -2m-.5 -4v4h4"></path>
|
||||
<path d="M4 13a8.1 8.1 0 0 0 15.5 2m.5 4v-4h-4"></path>
|
||||
<path d="M12 12m-1 0a1 1 0 1 0 2 0a1 1 0 1 0 -2 0"></path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div id="editor"></div>
|
||||
</div>
|
||||
<script type="module">
|
||||
import { text as shorttext } from "./shorttext.js";
|
||||
import { text as longtext } from "./longtext.js";
|
||||
import editor from "./tiptap.js";
|
||||
|
||||
let editorContainer = document.querySelector("#editor");
|
||||
let editorInstance = null;
|
||||
|
||||
let buttons = {
|
||||
create: document.querySelector("#create"),
|
||||
highlight: document.querySelector("#highlight"),
|
||||
unhighlight: document.querySelector("#unhighlight"),
|
||||
long: document.querySelector("#long"),
|
||||
short: document.querySelector("#short"),
|
||||
scroll: document.querySelector("#scroll"),
|
||||
layout: document.querySelector("#layout"),
|
||||
};
|
||||
buttons.scroll.addEventListener("click", scroll);
|
||||
buttons.highlight.addEventListener("click", highlight);
|
||||
buttons.unhighlight.addEventListener("click", unhighlight);
|
||||
buttons.long.addEventListener("click", long);
|
||||
buttons.short.addEventListener("click", short);
|
||||
buttons.layout.addEventListener("click", layout);
|
||||
|
||||
buttons.create.addEventListener("click", (e) => {
|
||||
if (!editorInstance) {
|
||||
editorInstance = editor(editorContainer);
|
||||
editorInstance.ready.then(() => {
|
||||
buttons.unhighlight.classList.add("active", "true");
|
||||
buttons.create.setAttribute("disabled", "true");
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
function layout() {
|
||||
// Todo - is this necessary with the runner?
|
||||
const body = document.body.getBoundingClientRect();
|
||||
layout.e = document.elementFromPoint((body.width / 2) | 0, (body.height / 2) | 0);
|
||||
}
|
||||
|
||||
function highlight() {
|
||||
buttons.unhighlight.classList.toggle("active", false);
|
||||
buttons.highlight.classList.toggle("active", true);
|
||||
editorInstance.format(true);
|
||||
}
|
||||
function unhighlight() {
|
||||
buttons.unhighlight.classList.toggle("active", true);
|
||||
buttons.highlight.classList.toggle("active", false);
|
||||
editorInstance.format(false);
|
||||
}
|
||||
function long() {
|
||||
buttons.short.classList.toggle("active", false);
|
||||
buttons.long.classList.toggle("active", true);
|
||||
editorInstance.setValue(longtext);
|
||||
}
|
||||
function short() {
|
||||
buttons.short.classList.toggle("active", true);
|
||||
buttons.long.classList.toggle("active", false);
|
||||
editorInstance.setValue(shorttext);
|
||||
}
|
||||
|
||||
function scroll() {
|
||||
let isTop = editorInstance.getScrollTop() == 0;
|
||||
editorInstance.setScrollTop(isTop ? editorInstance.getScrollHeight() : 0);
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
42
resources/editors/tiptap.js
Normal file
42
resources/editors/tiptap.js
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
// https://tiptap.dev/examples/default
|
||||
import { Editor } from "@tiptap/core";
|
||||
import StarterKit from "@tiptap/starter-kit";
|
||||
|
||||
export default function (element, value) {
|
||||
let editor = new Editor({
|
||||
element,
|
||||
extensions: [StarterKit],
|
||||
content: value,
|
||||
editorProps: {
|
||||
attributes: {
|
||||
spellcheck: "false",
|
||||
},
|
||||
},
|
||||
});
|
||||
return {
|
||||
editor,
|
||||
// Anything before this promise resolves will happen before timing starts
|
||||
ready: Promise.resolve(),
|
||||
getScrollHeight() {
|
||||
return element.scrollHeight;
|
||||
},
|
||||
getScrollTop() {
|
||||
return element.scrollTop;
|
||||
},
|
||||
setScrollTop(value) {
|
||||
element.scrollTop = value;
|
||||
},
|
||||
setValue(value) {
|
||||
// Recommendation is to call focus before most commands
|
||||
// https://tiptap.dev/api/commands#chain-commands
|
||||
editor.chain().focus().setContent(value).setTextSelection(0).run();
|
||||
element.scrollTop = 0;
|
||||
},
|
||||
format(on) {
|
||||
if (on)
|
||||
editor.chain().focus().selectAll().setBold().setTextSelection(0).run();
|
||||
else
|
||||
editor.chain().focus().selectAll().unsetBold().setTextSelection(0).run();
|
||||
},
|
||||
};
|
||||
}
|
||||
17
resources/editors/vite.config.js
Normal file
17
resources/editors/vite.config.js
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
import { resolve } from "path";
|
||||
import { defineConfig } from "vite";
|
||||
export default defineConfig({
|
||||
base: "./", // Since this will be loaded from the project root
|
||||
build: {
|
||||
modulePreload: { polyfill: false },
|
||||
minify: false,
|
||||
sourcemap: true,
|
||||
rollupOptions: {
|
||||
input: {
|
||||
codemirror: resolve(__dirname, "codemirror.html"),
|
||||
main: resolve(__dirname, "index.html"),
|
||||
tiptap: resolve(__dirname, "tiptap.html"),
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
BIN
resources/favicon.png
Normal file
BIN
resources/favicon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.9 KiB |
BIN
resources/gauge.png
Normal file
BIN
resources/gauge.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue