chore(tools): new tooling (#948)
commit
f3a088596e
@ -1,39 +0,0 @@
|
||||
{
|
||||
"ignoreDefaults": true,
|
||||
"exclude": [
|
||||
"^coverage/",
|
||||
"^node_modules/",
|
||||
"^es/",
|
||||
"^helpers/",
|
||||
"^modules/",
|
||||
"^test/",
|
||||
"^h.d.ts",
|
||||
"^h.js",
|
||||
"^h.js.map",
|
||||
"^hooks.d.ts",
|
||||
"^hooks.js",
|
||||
"^hooks.js.map",
|
||||
"^htmldomapi.d.ts",
|
||||
"^htmldomapi.js",
|
||||
"^htmldomapi.js.map",
|
||||
"^is.d.ts",
|
||||
"^is.js",
|
||||
"^is.js.map",
|
||||
"^jsx.d.ts",
|
||||
"^jsx-global.d.ts",
|
||||
"^jsx.js",
|
||||
"^jsx.js.map",
|
||||
"^snabbdom.d.ts",
|
||||
"^snabbdom.js",
|
||||
"^snabbdom.js.map",
|
||||
"^thunk.d.ts",
|
||||
"^thunk.js",
|
||||
"^thunk.js.map",
|
||||
"^tovnode.d.ts",
|
||||
"^tovnode.js",
|
||||
"^tovnode.js.map",
|
||||
"^vnode.d.ts",
|
||||
"^vnode.js",
|
||||
"^vnode.js.map"
|
||||
]
|
||||
}
|
@ -1,45 +0,0 @@
|
||||
module.exports = {
|
||||
extends: 'standard-with-typescript',
|
||||
parserOptions: {
|
||||
project: [
|
||||
'./src/package/tsconfig.json',
|
||||
'./src/test/tsconfig.json',
|
||||
]
|
||||
},
|
||||
plugins: [
|
||||
'markdown',
|
||||
],
|
||||
overrides: [
|
||||
{
|
||||
files: ['**/*.md'],
|
||||
processor: 'markdown/markdown'
|
||||
},
|
||||
{
|
||||
files: ['**/*.md/*.@(mjs|ts)'],
|
||||
rules: {
|
||||
'no-undef': 'off',
|
||||
'no-unused-expressions': 'off',
|
||||
'no-unused-vars': 'off',
|
||||
},
|
||||
}
|
||||
],
|
||||
rules: {
|
||||
'import/newline-after-import': 'error',
|
||||
'max-statements-per-line': 'error',
|
||||
'no-var': 'error',
|
||||
'@typescript-eslint/no-non-null-assertion': 'off',
|
||||
'@typescript-eslint/consistent-type-definitions': 'off',
|
||||
'@typescript-eslint/explicit-function-return-type': 'off',
|
||||
'@typescript-eslint/no-unnecessary-type-assertion': 'off',
|
||||
'@typescript-eslint/no-unused-expressions': 'off',
|
||||
'@typescript-eslint/prefer-optional-chain': 'off',
|
||||
'@typescript-eslint/prefer-includes': 'off',
|
||||
'@typescript-eslint/prefer-nullish-coalescing': 'off',
|
||||
'@typescript-eslint/no-dynamic-delete': 'off',
|
||||
'@typescript-eslint/restrict-plus-operands': 'off',
|
||||
'@typescript-eslint/strict-boolean-expressions': 'off',
|
||||
'@typescript-eslint/method-signature-style': 'off',
|
||||
'comma-dangle': ['error', 'only-multiline'],
|
||||
'no-mixed-operators': 'off',
|
||||
}
|
||||
}
|
@ -0,0 +1,63 @@
|
||||
module.exports = {
|
||||
root: true,
|
||||
parser: "@typescript-eslint/parser",
|
||||
plugins: ["@typescript-eslint"],
|
||||
extends: [
|
||||
"eslint:recommended",
|
||||
"plugin:@typescript-eslint/recommended",
|
||||
"plugin:markdown/recommended",
|
||||
"plugin:import/recommended",
|
||||
"plugin:import/typescript",
|
||||
"prettier",
|
||||
],
|
||||
env: {
|
||||
browser: true,
|
||||
node: false,
|
||||
},
|
||||
overrides: [
|
||||
{
|
||||
files: ["*.ts", "*.tsx"],
|
||||
parserOptions: {
|
||||
tsconfigRootDir: __dirname,
|
||||
project: ["./tsconfig.json", "test/tsconfig.json"],
|
||||
},
|
||||
extends: [
|
||||
"plugin:@typescript-eslint/recommended-requiring-type-checking",
|
||||
],
|
||||
rules: {
|
||||
"@typescript-eslint/no-explicit-any": "off",
|
||||
"@typescript-eslint/no-unsafe-member-access": "off",
|
||||
"@typescript-eslint/no-unsafe-assignment": "off",
|
||||
"@typescript-eslint/no-unsafe-call": "off",
|
||||
"@typescript-eslint/no-unsafe-return": "off",
|
||||
"@typescript-eslint/no-non-null-assertion": "off",
|
||||
"@typescript-eslint/explicit-module-boundary-types": "off",
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ["test/**/*.ts", "test/unit/*.tsx"],
|
||||
env: {
|
||||
browser: true,
|
||||
node: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ["*.js"],
|
||||
excludedFiles: ["examples/**"],
|
||||
extends: ["plugin:node/recommended"],
|
||||
env: {
|
||||
node: true,
|
||||
browser: false,
|
||||
},
|
||||
rules: {
|
||||
"@typescript-eslint/no-var-requires": "off",
|
||||
},
|
||||
},
|
||||
],
|
||||
rules: {
|
||||
"max-statements-per-line": "error",
|
||||
"no-var": "error",
|
||||
"import/newline-after-import": "error",
|
||||
"import/no-default-export": "error",
|
||||
},
|
||||
};
|
@ -1,26 +1,21 @@
|
||||
name: CI
|
||||
|
||||
on: pull_request
|
||||
on:
|
||||
pull_request:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
ci:
|
||||
runs-on: ubuntu-latest
|
||||
environment: ci
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Turnstyle
|
||||
uses: softprops/turnstyle@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Read .nvmrc
|
||||
run: echo ::set-output name=node-version::$(cat .nvmrc)
|
||||
id: nvm
|
||||
- uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: ${{ steps.nvm.outputs.node-version }}
|
||||
|
||||
- run: npm ci
|
||||
- run: npx run-s test docs check-clean
|
||||
|
||||
- run: npm run test:ci
|
||||
env:
|
||||
BROWSER_STACK_USERNAME: ${{ secrets.BROWSER_STACK_USERNAME }}
|
||||
BROWSER_STACK_ACCESS_KEY: ${{ secrets. BROWSER_STACK_ACCESS_KEY }}
|
||||
|
@ -1 +1 @@
|
||||
_
|
||||
_
|
||||
|
@ -1,4 +1,4 @@
|
||||
#!/bin/sh
|
||||
. "$(dirname "$0")/_/husky.sh"
|
||||
|
||||
npx commitlint --config commitlint.config.json --edit $1
|
||||
npm exec commithelper -- check --file $1 --fix
|
||||
|
@ -1,4 +1,4 @@
|
||||
#!/bin/sh
|
||||
. "$(dirname "$0")/_/husky.sh"
|
||||
|
||||
npm exec -- npm-run-all -s docs check-clean test
|
||||
npm exec -- lint-staged
|
||||
|
@ -0,0 +1,4 @@
|
||||
#!/bin/sh
|
||||
. "$(dirname "$0")/_/husky.sh"
|
||||
|
||||
exec < /dev/tty && npm exec commithelper -- prompt --file $1
|
@ -0,0 +1 @@
|
||||
/coverage/
|
@ -0,0 +1,3 @@
|
||||
build/
|
||||
node_modules/
|
||||
coverage/
|
@ -1 +0,0 @@
|
||||
/CHANGELOG.md
|
@ -1,5 +0,0 @@
|
||||
{
|
||||
"skip": {
|
||||
"tag": true
|
||||
}
|
||||
}
|
@ -1,101 +0,0 @@
|
||||
module.exports = {
|
||||
// Latest mainstream
|
||||
BS_Chrome_Current: {
|
||||
base: 'BrowserStack',
|
||||
browser: 'chrome',
|
||||
browser_version: 'latest',
|
||||
os: 'Windows',
|
||||
os_version: '10',
|
||||
},
|
||||
BS_Firefox_Current: {
|
||||
base: 'BrowserStack',
|
||||
browser: 'firefox',
|
||||
browser_version: 'latest',
|
||||
os: 'Windows',
|
||||
os_version: '10',
|
||||
},
|
||||
BS_Safari_Current: {
|
||||
base: 'BrowserStack',
|
||||
browser: 'safari',
|
||||
browser_version: 'latest',
|
||||
os: 'OS X',
|
||||
os_version: 'High Sierra',
|
||||
},
|
||||
BS_Android_8: {
|
||||
base: 'BrowserStack',
|
||||
browser: 'Android',
|
||||
device: 'Google Pixel 2',
|
||||
os: 'Android',
|
||||
os_version: '8.0',
|
||||
real_mobile: true,
|
||||
},
|
||||
|
||||
// Older mainstream
|
||||
/* https://github.com/snabbdom/snabbdom/issues/468
|
||||
BS_Chrome_49: {
|
||||
base: 'BrowserStack',
|
||||
browser: 'chrome',
|
||||
browser_version: '49',
|
||||
os: 'Windows',
|
||||
os_version: '10',
|
||||
},
|
||||
*/
|
||||
BS_Firefox_52: {
|
||||
base: 'BrowserStack',
|
||||
browser: 'firefox',
|
||||
browser_version: '52',
|
||||
os: 'Windows',
|
||||
os_version: '10',
|
||||
},
|
||||
/* https://github.com/snabbdom/snabbdom/issues/469
|
||||
BS_Safari_9: {
|
||||
base: 'BrowserStack',
|
||||
browser: 'safari',
|
||||
browser_version: '9.1',
|
||||
os: 'OS X',
|
||||
os_version: 'El Capitan',
|
||||
},
|
||||
*/
|
||||
|
||||
// Misc
|
||||
BS_Android_4_4: {
|
||||
base: 'BrowserStack',
|
||||
device_browser: 'ucbrowser',
|
||||
device: 'Google Nexus 5',
|
||||
os: 'Android',
|
||||
os_version: '4.4',
|
||||
real_mobile: true,
|
||||
},
|
||||
/* https://github.com/snabbdom/snabbdom/issues/470
|
||||
BS_iphone_10: {
|
||||
base: 'BrowserStack',
|
||||
browser: 'Mobile Safari',
|
||||
browser_version: null,
|
||||
device: 'iPhone 7',
|
||||
real_mobile: true,
|
||||
os: 'ios',
|
||||
os_version: '10.3',
|
||||
},
|
||||
*/
|
||||
BS_MS_Edge: {
|
||||
base: 'BrowserStack',
|
||||
browser: 'edge',
|
||||
browser_version: 'latest',
|
||||
os: 'Windows',
|
||||
os_version: '10',
|
||||
},
|
||||
BS_IE_11: {
|
||||
base: 'BrowserStack',
|
||||
browser: 'ie',
|
||||
browser_version: '11.0',
|
||||
os: 'Windows',
|
||||
os_version: '7',
|
||||
},
|
||||
BS_IE_10: {
|
||||
base: 'BrowserStack',
|
||||
browser: 'ie',
|
||||
browser_version: '10.0',
|
||||
os: 'Windows',
|
||||
os_version: '7',
|
||||
},
|
||||
}
|
@ -0,0 +1,89 @@
|
||||
module.exports = {
|
||||
// Latest mainstream
|
||||
BS_Chrome_Current: {
|
||||
base: "BrowserStack",
|
||||
browser: "chrome",
|
||||
browser_version: "latest",
|
||||
os: "Windows",
|
||||
os_version: "10",
|
||||
},
|
||||
BS_Firefox_Current: {
|
||||
base: "BrowserStack",
|
||||
browser: "firefox",
|
||||
browser_version: "latest",
|
||||
os: "Windows",
|
||||
os_version: "10",
|
||||
},
|
||||
BS_Safari_Current: {
|
||||
base: "BrowserStack",
|
||||
browser: "safari",
|
||||
browser_version: "latest",
|
||||
os: "OS X",
|
||||
os_version: "Big Sur",
|
||||
},
|
||||
BS_Android_8: {
|
||||
base: "BrowserStack",
|
||||
browser: "Android",
|
||||
device: "Google Pixel 2",
|
||||
os: "Android",
|
||||
os_version: "8.0",
|
||||
real_mobile: true,
|
||||
},
|
||||
|
||||
// Older mainstream
|
||||
BS_Chrome_50: {
|
||||
base: "BrowserStack",
|
||||
browser: "chrome",
|
||||
browser_version: "50",
|
||||
os: "Windows",
|
||||
os_version: "10",
|
||||
},
|
||||
BS_Firefox_52: {
|
||||
base: "BrowserStack",
|
||||
browser: "firefox",
|
||||
browser_version: "52",
|
||||
os: "Windows",
|
||||
os_version: "10",
|
||||
},
|
||||
BS_Safari_10: {
|
||||
base: "BrowserStack",
|
||||
browser: "safari",
|
||||
browser_version: "10.1",
|
||||
os: "OS X",
|
||||
os_version: "Sierra",
|
||||
},
|
||||
|
||||
// Misc
|
||||
BS_Android_4_4: {
|
||||
base: "BrowserStack",
|
||||
device_browser: "ucbrowser",
|
||||
device: "Google Nexus 5",
|
||||
os: "Android",
|
||||
os_version: "4.4",
|
||||
real_mobile: true,
|
||||
},
|
||||
BS_iphone_10: {
|
||||
base: "BrowserStack",
|
||||
browser: "Mobile Safari",
|
||||
browser_version: null,
|
||||
device: "iPhone 7",
|
||||
real_mobile: true,
|
||||
os: "ios",
|
||||
os_version: "10.3",
|
||||
},
|
||||
BS_MS_Edge: {
|
||||
base: "BrowserStack",
|
||||
browser: "edge",
|
||||
browser_version: "latest",
|
||||
os: "Windows",
|
||||
os_version: "10",
|
||||
},
|
||||
BS_IE_11: {
|
||||
base: "BrowserStack",
|
||||
browser: "ie",
|
||||
browser_version: "11.0",
|
||||
os: "Windows",
|
||||
os_version: "7",
|
||||
es5: true,
|
||||
},
|
||||
};
|
@ -1,29 +0,0 @@
|
||||
{
|
||||
"extends": [
|
||||
"@commitlint/config-conventional"
|
||||
],
|
||||
"rules": {
|
||||
"scope-empty": [2, "never"],
|
||||
"scope-enum": [2, "always", [
|
||||
"ci",
|
||||
"commitlint",
|
||||
"deps",
|
||||
"docs",
|
||||
"eslint",
|
||||
"eventlisteners",
|
||||
"examples",
|
||||
"exports",
|
||||
"format",
|
||||
"git",
|
||||
"github",
|
||||
"karma",
|
||||
"npm",
|
||||
"package",
|
||||
"relic",
|
||||
"style",
|
||||
"ttypescript",
|
||||
"typescript",
|
||||
"vscode"
|
||||
]]
|
||||
}
|
||||
}
|
@ -1,74 +1,83 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
|
||||
<meta charset="utf-8">
|
||||
<title>Carousel</title>
|
||||
<script type="module" src="./index.js"></script>
|
||||
<style type="text/css">
|
||||
div.view {
|
||||
margin: 10px;
|
||||
}
|
||||
h1 {
|
||||
font-size: 24px;
|
||||
color: #505000;
|
||||
}
|
||||
svg {
|
||||
display: block;
|
||||
margin-bottom: 10px;
|
||||
border: 1px solid gray;
|
||||
}
|
||||
g#carousel {
|
||||
-webkit-transition: -webkit-transform 1s ease;
|
||||
transition: transform 1s ease;
|
||||
}
|
||||
polygon {
|
||||
stroke: #808000;
|
||||
transition: fill 0.5s linear;
|
||||
}
|
||||
polygon#yellow {
|
||||
fill: rgba(255,255,0,0.4);
|
||||
}
|
||||
polygon#yellow:hover, polygon#yellow:active {
|
||||
fill: yellow;
|
||||
}
|
||||
polygon#green {
|
||||
fill: rgba(0,128,0,0.4);
|
||||
}
|
||||
polygon#green:hover, polygon#green:active {
|
||||
fill: green;
|
||||
}
|
||||
polygon#magenta {
|
||||
fill: rgba(255,0,255,0.4);
|
||||
}
|
||||
polygon#magenta:hover, polygon#magenta:active {
|
||||
fill: magenta;
|
||||
}
|
||||
polygon#red {
|
||||
fill: rgba(255,0,0,0.4);
|
||||
}
|
||||
polygon#red:hover, polygon#red:active {
|
||||
fill: red;
|
||||
}
|
||||
polygon#cyan {
|
||||
fill: rgba(0,255,255,0.4);
|
||||
}
|
||||
polygon#cyan:hover, polygon#cyan:active {
|
||||
fill: cyan;
|
||||
}
|
||||
polygon#blue {
|
||||
fill: rgba(0,0,255,0.4);
|
||||
}
|
||||
polygon#blue:hover, polygon#blue:active {
|
||||
fill: blue;
|
||||
}
|
||||
button {
|
||||
font-size: 15px;
|
||||
margin: 0 0.7em 0.7em 0;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="container"></div>
|
||||
</body>
|
||||
<head>
|
||||
<meta
|
||||
name="viewport"
|
||||
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"
|
||||
/>
|
||||
<meta charset="utf-8" />
|
||||
<title>Carousel</title>
|
||||
<script type="module" src="./index.js"></script>
|
||||
<style type="text/css">
|
||||
div.view {
|
||||
margin: 10px;
|
||||
}
|
||||
h1 {
|
||||
font-size: 24px;
|
||||
color: #505000;
|
||||
}
|
||||
svg {
|
||||
display: block;
|
||||
margin-bottom: 10px;
|
||||
border: 1px solid gray;
|
||||
}
|
||||
g#carousel {
|
||||
-webkit-transition: -webkit-transform 1s ease;
|
||||
transition: transform 1s ease;
|
||||
}
|
||||
polygon {
|
||||
stroke: #808000;
|
||||
transition: fill 0.5s linear;
|
||||
}
|
||||
polygon#yellow {
|
||||
fill: rgba(255, 255, 0, 0.4);
|
||||
}
|
||||
polygon#yellow:hover,
|
||||
polygon#yellow:active {
|
||||
fill: yellow;
|
||||
}
|
||||
polygon#green {
|
||||
fill: rgba(0, 128, 0, 0.4);
|
||||
}
|
||||
polygon#green:hover,
|
||||
polygon#green:active {
|
||||
fill: green;
|
||||
}
|
||||
polygon#magenta {
|
||||
fill: rgba(255, 0, 255, 0.4);
|
||||
}
|
||||
polygon#magenta:hover,
|
||||
polygon#magenta:active {
|
||||
fill: magenta;
|
||||
}
|
||||
polygon#red {
|
||||
fill: rgba(255, 0, 0, 0.4);
|
||||
}
|
||||
polygon#red:hover,
|
||||
polygon#red:active {
|
||||
fill: red;
|
||||
}
|
||||
polygon#cyan {
|
||||
fill: rgba(0, 255, 255, 0.4);
|
||||
}
|
||||
polygon#cyan:hover,
|
||||
polygon#cyan:active {
|
||||
fill: cyan;
|
||||
}
|
||||
polygon#blue {
|
||||
fill: rgba(0, 0, 255, 0.4);
|
||||
}
|
||||
polygon#blue:hover,
|
||||
polygon#blue:active {
|
||||
fill: blue;
|
||||
}
|
||||
button {
|
||||
font-size: 15px;
|
||||
margin: 0 0.7em 0.7em 0;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="container"></div>
|
||||
</body>
|
||||
</html>
|
||||
|
@ -1,74 +1,119 @@
|
||||
import { init } from '../../build/package/init.js'
|
||||
import { attributesModule } from '../../build/package/modules/attributes.js'
|
||||
import { styleModule } from '../../build/package/modules/style.js'
|
||||
import { eventListenersModule } from '../../build/package/modules/eventlisteners.js'
|
||||
import { h } from '../../build/package/h.js'
|
||||
import {
|
||||
init,
|
||||
attributesModule,
|
||||
styleModule,
|
||||
eventListenersModule,
|
||||
h,
|
||||
} from "../../build/index.js";
|
||||
|
||||
var patch = init([attributesModule, styleModule, eventListenersModule])
|
||||
const patch = init([attributesModule, styleModule, eventListenersModule]);
|
||||
|
||||
var vnode
|
||||
let vnode;
|
||||
|
||||
var data = {
|
||||
degRotation: 0
|
||||
}
|
||||
let data = {
|
||||
degRotation: 0,
|
||||
};
|
||||
|
||||
function gRotation () {
|
||||
function gRotation() {
|
||||
// console.log("gRotation: %s", data.degRotation);
|
||||
return 'rotate(' + data.degRotation + 'deg)'
|
||||
return "rotate(" + data.degRotation + "deg)";
|
||||
}
|
||||
|
||||
function triangleClick (id) {
|
||||
console.log('triangleClick: %s', id)
|
||||
render()
|
||||
function triangleClick(id) {
|
||||
console.log("triangleClick: %s", id);
|
||||
render();
|
||||
}
|
||||
|
||||
function handleRotate (degs) {
|
||||
data.degRotation += degs
|
||||
console.log('handleRotate: %s, %s', degs, data.degRotation)
|
||||
render()
|
||||
function handleRotate(degs) {
|
||||
data.degRotation += degs;
|
||||
console.log("handleRotate: %s, %s", degs, data.degRotation);
|
||||
render();
|
||||
}
|
||||
|
||||
function handleReset (degs) {
|
||||
data.degRotation = degs
|
||||
console.log('handleReset: %s', degs)
|
||||
render()
|
||||
function handleReset(degs) {
|
||||
data.degRotation = degs;
|
||||
console.log("handleReset: %s", degs);
|
||||
render();
|
||||
}
|
||||
|
||||
function render () {
|
||||
vnode = patch(vnode, view(data))
|
||||
function render() {
|
||||
vnode = patch(vnode, view(data));
|
||||
}
|
||||
|
||||
const hTriangle = (id, degRotation) =>
|
||||
h('polygon#' + id, {
|
||||
h("polygon#" + id, {
|
||||
attrs: {
|
||||
points: '-50,-88 0,-175 50,-88',
|
||||
transform: 'rotate(' + degRotation + ')',
|
||||
'stroke-width': 3
|
||||
points: "-50,-88 0,-175 50,-88",
|
||||
transform: "rotate(" + degRotation + ")",
|
||||
"stroke-width": 3,
|
||||
},
|
||||
on: {
|
||||
click: () => {
|
||||
triangleClick(id);
|
||||
},
|
||||
},
|
||||
on: { click: () => { triangleClick(id) } }
|
||||
})
|
||||
});
|
||||
|
||||
const view = (data) =>
|
||||
h('div.view', [
|
||||
h('h1', 'Snabbdom SVG Carousel'),
|
||||
h('svg', { attrs: { width: 380, height: 380, viewBox: [-190, -190, 380, 380] } }, [
|
||||
h('g#carousel',
|
||||
{ style: { '-webkit-transform': gRotation(), transform: gRotation() } }, [
|
||||
hTriangle('yellow', 0),
|
||||
hTriangle('green', 60),
|
||||
hTriangle('magenta', 120),
|
||||
hTriangle('red', 180),
|
||||
hTriangle('cyan', 240),
|
||||
hTriangle('blue', 300)
|
||||
])
|
||||
]),
|
||||
h('button', { on: { click: () => { handleRotate(60) } } }, 'Rotate Clockwise'),
|
||||
h('button', { on: { click: () => { handleRotate(-60) } } }, 'Rotate Anticlockwise'),
|
||||
h('button', { on: { click: () => { handleReset(0) } } }, 'Reset')
|
||||
])
|
||||
const view = () =>
|
||||
h("div.view", [
|
||||
h("h1", "Snabbdom SVG Carousel"),
|
||||
h(
|
||||
"svg",
|
||||
{ attrs: { width: 380, height: 380, viewBox: [-190, -190, 380, 380] } },
|
||||
[
|
||||
h(
|
||||
"g#carousel",
|
||||
{
|
||||
style: { "-webkit-transform": gRotation(), transform: gRotation() },
|
||||
},
|
||||
[
|
||||
hTriangle("yellow", 0),
|
||||
hTriangle("green", 60),
|
||||
hTriangle("magenta", 120),
|
||||
hTriangle("red", 180),
|
||||
hTriangle("cyan", 240),
|
||||
hTriangle("blue", 300),
|
||||
]
|
||||
),
|
||||
]
|
||||
),
|
||||
h(
|
||||
"button",
|
||||
{
|
||||
on: {
|
||||
click: () => {
|
||||
handleRotate(60);
|
||||
},
|
||||
},
|
||||
},
|
||||
"Rotate Clockwise"
|
||||
),
|
||||
h(
|
||||
"button",
|
||||
{
|
||||
on: {
|
||||
click: () => {
|
||||
handleRotate(-60);
|
||||
},
|
||||
},
|
||||
},
|
||||
"Rotate Anticlockwise"
|
||||
),
|
||||
h(
|
||||
"button",
|
||||
{
|
||||
on: {
|
||||
click: () => {
|
||||
handleReset(0);
|
||||
},
|
||||
},
|
||||
},
|
||||
"Reset"
|
||||
),
|
||||
]);
|
||||
|
||||
window.addEventListener('DOMContentLoaded', () => {
|
||||
var container = document.getElementById('container')
|
||||
vnode = patch(container, view(data))
|
||||
render()
|
||||
})
|
||||
window.addEventListener("DOMContentLoaded", () => {
|
||||
const container = document.getElementById("container");
|
||||
vnode = patch(container, view(data));
|
||||
render();
|
||||
});
|
||||
|
@ -1,136 +1,239 @@
|
||||
/* jshint esnext: true */
|
||||
import { init } from '../../build/package/init.js'
|
||||
import { classModule } from '../../build/package/modules/class.js'
|
||||
import { heroModule } from '../../build/package/modules/hero.js'
|
||||
import { styleModule } from '../../build/package/modules/style.js'
|
||||
import { eventListenersModule } from '../../build/package/modules/eventlisteners.js'
|
||||
import { h } from '../../build/package/h.js'
|
||||
import {
|
||||
init,
|
||||
classModule,
|
||||
heroModule,
|
||||
styleModule,
|
||||
eventListenersModule,
|
||||
h,
|
||||
} from "../../build/index.js";
|
||||
|
||||
var patch = init([classModule, heroModule, styleModule, eventListenersModule])
|
||||
const patch = init([
|
||||
classModule,
|
||||
heroModule,
|
||||
styleModule,
|
||||
eventListenersModule,
|
||||
]);
|
||||
|
||||
var vnode
|
||||
let vnode;
|
||||
|
||||
var data = {
|
||||
const data = {
|
||||
selected: undefined,
|
||||
movies: [
|
||||
{ rank: 1, title: 'This is an', desc: 'Lorem ipsum dolor sit amet, sed pede integer vitae bibendum, accumsan sit, vulputate aenean tempora ipsum. Lorem sed id et metus, eros posuere suspendisse nec nunc justo, fusce augue placerat nibh purus suspendisse. Aliquam aliquam, ut eget. Mollis a eget sed nibh tincidunt nec, mi integer, proin magna lacus iaculis tortor. Aliquam vel arcu arcu, vivamus a urna fames felis vel wisi, cursus tortor nec erat dignissim cras sem, mauris ac venenatis tellus elit.' },
|
||||
{ rank: 2, title: 'example of', desc: 'Consequuntur ipsum nulla, consequat curabitur in magnis risus. Taciti mattis bibendum tellus nibh, at dui neque eget, odio pede ut, sapien pede, ipsum ut. Sagittis dui, sodales sem, praesent ipsum conubia eget lorem lobortis wisi.' },
|
||||
{ rank: 3, title: 'Snabbdom', desc: 'Quam lorem aliquam fusce wisi, urna purus ipsum pharetra sed, at cras sodales enim vestibulum odio cras, luctus integer phasellus.' },
|
||||
{ rank: 4, title: 'doing hero transitions', desc: 'Et orci hac ultrices id in. Diam ultrices luctus egestas, sem aliquam auctor molestie odio laoreet. Pede nam cubilia, diam vestibulum ornare natoque, aenean etiam fusce id, eget dictum blandit et mauris mauris. Metus amet ad, elit porttitor a aliquet commodo lacus, integer neque imperdiet augue laoreet, nonummy turpis lacus sed pulvinar condimentum platea. Wisi eleifend quis, tristique dictum, ac dictumst. Sem nec tristique vel vehicula fringilla, nibh eu et posuere mi rhoncus.' },
|
||||
{ rank: 5, title: 'using the', desc: 'Pede nam cubilia, diam vestibulum ornare natoque, aenean etiam fusce id, eget dictum blandit et mauris mauris. Metus amet ad, elit porttitor a aliquet commodo lacus, integer neque imperdiet augue laoreet, nonummy turpis lacus sed pulvinar condimentum platea. Wisi eleifend quis, tristique dictum, ac dictumst. Sem nec tristique vel vehicula fringilla, nibh eu et posuere mi rhoncus.' },
|
||||
{ rank: 6, title: 'module for hero transitions', desc: 'Sapien laoreet, ligula elit tortor nulla pellentesque, maecenas enim turpis, quae duis venenatis vivamus ultricies, nunc imperdiet sollicitudin ipsum malesuada. Ut sem. Wisi fusce nullam nibh enim. Nisl hymenaeos id sed sed in. Proin leo et, pulvinar nunc pede laoreet.' },
|
||||
{ rank: 7, title: 'click on ar element in', desc: 'Accumsan quia, id nascetur dui et congue erat, id excepteur, primis ratione nec. At nulla et. Suspendisse lobortis, lobortis in tortor fringilla, duis adipiscing vestibulum voluptates sociosqu auctor.' },
|
||||
{ rank: 8, title: 'the list', desc: 'Ante tellus egestas vel hymenaeos, ut viverra nibh ut, ipsum nibh donec donec dolor. Eros ridiculus vel egestas convallis ipsum, commodo ut venenatis nullam porta iaculis, suspendisse ante proin leo, felis risus etiam.' },
|
||||
{ rank: 9, title: 'to witness', desc: 'Metus amet ad, elit porttitor a aliquet commodo lacus, integer neque imperdiet augue laoreet, nonummy turpis lacus sed pulvinar condimentum platea. Wisi eleifend quis, tristique dictum, ac dictumst.' },
|
||||
{ rank: 10, title: 'the effect', desc: 'Et orci hac ultrices id in. Diam ultrices luctus egestas, sem aliquam auctor molestie odio laoreet. Pede nam cubilia, diam vestibulum ornare natoque, aenean etiam fusce id, eget dictum blandit et mauris mauris' },
|
||||
]
|
||||
}
|
||||
{
|
||||
rank: 1,
|
||||
title: "This is an",
|
||||
desc:
|
||||
"Lorem ipsum dolor sit amet, sed pede integer vitae bibendum, accumsan sit, vulputate aenean tempora ipsum. Lorem sed id et metus, eros posuere suspendisse nec nunc justo, fusce augue placerat nibh purus suspendisse. Aliquam aliquam, ut eget. Mollis a eget sed nibh tincidunt nec, mi integer, proin magna lacus iaculis tortor. Aliquam vel arcu arcu, vivamus a urna fames felis vel wisi, cursus tortor nec erat dignissim cras sem, mauris ac venenatis tellus elit.",
|
||||
},
|
||||
{
|
||||
rank: 2,
|
||||
title: "example of",
|
||||
desc:
|
||||
"Consequuntur ipsum nulla, consequat curabitur in magnis risus. Taciti mattis bibendum tellus nibh, at dui neque eget, odio pede ut, sapien pede, ipsum ut. Sagittis dui, sodales sem, praesent ipsum conubia eget lorem lobortis wisi.",
|
||||
},
|
||||
{
|
||||
rank: 3,
|
||||
title: "Snabbdom",
|
||||
desc:
|
||||
"Quam lorem aliquam fusce wisi, urna purus ipsum pharetra sed, at cras sodales enim vestibulum odio cras, luctus integer phasellus.",
|
||||
},
|
||||
{
|
||||
rank: 4,
|
||||
title: "doing hero transitions",
|
||||
desc:
|
||||
"Et orci hac ultrices id in. Diam ultrices luctus egestas, sem aliquam auctor molestie odio laoreet. Pede nam cubilia, diam vestibulum ornare natoque, aenean etiam fusce id, eget dictum blandit et mauris mauris. Metus amet ad, elit porttitor a aliquet commodo lacus, integer neque imperdiet augue laoreet, nonummy turpis lacus sed pulvinar condimentum platea. Wisi eleifend quis, tristique dictum, ac dictumst. Sem nec tristique vel vehicula fringilla, nibh eu et posuere mi rhoncus.",
|
||||
},
|
||||
{
|
||||
rank: 5,
|
||||
title: "using the",
|
||||
desc:
|
||||
"Pede nam cubilia, diam vestibulum ornare natoque, aenean etiam fusce id, eget dictum blandit et mauris mauris. Metus amet ad, elit porttitor a aliquet commodo lacus, integer neque imperdiet augue laoreet, nonummy turpis lacus sed pulvinar condimentum platea. Wisi eleifend quis, tristique dictum, ac dictumst. Sem nec tristique vel vehicula fringilla, nibh eu et posuere mi rhoncus.",
|
||||
},
|
||||
{
|
||||
rank: 6,
|
||||
title: "module for hero transitions",
|
||||
desc:
|
||||
"Sapien laoreet, ligula elit tortor nulla pellentesque, maecenas enim turpis, quae duis venenatis vivamus ultricies, nunc imperdiet sollicitudin ipsum malesuada. Ut sem. Wisi fusce nullam nibh enim. Nisl hymenaeos id sed sed in. Proin leo et, pulvinar nunc pede laoreet.",
|
||||
},
|
||||
{
|
||||
rank: 7,
|
||||
title: "click on ar element in",
|
||||
desc:
|
||||
"Accumsan quia, id nascetur dui et congue erat, id excepteur, primis ratione nec. At nulla et. Suspendisse lobortis, lobortis in tortor fringilla, duis adipiscing vestibulum voluptates sociosqu auctor.",
|
||||
},
|
||||
{
|
||||
rank: 8,
|
||||
title: "the list",
|
||||
desc:
|
||||
"Ante tellus egestas vel hymenaeos, ut viverra nibh ut, ipsum nibh donec donec dolor. Eros ridiculus vel egestas convallis ipsum, commodo ut venenatis nullam porta iaculis, suspendisse ante proin leo, felis risus etiam.",
|
||||
},
|
||||
{
|
||||
rank: 9,
|
||||
title: "to witness",
|
||||
desc:
|
||||
"Metus amet ad, elit porttitor a aliquet commodo lacus, integer neque imperdiet augue laoreet, nonummy turpis lacus sed pulvinar condimentum platea. Wisi eleifend quis, tristique dictum, ac dictumst.",
|
||||
},
|
||||
{
|
||||
rank: 10,
|
||||
title: "the effect",
|
||||
desc:
|
||||
"Et orci hac ultrices id in. Diam ultrices luctus egestas, sem aliquam auctor molestie odio laoreet. Pede nam cubilia, diam vestibulum ornare natoque, aenean etiam fusce id, eget dictum blandit et mauris mauris",
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
function select (m) {
|
||||
data.selected = m
|
||||
render()
|
||||
function select(m) {
|
||||
data.selected = m;
|
||||
render();
|
||||
}
|
||||
|
||||
function render () {
|
||||
vnode = patch(vnode, view(data))
|
||||
function render() {
|
||||
vnode = patch(vnode, view(data));
|
||||
}
|
||||
|
||||
const fadeInOutStyle = {
|
||||
opacity: '0', delayed: { opacity: '1' }, remove: { opacity: '0' }
|
||||
}
|
||||
opacity: "0",
|
||||
delayed: { opacity: "1" },
|
||||
remove: { opacity: "0" },
|
||||
};
|
||||
|
||||
const detailView = (movie) =>
|
||||
h('div.page', { style: fadeInOutStyle }, [
|
||||
h('div.header', [
|
||||
h('div.header-content.detail', {
|
||||
style: { opacity: '1', remove: { opacity: '0' } },
|
||||
}, [
|
||||
h('div.rank', [
|
||||
h('span.header-rank.hero', { hero: { id: 'rank' + movie.rank } }, movie.rank),
|
||||
h('div.rank-circle', {
|
||||
style: {
|
||||
transform: 'scale(0)',
|
||||
delayed: { transform: 'scale(1)' },
|
||||
destroy: { transform: 'scale(0)' }
|
||||
h("div.page", { style: fadeInOutStyle }, [
|
||||
h("div.header", [
|
||||
h(
|
||||
"div.header-content.detail",
|
||||
{
|
||||
style: { opacity: "1", remove: { opacity: "0" } },
|
||||
},
|
||||
[
|
||||
h("div.rank", [
|
||||
h(
|
||||
"span.header-rank.hero",
|
||||
{ hero: { id: "rank" + movie.rank } },
|
||||
movie.rank
|
||||
),
|
||||
h("div.rank-circle", {
|
||||
style: {
|
||||
transform: "scale(0)",
|
||||
delayed: { transform: "scale(1)" },
|
||||
destroy: { transform: "scale(0)" },
|
||||
},
|
||||
}),
|
||||
]),
|
||||
h(
|
||||
"div.hero.header-title",
|
||||
{ hero: { id: movie.title } },
|
||||
movie.title
|
||||
),
|
||||
h("div.spacer"),
|
||||
h(
|
||||
"div.close",
|
||||
{
|
||||
on: {
|
||||
click: () => {
|
||||
select(undefined);
|
||||
},
|
||||
},
|
||||
style: {
|
||||
transform: "scale(0)",
|
||||
delayed: { transform: "scale(1)" },
|
||||
destroy: { transform: "scale(0)" },
|
||||
},
|
||||
},
|
||||
}),
|
||||
]),
|
||||
h('div.hero.header-title', { hero: { id: movie.title } }, movie.title),
|
||||
h('div.spacer'),
|
||||
h('div.close', {
|
||||
on: { click: () => { select(undefined) } },
|
||||
"x"
|
||||
),
|
||||
]
|
||||
),
|
||||
]),
|
||||
h("div.page-content", [
|
||||
h(
|
||||
"div.desc",
|
||||
{
|
||||
style: {
|
||||
transform: 'scale(0)',
|
||||
delayed: { transform: 'scale(1)' },
|
||||
destroy: { transform: 'scale(0)' }
|
||||
opacity: "0",
|
||||
transform: "translateX(3em)",
|
||||
delayed: { opacity: "1", transform: "translate(0)" },
|
||||
remove: {
|
||||
opacity: "0",
|
||||
position: "absolute",
|
||||
top: "0",
|
||||
left: "0",
|
||||
transform: "translateX(3em)",
|
||||
},
|
||||
},
|
||||
}, 'x'),
|
||||
]),
|
||||
]),
|
||||
h('div.page-content', [
|
||||
h('div.desc', {
|
||||
style: {
|
||||
opacity: '0',
|
||||
transform: 'translateX(3em)',
|
||||
delayed: { opacity: '1', transform: 'translate(0)' },
|
||||
remove: {
|
||||
opacity: '0',
|
||||
position: 'absolute',
|
||||
top: '0',
|
||||
left: '0',
|
||||
transform: 'translateX(3em)'
|
||||
}
|
||||
}
|
||||
}, [
|
||||
h('h2', 'Description:'),
|
||||
h('span', movie.desc),
|
||||
]),
|
||||
},
|
||||
[h("h2", "Description:"), h("span", movie.desc)]
|
||||
),
|
||||
]),
|
||||
])
|
||||
]);
|
||||
|
||||
const overviewView = (movies) =>
|
||||
h('div.page', { style: fadeInOutStyle }, [
|
||||
h('div.header', [
|
||||
h('div.header-content.overview', {
|
||||
style: fadeInOutStyle,
|
||||
}, [
|
||||
h('div.header-title', {
|
||||
style: {
|
||||
transform: 'translateY(-2em)',
|
||||
delayed: { transform: 'translate(0)' },
|
||||
destroy: { transform: 'translateY(-2em)' }
|
||||
}
|
||||
}, 'Top 10 movies'),
|
||||
h('div.spacer'),
|
||||
]),
|
||||
h("div.page", { style: fadeInOutStyle }, [
|
||||
h("div.header", [
|
||||
h(
|
||||
"div.header-content.overview",
|
||||
{
|
||||
style: fadeInOutStyle,
|
||||
},
|
||||
[
|
||||
h(
|
||||
"div.header-title",
|
||||
{
|
||||
style: {
|
||||
transform: "translateY(-2em)",
|
||||
delayed: { transform: "translate(0)" },
|
||||
destroy: { transform: "translateY(-2em)" },
|
||||
},
|
||||
},
|
||||
"Top 10 movies"
|
||||
),
|
||||
h("div.spacer"),
|
||||
]
|
||||
),
|
||||
]),
|
||||
h('div.page-content', [
|
||||
h('div.list', {
|
||||
style: {
|
||||
opacity: '0',
|
||||
delayed: { opacity: '1' },
|
||||
remove: { opacity: '0', position: 'absolute', top: '0', left: '0' }
|
||||
}
|
||||
}, movies.map((movie) =>
|
||||
h('div.row', {
|
||||
on: { click: () => { select(movie) } },
|
||||
}, [
|
||||
h('div.hero.rank', [
|
||||
h('span.hero', { hero: { id: 'rank' + movie.rank } }, movie.rank)
|
||||
]),
|
||||
h('div.hero', { hero: { id: movie.title } }, movie.title)
|
||||
])
|
||||
)),
|
||||
h("div.page-content", [
|
||||
h(
|
||||
"div.list",
|
||||
{
|
||||
style: {
|
||||
opacity: "0",
|
||||
delayed: { opacity: "1" },
|
||||
remove: { opacity: "0", position: "absolute", top: "0", left: "0" },
|
||||
},
|
||||
},
|
||||
movies.map((movie) =>
|
||||
h(
|
||||
"div.row",
|
||||
{
|
||||
on: {
|
||||
click: () => {
|
||||
select(movie);
|
||||
},
|
||||
},
|
||||
},
|
||||
[
|
||||
h("div.hero.rank", [
|
||||
h(
|
||||
"span.hero",
|
||||
{ hero: { id: "rank" + movie.rank } },
|
||||
movie.rank
|
||||
),
|
||||
]),
|
||||
h("div.hero", { hero: { id: movie.title } }, movie.title),
|
||||
]
|
||||
)
|
||||
)
|
||||
),
|
||||
]),
|
||||
])
|
||||
]);
|
||||
|
||||
const view = (data) =>
|
||||
h('div.page-container', [
|
||||
h("div.page-container", [
|
||||
data.selected ? detailView(data.selected) : overviewView(data.movies),
|
||||
])
|
||||
]);
|
||||
|
||||
window.addEventListener('DOMContentLoaded', () => {
|
||||
var container = document.getElementById('container')
|
||||
vnode = patch(container, view(data))
|
||||
render()
|
||||
})
|
||||
window.addEventListener("DOMContentLoaded", () => {
|
||||
const container = document.getElementById("container");
|
||||
vnode = patch(container, view(data));
|
||||
render();
|
||||
});
|
||||
|
@ -1,15 +1,22 @@
|
||||
import { init } from '../../build/package/init.js'
|
||||
import { attributesModule } from '../../build/package/modules/attributes.js'
|
||||
import { h } from '../../build/package/h.js'
|
||||
import { init, attributesModule, h } from "../../build/index.js";
|
||||
|
||||
var patch = init([attributesModule])
|
||||
const patch = init([attributesModule]);
|
||||
|
||||
window.addEventListener('DOMContentLoaded', () => {
|
||||
var container = document.getElementById('container')
|
||||
var vnode = h('div', [
|
||||
h('svg', { attrs: { width: 100, height: 100 } }, [
|
||||
h('circle', { attrs: { cx: 50, cy: 50, r: 40, stroke: 'green', 'stroke-width': 4, fill: 'yellow' } })
|
||||
])
|
||||
])
|
||||
patch(container, vnode)
|
||||
})
|
||||
window.addEventListener("DOMContentLoaded", () => {
|
||||
const container = document.getElementById("container");
|
||||
const vnode = h("div", [
|
||||
h("svg", { attrs: { width: 100, height: 100 } }, [
|
||||
h("circle", {
|
||||
attrs: {
|
||||
cx: 50,
|
||||
cy: 50,
|
||||
r: 40,
|
||||
stroke: "green",
|
||||
"stroke-width": 4,
|
||||
fill: "yellow",
|
||||
},
|
||||
}),
|
||||
]),
|
||||
]);
|
||||
patch(container, vnode);
|
||||
});
|
||||
|
@ -1,48 +1,50 @@
|
||||
const chalk = require('chalk')
|
||||
const Table = require('tty-table')
|
||||
const chalk = require("chalk");
|
||||
const Table = require("tty-table");
|
||||
|
||||
exports['reporter:benchmark'] = ['type', BenchmarkReporter]
|
||||
exports["reporter:benchmark"] = ["type", BenchmarkReporter];
|
||||
|
||||
function BenchmarkReporter (baseReporterDecorator) {
|
||||
baseReporterDecorator(this)
|
||||
const resultsPerBrowser = new Map()
|
||||
function BenchmarkReporter(baseReporterDecorator) {
|
||||
baseReporterDecorator(this);
|
||||
const resultsPerBrowser = new Map();
|
||||
this.onBrowserInfo = function (browser, info) {
|
||||
if (!info.benchmark) return
|
||||
if (!info.benchmark) return;
|
||||
if (!resultsPerBrowser.has(browser.name)) {
|
||||
resultsPerBrowser.set(browser.name, info.benchmark)
|
||||
resultsPerBrowser.set(browser.name, info.benchmark);
|
||||
}
|
||||
}
|
||||
};
|
||||
this.onRunComplete = function () {
|
||||
if (resultsPerBrowser.size === 0) return
|
||||
this.writeCommonMsg(chalk.underline.bold('\nBENCHMARK (times in seconds):\n'))
|
||||
if (resultsPerBrowser.size === 0) return;
|
||||
this.writeCommonMsg(
|
||||
chalk.underline.bold("\nBENCHMARK (times in seconds):\n")
|
||||
);
|
||||
resultsPerBrowser.forEach((results, browserName) => {
|
||||
this.writeCommonMsg(` ${chalk.bold(browserName)}:\n`)
|
||||
this.writeCommonMsg(` ${chalk.bold(browserName)}:\n`);
|
||||
const rows = results.map(({ cur, ref }, i) => ({
|
||||
i: String(i),
|
||||
cur: cur.toFixed(0),
|
||||
ref: ref.toFixed(0),
|
||||
diff: `${(cur / ref * 100).toFixed(2)}%`,
|
||||
}))
|
||||
diff: `${((cur / ref) * 100).toFixed(2)}%`,
|
||||
}));
|
||||
const header = [
|
||||
{
|
||||
value: 'i',
|
||||
align: 'right'
|
||||
value: "i",
|
||||
align: "right",
|
||||
},
|
||||
{
|
||||
value: 'ref',
|
||||
align: 'right'
|
||||
value: "ref",
|
||||
align: "right",
|
||||
},
|
||||
{
|
||||
value: 'cur',
|
||||
align: 'right'
|
||||
value: "cur",
|
||||
align: "right",
|
||||
},
|
||||
{
|
||||
value: 'diff',
|
||||
align: 'right'
|
||||
value: "diff",
|
||||
align: "right",
|
||||
},
|
||||
]
|
||||
console.log(Table(header, rows).render())
|
||||
})
|
||||
resultsPerBrowser.clear()
|
||||
}
|
||||
];
|
||||
console.log(Table(header, rows).render());
|
||||
});
|
||||
resultsPerBrowser.clear();
|
||||
};
|
||||
}
|
||||
|
@ -1,56 +0,0 @@
|
||||
const ci = !!process.env.CI
|
||||
const watch = !!process.env.WATCH
|
||||
const live = !!process.env.LIVE
|
||||
|
||||
const ip = 'bs-local.com'
|
||||
|
||||
const browserstack = require('./browserstack-karma.cjs')
|
||||
|
||||
// https://www.browserstack.com/open-source (text search "parallels")
|
||||
const BROWSERSTACK_OPEN_SOURCE_CONCURRENCY = 5
|
||||
|
||||
const browsers = ci
|
||||
? Object.keys(browserstack)
|
||||
: live
|
||||
? undefined
|
||||
: watch
|
||||
? ['Chrome']
|
||||
: ['Chrome', 'Firefox']
|
||||
|
||||
module.exports = function (config) {
|
||||
config.set({
|
||||
basePath: '.',
|
||||
frameworks: ['mocha'],
|
||||
// list of files / patterns to load in the browser
|
||||
files: [
|
||||
{ pattern: process.env.FILES_PATTERN },
|
||||
],
|
||||
plugins: [
|
||||
'karma-mocha',
|
||||
require('karma-mocha-reporter'),
|
||||
require('./karma-benchmark-reporter.cjs'),
|
||||
'karma-chrome-launcher',
|
||||
'karma-firefox-launcher',
|
||||
'karma-browserstack-launcher',
|
||||
],
|
||||
hostname: ci ? ip : 'localhost',
|
||||
browserStack: {
|
||||
name: 'Snabbdom',
|
||||
retryLimit: 1,
|
||||
},
|
||||
client: {
|
||||
captureConsole: true,
|
||||
},
|
||||
customLaunchers: browserstack,
|
||||
reporters: ['mocha', 'benchmark', 'BrowserStack'],
|
||||
mochaReporter: {
|
||||
showDiff: true
|
||||
},
|
||||
port: 9876,
|
||||
colors: true,
|
||||
autoWatch: true,
|
||||
browsers: browsers,
|
||||
singleRun: !watch && !live,
|
||||
concurrency: ci ? BROWSERSTACK_OPEN_SOURCE_CONCURRENCY : Infinity,
|
||||
})
|
||||
}
|
@ -0,0 +1,78 @@
|
||||
const ci = !!process.env.CI;
|
||||
const watch = !!process.env.WATCH;
|
||||
const live = !!process.env.LIVE;
|
||||
const es5 = !!process.env.ES5;
|
||||
|
||||
const ip = "bs-local.com";
|
||||
|
||||
const browserstack = require("./browserstack-karma.js");
|
||||
|
||||
// https://www.browserstack.com/open-source (text search "parallels")
|
||||
const BROWSERSTACK_OPEN_SOURCE_CONCURRENCY = 5;
|
||||
|
||||
const getBrowserstackBrowsers = () =>
|
||||
Object.keys(browserstack).filter((k) => !!browserstack[k].es5 === es5);
|
||||
|
||||
const browsers = ci
|
||||
? getBrowserstackBrowsers()
|
||||
: live
|
||||
? undefined
|
||||
: watch
|
||||
? ["Chrome"]
|
||||
: ["ChromeHeadless", "FirefoxHeadless"];
|
||||
|
||||
module.exports = function (config) {
|
||||
config.set({
|
||||
basePath: ".",
|
||||
frameworks: ["mocha", "karma-typescript"],
|
||||
// list of files / patterns to load in the browser
|
||||
files: process.env.FILES_PATTERN.split(",")
|
||||
.map((p) => ({ pattern: p }))
|
||||
.concat({ pattern: "src/**/*.ts" }),
|
||||
preprocessors: {
|
||||
"**/*.ts": "karma-typescript",
|
||||
"**/*.tsx": "karma-typescript",
|
||||
},
|
||||
plugins: [
|
||||
"karma-mocha",
|
||||
"karma-typescript",
|
||||
"karma-mocha-reporter",
|
||||
require("./karma-benchmark-reporter.cjs"),
|
||||
"karma-chrome-launcher",
|
||||
"karma-firefox-launcher",
|
||||
"karma-browserstack-launcher",
|
||||
],
|
||||
hostname: ci ? ip : "localhost",
|
||||
karmaTypescriptConfig: {
|
||||
compilerOptions: {
|
||||
...require("./tsconfig.json").compilerOptions,
|
||||
...require("./test/tsconfig.json").compilerOptions,
|
||||
sourceMap: false,
|
||||
inlineSourceMap: true,
|
||||
target: es5 ? "es5" : "es6",
|
||||
},
|
||||
bundlerOptions: {
|
||||
sourceMap: true,
|
||||
},
|
||||
include: process.env.FILES_PATTERN.split(",").concat("src/**/*.ts"),
|
||||
},
|
||||
browserStack: {
|
||||
name: "Snabbdom",
|
||||
retryLimit: 1,
|
||||
},
|
||||
client: {
|
||||
captureConsole: true,
|
||||
},
|
||||
customLaunchers: browserstack,
|
||||
reporters: ["karma-typescript", "mocha", "benchmark", "BrowserStack"],
|
||||
mochaReporter: {
|
||||
showDiff: true,
|
||||
},
|
||||
port: 9876,
|
||||
colors: true,
|
||||
autoWatch: true,
|
||||
browsers: browsers,
|
||||
singleRun: !watch && !live,
|
||||
concurrency: ci ? BROWSERSTACK_OPEN_SOURCE_CONCURRENCY : Infinity,
|
||||
});
|
||||
};
|
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,81 @@
|
||||
import { vnode, VNode, VNodeData } from "./vnode";
|
||||
import * as is from "./is";
|
||||
|
||||
export type VNodes = VNode[];
|
||||
export type VNodeChildElement = VNode | string | number | undefined | null;
|
||||
export type ArrayOrElement<T> = T | T[];
|
||||
export type VNodeChildren = ArrayOrElement<VNodeChildElement>;
|
||||
|
||||
function addNS(
|
||||
data: any,
|
||||
children: VNodes | undefined,
|
||||
sel: string | undefined
|
||||
): void {
|
||||
data.ns = "http://www.w3.org/2000/svg";
|
||||
if (sel !== "foreignObject" && children !== undefined) {
|
||||
for (let i = 0; i < children.length; ++i) {
|
||||
const childData = children[i].data;
|
||||
if (childData !== undefined) {
|
||||
addNS(childData, children[i].children as VNodes, children[i].sel);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function h(sel: string): VNode;
|
||||
export function h(sel: string, data: VNodeData | null): VNode;
|
||||
export function h(sel: string, children: VNodeChildren): VNode;
|
||||
export function h(
|
||||
sel: string,
|
||||
data: VNodeData | null,
|
||||
children: VNodeChildren
|
||||
): VNode;
|
||||
export function h(sel: any, b?: any, c?: any): VNode {
|
||||
let data: VNodeData = {};
|
||||
let children: any;
|
||||
let text: any;
|
||||
let i: number;
|
||||
if (c !== undefined) {
|
||||
if (b !== null) {
|
||||
data = b;
|
||||
}
|
||||
if (is.array(c)) {
|
||||
children = c;
|
||||
} else if (is.primitive(c)) {
|
||||
text = c;
|
||||
} else if (c && c.sel) {
|
||||
children = [c];
|
||||
}
|
||||
} else if (b !== undefined && b !== null) {
|
||||
if (is.array(b)) {
|
||||
children = b;
|
||||
} else if (is.primitive(b)) {
|
||||
text = b;
|
||||
} else if (b && b.sel) {
|
||||
children = [b];
|
||||
} else {
|
||||
data = b;
|
||||
}
|
||||
}
|
||||
if (children !== undefined) {
|
||||
for (i = 0; i < children.length; ++i) {
|
||||
if (is.primitive(children[i]))
|
||||
children[i] = vnode(
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
children[i],
|
||||
undefined
|
||||
);
|
||||
}
|
||||
}
|
||||
if (
|
||||
sel[0] === "s" &&
|
||||
sel[1] === "v" &&
|
||||
sel[2] === "g" &&
|
||||
(sel.length === 3 || sel[3] === "." || sel[3] === "#")
|
||||
) {
|
||||
addNS(data, children, sel);
|
||||
}
|
||||
return vnode(sel, data, children, text, undefined);
|
||||
}
|
@ -0,0 +1,64 @@
|
||||
import { VNode, VNodeData } from "../vnode";
|
||||
|
||||
export interface AttachData {
|
||||
[key: string]: any;
|
||||
[i: number]: any;
|
||||
placeholder?: any;
|
||||
real?: Node;
|
||||
}
|
||||
|
||||
interface VNodeDataWithAttach extends VNodeData {
|
||||
attachData: AttachData;
|
||||
}
|
||||
|
||||
interface VNodeWithAttachData extends VNode {
|
||||
data: VNodeDataWithAttach;
|
||||
}
|
||||
|
||||
function pre(vnode: VNodeWithAttachData, newVnode: VNodeWithAttachData): void {
|
||||
const attachData = vnode.data.attachData;
|
||||
// Copy created placeholder and real element from old vnode
|
||||
newVnode.data.attachData.placeholder = attachData.placeholder;
|
||||
newVnode.data.attachData.real = attachData.real;
|
||||
// Mount real element in vnode so the patch process operates on it
|
||||
vnode.elm = vnode.data.attachData.real;
|
||||
}
|
||||
|
||||
function post(_: any, vnode: VNodeWithAttachData): void {
|
||||
// Mount dummy placeholder in vnode so potential reorders use it
|
||||
vnode.elm = vnode.data.attachData.placeholder;
|
||||
}
|
||||
|
||||
function destroy(vnode: VNodeWithAttachData): void {
|
||||
// Remove placeholder
|
||||
if (vnode.elm !== undefined) {
|
||||
(vnode.elm.parentNode as HTMLElement).removeChild(vnode.elm);
|
||||
}
|
||||
// Remove real element from where it was inserted
|
||||
vnode.elm = vnode.data.attachData.real;
|
||||
}
|
||||
|
||||
function create(_: any, vnode: VNodeWithAttachData): void {
|
||||
const real = vnode.elm;
|
||||
const attachData = vnode.data.attachData;
|
||||
const placeholder = document.createElement("span");
|
||||
// Replace actual element with dummy placeholder
|
||||
// Snabbdom will then insert placeholder instead
|
||||
vnode.elm = placeholder;
|
||||
attachData.target.appendChild(real);
|
||||
attachData.real = real;
|
||||
attachData.placeholder = placeholder;
|
||||
}
|
||||
|
||||
export function attachTo(target: Element, vnode: VNode): VNode {
|
||||
if (vnode.data === undefined) vnode.data = {};
|
||||
if (vnode.data.hook === undefined) vnode.data.hook = {};
|
||||
const data = vnode.data;
|
||||
const hook = vnode.data.hook;
|
||||
data.attachData = { target: target, placeholder: undefined, real: undefined };
|
||||
hook.create = create;
|
||||
hook.prepatch = pre;
|
||||
hook.postpatch = post;
|
||||
hook.destroy = destroy;
|
||||
return vnode;
|
||||
}
|
@ -0,0 +1,25 @@
|
||||
import { VNode } from "./vnode";
|
||||
|
||||
export type PreHook = () => any;
|
||||
export type InitHook = (vNode: VNode) => any;
|
||||
export type CreateHook = (emptyVNode: VNode, vNode: VNode) => any;
|
||||
export type InsertHook = (vNode: VNode) => any;
|
||||
export type PrePatchHook = (oldVNode: VNode, vNode: VNode) => any;
|
||||
export type UpdateHook = (oldVNode: VNode, vNode: VNode) => any;
|
||||
export type PostPatchHook = (oldVNode: VNode, vNode: VNode) => any;
|
||||
export type DestroyHook = (vNode: VNode) => any;
|
||||
export type RemoveHook = (vNode: VNode, removeCallback: () => void) => any;
|
||||
export type PostHook = () => any;
|
||||
|
||||
export interface Hooks {
|
||||
pre?: PreHook;
|
||||
init?: InitHook;
|
||||
create?: CreateHook;
|
||||
insert?: InsertHook;
|
||||
prepatch?: PrePatchHook;
|
||||
update?: UpdateHook;
|
||||
postpatch?: PostPatchHook;
|
||||
destroy?: DestroyHook;
|
||||
remove?: RemoveHook;
|
||||
post?: PostHook;
|
||||
}
|
@ -0,0 +1,117 @@
|
||||
export interface DOMAPI {
|
||||
createElement: (
|
||||
tagName: any,
|
||||
options?: ElementCreationOptions
|
||||
) => HTMLElement;
|
||||
createElementNS: (
|
||||
namespaceURI: string,
|
||||
qualifiedName: string,
|
||||
options?: ElementCreationOptions
|
||||
) => Element;
|
||||
createTextNode: (text: string) => Text;
|
||||
createComment: (text: string) => Comment;
|
||||
insertBefore: (
|
||||
parentNode: Node,
|
||||
newNode: Node,
|
||||
referenceNode: Node | null
|
||||
) => void;
|
||||
removeChild: (node: Node, child: Node) => void;
|
||||
appendChild: (node: Node, child: Node) => void;
|
||||
parentNode: (node: Node) => Node | null;
|
||||
nextSibling: (node: Node) => Node | null;
|
||||
tagName: (elm: Element) => string;
|
||||
setTextContent: (node: Node, text: string | null) => void;
|
||||
getTextContent: (node: Node) => string | null;
|
||||
isElement: (node: Node) => node is Element;
|
||||
isText: (node: Node) => node is Text;
|
||||
isComment: (node: Node) => node is Comment;
|
||||
}
|
||||
|
||||
function createElement(
|
||||
tagName: any,
|
||||
options?: ElementCreationOptions
|
||||
): HTMLElement {
|
||||
return document.createElement(tagName, options);
|
||||
}
|
||||
|
||||
function createElementNS(
|
||||
namespaceURI: string,
|
||||
qualifiedName: string,
|
||||
options?: ElementCreationOptions
|
||||
): Element {
|
||||
return document.createElementNS(namespaceURI, qualifiedName, options);
|
||||
}
|
||||
|
||||
function createTextNode(text: string): Text {
|
||||
return document.createTextNode(text);
|
||||
}
|
||||
|
||||
function createComment(text: string): Comment {
|
||||
return document.createComment(text);
|
||||
}
|
||||
|
||||
function insertBefore(
|
||||
parentNode: Node,
|
||||
newNode: Node,
|
||||
referenceNode: Node | null
|
||||
): void {
|
||||
parentNode.insertBefore(newNode, referenceNode);
|
||||
}
|
||||
|
||||
function removeChild(node: Node, child: Node): void {
|
||||
node.removeChild(child);
|
||||
}
|
||||
|
||||
function appendChild(node: Node, child: Node): void {
|
||||
node.appendChild(child);
|
||||
}
|
||||
|
||||
function parentNode(node: Node): Node | null {
|
||||
return node.parentNode;
|
||||
}
|
||||
|
||||
function nextSibling(node: Node): Node | null {
|
||||
return node.nextSibling;
|
||||
}
|
||||
|
||||
function tagName(elm: Element): string {
|
||||
return elm.tagName;
|
||||
}
|
||||
|
||||
function setTextContent(node: Node, text: string | null): void {
|
||||
node.textContent = text;
|
||||
}
|
||||
|
||||
function getTextContent(node: Node): string | null {
|
||||
return node.textContent;
|
||||
}
|
||||
|
||||
function isElement(node: Node): node is Element {
|
||||
return node.nodeType === 1;
|
||||
}
|
||||
|
||||
function isText(node: Node): node is Text {
|
||||
return node.nodeType === 3;
|
||||
}
|
||||
|
||||
function isComment(node: Node): node is Comment {
|
||||
return node.nodeType === 8;
|
||||
}
|
||||
|
||||
export const htmlDomApi: DOMAPI = {
|
||||
createElement,
|
||||
createElementNS,
|
||||
createTextNode,
|
||||
createComment,
|
||||
insertBefore,
|
||||
removeChild,
|
||||
appendChild,
|
||||
parentNode,
|
||||
nextSibling,
|
||||
tagName,
|
||||
setTextContent,
|
||||
getTextContent,
|
||||
isElement,
|
||||
isText,
|
||||
isComment,
|
||||
};
|
@ -0,0 +1,33 @@
|
||||
// core
|
||||
export { DOMAPI, htmlDomApi } from "./htmldomapi";
|
||||
export { init } from "./init";
|
||||
export { ThunkData, Thunk, ThunkFn, thunk } from "./thunk";
|
||||
export { Key, VNode, VNodeData, vnode } from "./vnode";
|
||||
|
||||
// helpers
|
||||
export { AttachData, attachTo } from "./helpers/attachto";
|
||||
export { array, primitive } from "./is";
|
||||
export { toVNode } from "./tovnode";
|
||||
export {
|
||||
VNodes,
|
||||
VNodeChildElement,
|
||||
ArrayOrElement,
|
||||
VNodeChildren,
|
||||
h,
|
||||
} from "./h";
|
||||
|
||||
// types
|
||||
export * from "./hooks";
|
||||
export { Module } from "./modules/module";
|
||||
|
||||
// modules
|
||||
export { Attrs, attributesModule } from "./modules/attributes";
|
||||
export { Classes, classModule } from "./modules/class";
|
||||
export { Dataset, datasetModule } from "./modules/dataset";
|
||||
export { On, eventListenersModule } from "./modules/eventlisteners";
|
||||
export { Hero, heroModule } from "./modules/hero";
|
||||
export { Props, propsModule } from "./modules/props";
|
||||
export { VNodeStyle, styleModule } from "./modules/style";
|
||||
|
||||
// JSX
|
||||
export { JsxVNodeChild, JsxVNodeChildren, FunctionComponent, jsx } from "./jsx";
|
@ -0,0 +1,391 @@
|
||||
import { Module } from "./modules/module";
|
||||
import { vnode, VNode } from "./vnode";
|
||||
import * as is from "./is";
|
||||
import { htmlDomApi, DOMAPI } from "./htmldomapi";
|
||||
|
||||
type NonUndefined<T> = T extends undefined ? never : T;
|
||||
|
||||
function isUndef(s: any): boolean {
|
||||
return s === undefined;
|
||||
}
|
||||
function isDef<A>(s: A): s is NonUndefined<A> {
|
||||
return s !== undefined;
|
||||
}
|
||||
|
||||
type VNodeQueue = VNode[];
|
||||
|
||||
const emptyNode = vnode("", {}, [], undefined, undefined);
|
||||
|
||||
function sameVnode(vnode1: VNode, vnode2: VNode): boolean {
|
||||
const isSameKey = vnode1.key === vnode2.key;
|
||||
const isSameIs = vnode1.data?.is === vnode2.data?.is;
|
||||
const isSameSel = vnode1.sel === vnode2.sel;
|
||||
|
||||
return isSameSel && isSameKey && isSameIs;
|
||||
}
|
||||
|
||||
function isVnode(vnode: any): vnode is VNode {
|
||||
return vnode.sel !== undefined;
|
||||
}
|
||||
|
||||
type KeyToIndexMap = { [key: string]: number };
|
||||
|
||||
type ArraysOf<T> = {
|
||||
[K in keyof T]: Array<T[K]>;
|
||||
};
|
||||
|
||||
type ModuleHooks = ArraysOf<Required<Module>>;
|
||||
|
||||
function createKeyToOldIdx(
|
||||
children: VNode[],
|
||||
beginIdx: number,
|
||||
endIdx: number
|
||||
): KeyToIndexMap {
|
||||
const map: KeyToIndexMap = {};
|
||||
for (let i = beginIdx; i <= endIdx; ++i) {
|
||||
const key = children[i]?.key;
|
||||
if (key !== undefined) {
|
||||
map[key] = i;
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
const hooks: Array<keyof Module> = [
|
||||
"create",
|
||||
"update",
|
||||
"remove",
|
||||
"destroy",
|
||||
"pre",
|
||||
"post",
|
||||
];
|
||||
|
||||
export function init(modules: Array<Partial<Module>>, domApi?: DOMAPI) {
|
||||
let i: number;
|
||||
let j: number;
|
||||
const cbs: ModuleHooks = {
|
||||
create: [],
|
||||
update: [],
|
||||
remove: [],
|
||||
destroy: [],
|
||||
pre: [],
|
||||
post: [],
|
||||
};
|
||||
|
||||
const api: DOMAPI = domApi !== undefined ? domApi : htmlDomApi;
|
||||
|
||||
for (i = 0; i < hooks.length; ++i) {
|
||||
cbs[hooks[i]] = [];
|
||||
for (j = 0; j < modules.length; ++j) {
|
||||
const hook = modules[j][hooks[i]];
|
||||
if (hook !== undefined) {
|
||||
(cbs[hooks[i]] as any[]).push(hook);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function emptyNodeAt(elm: Element) {
|
||||
const id = elm.id ? "#" + elm.id : "";
|
||||
const c = elm.className ? "." + elm.className.split(" ").join(".") : "";
|
||||
return vnode(
|
||||
api.tagName(elm).toLowerCase() + id + c,
|
||||
{},
|
||||
[],
|
||||
undefined,
|
||||
elm
|
||||
);
|
||||
}
|
||||
|
||||
function createRmCb(childElm: Node, listeners: number) {
|
||||
return function rmCb() {
|
||||
if (--listeners === 0) {
|
||||
const parent = api.parentNode(childElm) as Node;
|
||||
api.removeChild(parent, childElm);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function createElm(vnode: VNode, insertedVnodeQueue: VNodeQueue): Node {
|
||||
let i: any;
|
||||
let data = vnode.data;
|
||||
if (data !== undefined) {
|
||||
const init = data.hook?.init;
|
||||
if (isDef(init)) {
|
||||
init(vnode);
|
||||
data = vnode.data;
|
||||
}
|
||||
}
|
||||
const children = vnode.children;
|
||||
const sel = vnode.sel;
|
||||
if (sel === "!") {
|
||||
if (isUndef(vnode.text)) {
|
||||
vnode.text = "";
|
||||
}
|
||||
vnode.elm = api.createComment(vnode.text!);
|
||||
} else if (sel !== undefined) {
|
||||
// Parse selector
|
||||
const hashIdx = sel.indexOf("#");
|
||||
const dotIdx = sel.indexOf(".", hashIdx);
|
||||
const hash = hashIdx > 0 ? hashIdx : sel.length;
|
||||
const dot = dotIdx > 0 ? dotIdx : sel.length;
|
||||
const tag =
|
||||
hashIdx !== -1 || dotIdx !== -1
|
||||
? sel.slice(0, Math.min(hash, dot))
|
||||
: sel;
|
||||
const elm = (vnode.elm =
|
||||
isDef(data) && isDef((i = data.ns))
|
||||
? api.createElementNS(i, tag, data)
|
||||
: api.createElement(tag, data));
|
||||
if (hash < dot) elm.setAttribute("id", sel.slice(hash + 1, dot));
|
||||
if (dotIdx > 0)
|
||||
elm.setAttribute("class", sel.slice(dot + 1).replace(/\./g, " "));
|
||||
for (i = 0; i < cbs.create.length; ++i) cbs.create[i](emptyNode, vnode);
|
||||
if (is.array(children)) {
|
||||
for (i = 0; i < children.length; ++i) {
|
||||
const ch = children[i];
|
||||
if (ch != null) {
|
||||
api.appendChild(elm, createElm(ch as VNode, insertedVnodeQueue));
|
||||
}
|
||||
}
|
||||
} else if (is.primitive(vnode.text)) {
|
||||
api.appendChild(elm, api.createTextNode(vnode.text));
|
||||
}
|
||||
const hook = vnode.data!.hook;
|
||||
if (isDef(hook)) {
|
||||
hook.create?.(emptyNode, vnode);
|
||||
if (hook.insert) {
|
||||
insertedVnodeQueue.push(vnode);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
vnode.elm = api.createTextNode(vnode.text!);
|
||||
}
|
||||
return vnode.elm;
|
||||
}
|
||||
|
||||
function addVnodes(
|
||||
parentElm: Node,
|
||||
before: Node | null,
|
||||
vnodes: VNode[],
|
||||
startIdx: number,
|
||||
endIdx: number,
|
||||
insertedVnodeQueue: VNodeQueue
|
||||
) {
|
||||
for (; startIdx <= endIdx; ++startIdx) {
|
||||
const ch = vnodes[startIdx];
|
||||
if (ch != null) {
|
||||
api.insertBefore(parentElm, createElm(ch, insertedVnodeQueue), before);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function invokeDestroyHook(vnode: VNode) {
|
||||
const data = vnode.data;
|
||||
if (data !== undefined) {
|
||||
data?.hook?.destroy?.(vnode);
|
||||
for (let i = 0; i < cbs.destroy.length; ++i) cbs.destroy[i](vnode);
|
||||
if (vnode.children !== undefined) {
|
||||
for (let j = 0; j < vnode.children.length; ++j) {
|
||||
const child = vnode.children[j];
|
||||
if (child != null && typeof child !== "string") {
|
||||
invokeDestroyHook(child);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function removeVnodes(
|
||||
parentElm: Node,
|
||||
vnodes: VNode[],
|
||||
startIdx: number,
|
||||
endIdx: number
|
||||
): void {
|
||||
for (; startIdx <= endIdx; ++startIdx) {
|
||||
let listeners: number;
|
||||
let rm: () => void;
|
||||
const ch = vnodes[startIdx];
|
||||
if (ch != null) {
|
||||
if (isDef(ch.sel)) {
|
||||
invokeDestroyHook(ch);
|
||||
listeners = cbs.remove.length + 1;
|
||||
rm = createRmCb(ch.elm!, listeners);
|
||||
for (let i = 0; i < cbs.remove.length; ++i) cbs.remove[i](ch, rm);
|
||||
const removeHook = ch?.data?.hook?.remove;
|
||||
if (isDef(removeHook)) {
|
||||
removeHook(ch, rm);
|
||||
} else {
|
||||
rm();
|
||||
}
|
||||
} else {
|
||||
// Text node
|
||||
api.removeChild(parentElm, ch.elm!);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function updateChildren(
|
||||
parentElm: Node,
|
||||
oldCh: VNode[],
|
||||
newCh: VNode[],
|
||||
insertedVnodeQueue: VNodeQueue
|
||||
) {
|
||||
let oldStartIdx = 0;
|
||||
let newStartIdx = 0;
|
||||
let oldEndIdx = oldCh.length - 1;
|
||||
let oldStartVnode = oldCh[0];
|
||||
let oldEndVnode = oldCh[oldEndIdx];
|
||||
let newEndIdx = newCh.length - 1;
|
||||
let newStartVnode = newCh[0];
|
||||
let newEndVnode = newCh[newEndIdx];
|
||||
let oldKeyToIdx: KeyToIndexMap | undefined;
|
||||
let idxInOld: number;
|
||||
let elmToMove: VNode;
|
||||
let before: any;
|
||||
|
||||
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
|
||||
if (oldStartVnode == null) {
|
||||
oldStartVnode = oldCh[++oldStartIdx]; // Vnode might have been moved left
|
||||
} else if (oldEndVnode == null) {
|
||||
oldEndVnode = oldCh[--oldEndIdx];
|
||||
} else if (newStartVnode == null) {
|
||||
newStartVnode = newCh[++newStartIdx];
|
||||
} else if (newEndVnode == null) {
|
||||
newEndVnode = newCh[--newEndIdx];
|
||||
} else if (sameVnode(oldStartVnode, newStartVnode)) {
|
||||
patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue);
|
||||
oldStartVnode = oldCh[++oldStartIdx];
|
||||
newStartVnode = newCh[++newStartIdx];
|
||||
} else if (sameVnode(oldEndVnode, newEndVnode)) {
|
||||
patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue);
|
||||
oldEndVnode = oldCh[--oldEndIdx];
|
||||
newEndVnode = newCh[--newEndIdx];
|
||||
} else if (sameVnode(oldStartVnode, newEndVnode)) {
|
||||
// Vnode moved right
|
||||
patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue);
|
||||
api.insertBefore(
|
||||
parentElm,
|
||||
oldStartVnode.elm!,
|
||||
api.nextSibling(oldEndVnode.elm!)
|
||||
);
|
||||
oldStartVnode = oldCh[++oldStartIdx];
|
||||
newEndVnode = newCh[--newEndIdx];
|
||||
} else if (sameVnode(oldEndVnode, newStartVnode)) {
|
||||
// Vnode moved left
|
||||
patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue);
|
||||
api.insertBefore(parentElm, oldEndVnode.elm!, oldStartVnode.elm!);
|
||||
oldEndVnode = oldCh[--oldEndIdx];
|
||||
newStartVnode = newCh[++newStartIdx];
|
||||
} else {
|
||||
if (oldKeyToIdx === undefined) {
|
||||
oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx);
|
||||
}
|
||||
idxInOld = oldKeyToIdx[newStartVnode.key as string];
|
||||
if (isUndef(idxInOld)) {
|
||||
// New element
|
||||
api.insertBefore(
|
||||
parentElm,
|
||||
createElm(newStartVnode, insertedVnodeQueue),
|
||||
oldStartVnode.elm!
|
||||
);
|
||||
} else {
|
||||
elmToMove = oldCh[idxInOld];
|
||||
if (elmToMove.sel !== newStartVnode.sel) {
|
||||
api.insertBefore(
|
||||
parentElm,
|
||||
createElm(newStartVnode, insertedVnodeQueue),
|
||||
oldStartVnode.elm!
|
||||
);
|
||||
} else {
|
||||
patchVnode(elmToMove, newStartVnode, insertedVnodeQueue);
|
||||
oldCh[idxInOld] = undefined as any;
|
||||
api.insertBefore(parentElm, elmToMove.elm!, oldStartVnode.elm!);
|
||||
}
|
||||
}
|
||||
newStartVnode = newCh[++newStartIdx];
|
||||
}
|
||||
}
|
||||
if (oldStartIdx <= oldEndIdx || newStartIdx <= newEndIdx) {
|
||||
if (oldStartIdx > oldEndIdx) {
|
||||
before = newCh[newEndIdx + 1] == null ? null : newCh[newEndIdx + 1].elm;
|
||||
addVnodes(
|
||||
parentElm,
|
||||
before,
|
||||
newCh,
|
||||
newStartIdx,
|
||||
newEndIdx,
|
||||
insertedVnodeQueue
|
||||
);
|
||||
} else {
|
||||
removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function patchVnode(
|
||||
oldVnode: VNode,
|
||||
vnode: VNode,
|
||||
insertedVnodeQueue: VNodeQueue
|
||||
) {
|
||||
const hook = vnode.data?.hook;
|
||||
hook?.prepatch?.(oldVnode, vnode);
|
||||
const elm = (vnode.elm = oldVnode.elm)!;
|
||||
const oldCh = oldVnode.children as VNode[];
|
||||
const ch = vnode.children as VNode[];
|
||||
if (oldVnode === vnode) return;
|
||||
if (vnode.data !== undefined) {
|
||||
for (let i = 0; i < cbs.update.length; ++i)
|
||||
cbs.update[i](oldVnode, vnode);
|
||||
vnode.data.hook?.update?.(oldVnode, vnode);
|
||||
}
|
||||
if (isUndef(vnode.text)) {
|
||||
if (isDef(oldCh) && isDef(ch)) {
|
||||
if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue);
|
||||
} else if (isDef(ch)) {
|
||||
if (isDef(oldVnode.text)) api.setTextContent(elm, "");
|
||||
addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue);
|
||||
} else if (isDef(oldCh)) {
|
||||
removeVnodes(elm, oldCh, 0, oldCh.length - 1);
|
||||
} else if (isDef(oldVnode.text)) {
|
||||
api.setTextContent(elm, "");
|
||||
}
|
||||
} else if (oldVnode.text !== vnode.text) {
|
||||
if (isDef(oldCh)) {
|
||||
removeVnodes(elm, oldCh, 0, oldCh.length - 1);
|
||||
}
|
||||
api.setTextContent(elm, vnode.text!);
|
||||
}
|
||||
hook?.postpatch?.(oldVnode, vnode);
|
||||
}
|
||||
|
||||
return function patch(oldVnode: VNode | Element, vnode: VNode): VNode {
|
||||
let i: number, elm: Node, parent: Node;
|
||||
const insertedVnodeQueue: VNodeQueue = [];
|
||||
for (i = 0; i < cbs.pre.length; ++i) cbs.pre[i]();
|
||||
|
||||
if (!isVnode(oldVnode)) {
|
||||
oldVnode = emptyNodeAt(oldVnode);
|
||||
}
|
||||
|
||||
if (sameVnode(oldVnode, vnode)) {
|
||||
patchVnode(oldVnode, vnode, insertedVnodeQueue);
|
||||
} else {
|
||||
elm = oldVnode.elm!;
|
||||
parent = api.parentNode(elm) as Node;
|
||||
|
||||
createElm(vnode, insertedVnodeQueue);
|
||||
|
||||
if (parent !== null) {
|
||||
api.insertBefore(parent, vnode.elm!, api.nextSibling(elm));
|
||||
removeVnodes(parent, [oldVnode], 0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
for (i = 0; i < insertedVnodeQueue.length; ++i) {
|
||||
insertedVnodeQueue[i].data!.hook!.insert!(insertedVnodeQueue[i]);
|
||||
}
|
||||
for (i = 0; i < cbs.post.length; ++i) cbs.post[i]();
|
||||
return vnode;
|
||||
};
|
||||
}
|
@ -0,0 +1,4 @@
|
||||
export const array = Array.isArray;
|
||||
export function primitive(s: any): s is string | number {
|
||||
return typeof s === "string" || typeof s === "number";
|
||||
}
|
@ -0,0 +1,87 @@
|
||||
/* eslint-disable @typescript-eslint/no-namespace, import/export */
|
||||
import { vnode, VNode, VNodeData } from "./vnode";
|
||||
import { h, ArrayOrElement } from "./h";
|
||||
|
||||
// See https://www.typescriptlang.org/docs/handbook/jsx.html#type-checking
|
||||
namespace JSXInternal {
|
||||
export type Element = VNode;
|
||||
export interface IntrinsicElements {
|
||||
[elemName: string]: VNodeData;
|
||||
}
|
||||
}
|
||||
|
||||
// for conditional rendering we support boolean child element e.g cond && <tag />
|
||||
export type JsxVNodeChild =
|
||||
| VNode
|
||||
| string
|
||||
| number
|
||||
| boolean
|
||||
| undefined
|
||||
| null;
|
||||
export type JsxVNodeChildren = ArrayOrElement<JsxVNodeChild>;
|
||||
|
||||
export type FunctionComponent = (
|
||||
props: { [prop: string]: any } | null,
|
||||
children?: VNode[]
|
||||
) => VNode;
|
||||
|
||||
function flattenAndFilter(
|
||||
children: JsxVNodeChildren[],
|
||||
flattened: VNode[]
|
||||
): VNode[] {
|
||||
for (const child of children) {
|
||||
// filter out falsey children, except 0 since zero can be a valid value e.g inside a chart
|
||||
if (
|
||||
child !== undefined &&
|
||||
child !== null &&
|
||||
child !== false &&
|
||||
child !== ""
|
||||
) {
|
||||
if (Array.isArray(child)) {
|
||||
flattenAndFilter(child, flattened);
|
||||
} else if (
|
||||
typeof child === "string" ||
|
||||
typeof child === "number" ||
|
||||
typeof child === "boolean"
|
||||
) {
|
||||
flattened.push(
|
||||
vnode(undefined, undefined, undefined, String(child), undefined)
|
||||
);
|
||||
} else {
|
||||
flattened.push(child);
|
||||
}
|
||||
}
|
||||
}
|
||||
return flattened;
|
||||
}
|
||||
|
||||
/**
|
||||
* jsx/tsx compatible factory function
|
||||
* see: https://www.typescriptlang.org/docs/handbook/jsx.html#factory-functions
|
||||
*/
|
||||
export function jsx(
|
||||
tag: string | FunctionComponent,
|
||||
data: VNodeData | null,
|
||||
...children: JsxVNodeChildren[]
|
||||
): VNode {
|
||||
const flatChildren = flattenAndFilter(children, []);
|
||||
if (typeof tag === "function") {
|
||||
// tag is a function component
|
||||
return tag(data, flatChildren);
|
||||
} else {
|
||||
if (
|
||||
flatChildren.length === 1 &&
|
||||
!flatChildren[0].sel &&
|
||||
flatChildren[0].text
|
||||
) {
|
||||
// only child is a simple text node, pass as text for a simpler vtree
|
||||
return h(tag, data, flatChildren[0].text);
|
||||
} else {
|
||||
return h(tag, data, flatChildren);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export namespace jsx {
|
||||
export import JSX = JSXInternal; // eslint-disable-line @typescript-eslint/no-unused-vars
|
||||
}
|
@ -0,0 +1,59 @@
|
||||
import { VNode, VNodeData } from "../vnode";
|
||||
import { Module } from "./module";
|
||||
|
||||
export type Attrs = Record<string, string | number | boolean>;
|
||||
|
||||
const xlinkNS = "http://www.w3.org/1999/xlink";
|
||||
const xmlNS = "http://www.w3.org/XML/1998/namespace";
|
||||
const colonChar = 58;
|
||||
const xChar = 120;
|
||||
|
||||
function updateAttrs(oldVnode: VNode, vnode: VNode): void {
|
||||
let key: string;
|
||||
const elm: Element = vnode.elm as Element;
|
||||
let oldAttrs = (oldVnode.data as VNodeData).attrs;
|
||||
let attrs = (vnode.data as VNodeData).attrs;
|
||||
|
||||
if (!oldAttrs && !attrs) return;
|
||||
if (oldAttrs === attrs) return;
|
||||
oldAttrs = oldAttrs || {};
|
||||
attrs = attrs || {};
|
||||
|
||||
// update modified attributes, add new attributes
|
||||
for (key in attrs) {
|
||||
const cur = attrs[key];
|
||||
const old = oldAttrs[key];
|
||||
if (old !== cur) {
|
||||
if (cur === true) {
|
||||
elm.setAttribute(key, "");
|
||||
} else if (cur === false) {
|
||||
elm.removeAttribute(key);
|
||||
} else {
|
||||
if (key.charCodeAt(0) !== xChar) {
|
||||
elm.setAttribute(key, cur as any);
|
||||
} else if (key.charCodeAt(3) === colonChar) {
|
||||
// Assume xml namespace
|
||||
elm.setAttributeNS(xmlNS, key, cur as any);
|
||||
} else if (key.charCodeAt(5) === colonChar) {
|
||||
// Assume xlink namespace
|
||||
elm.setAttributeNS(xlinkNS, key, cur as any);
|
||||
} else {
|
||||
elm.setAttribute(key, cur as any);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// remove removed attributes
|
||||
// use `in` operator since the previous `for` iteration uses it (.i.e. add even attributes with undefined value)
|
||||
// the other option is to remove all attributes with value == undefined
|
||||
for (key in oldAttrs) {
|
||||
if (!(key in attrs)) {
|
||||
elm.removeAttribute(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const attributesModule: Module = {
|
||||
create: updateAttrs,
|
||||
update: updateAttrs,
|
||||
};
|
@ -0,0 +1,32 @@
|
||||
import { VNode, VNodeData } from "../vnode";
|
||||
import { Module } from "./module";
|
||||
|
||||
export type Classes = Record<string, boolean>;
|
||||
|
||||
function updateClass(oldVnode: VNode, vnode: VNode): void {
|
||||
let cur: any;
|
||||
let name: string;
|
||||
const elm: Element = vnode.elm as Element;
|
||||
let oldClass = (oldVnode.data as VNodeData).class;
|
||||
let klass = (vnode.data as VNodeData).class;
|
||||
|
||||
if (!oldClass && !klass) return;
|
||||
if (oldClass === klass) return;
|
||||
oldClass = oldClass || {};
|
||||
klass = klass || {};
|
||||
|
||||
for (name in oldClass) {
|
||||
if (oldClass[name] && !Object.prototype.hasOwnProperty.call(klass, name)) {
|
||||
// was `true` and now not provided
|
||||
elm.classList.remove(name);
|
||||
}
|
||||
}
|
||||
for (name in klass) {
|
||||
cur = klass[name];
|
||||
if (cur !== oldClass[name]) {
|
||||
(elm.classList as any)[cur ? "add" : "remove"](name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const classModule: Module = { create: updateClass, update: updateClass };
|
@ -0,0 +1,50 @@
|
||||
import { VNode, VNodeData } from "../vnode";
|
||||
import { Module } from "./module";
|
||||
|
||||
export type Dataset = Record<string, string>;
|
||||
|
||||
const CAPS_REGEX = /[A-Z]/g;
|
||||
|
||||
function updateDataset(oldVnode: VNode, vnode: VNode): void {
|
||||
const elm: HTMLElement = vnode.elm as HTMLElement;
|
||||
let oldDataset = (oldVnode.data as VNodeData).dataset;
|
||||
let dataset = (vnode.data as VNodeData).dataset;
|
||||
let key: string;
|
||||
|
||||
if (!oldDataset && !dataset) return;
|
||||
if (oldDataset === dataset) return;
|
||||
oldDataset = oldDataset || {};
|
||||
dataset = dataset || {};
|
||||
const d = elm.dataset;
|
||||
|
||||
for (key in oldDataset) {
|
||||
if (!dataset[key]) {
|
||||
if (d) {
|
||||
if (key in d) {
|
||||
delete d[key];
|
||||
}
|
||||
} else {
|
||||
elm.removeAttribute(
|
||||
"data-" + key.replace(CAPS_REGEX, "-$&").toLowerCase()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
for (key in dataset) {
|
||||
if (oldDataset[key] !== dataset[key]) {
|
||||
if (d) {
|
||||
d[key] = dataset[key];
|
||||
} else {
|
||||
elm.setAttribute(
|
||||
"data-" + key.replace(CAPS_REGEX, "-$&").toLowerCase(),
|
||||
dataset[key]
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const datasetModule: Module = {
|
||||
create: updateDataset,
|
||||
update: updateDataset,
|
||||
};
|
@ -0,0 +1,110 @@
|
||||
import { VNode, VNodeData } from "../vnode";
|
||||
import { Module } from "./module";
|
||||
|
||||
type Listener<T> = (this: VNode, ev: T, vnode: VNode) => void;
|
||||
|
||||
export type On = {
|
||||
[N in keyof HTMLElementEventMap]?:
|
||||
| Listener<HTMLElementEventMap[N]>
|
||||
| Array<Listener<HTMLElementEventMap[N]>>;
|
||||
} & {
|
||||
[event: string]: Listener<any> | Array<Listener<any>>;
|
||||
};
|
||||
|
||||
type SomeListener<N extends keyof HTMLElementEventMap> =
|
||||
| Listener<HTMLElementEventMap[N]>
|
||||
| Listener<any>;
|
||||
|
||||
function invokeHandler<N extends keyof HTMLElementEventMap>(
|
||||
handler: SomeListener<N> | Array<SomeListener<N>>,
|
||||
vnode: VNode,
|
||||
event?: Event
|
||||
): void {
|
||||
if (typeof handler === "function") {
|
||||
// call function handler
|
||||
handler.call(vnode, event, vnode);
|
||||
} else if (typeof handler === "object") {
|
||||
// call multiple handlers
|
||||
for (let i = 0; i < handler.length; i++) {
|
||||
invokeHandler(handler[i], vnode, event);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function handleEvent(event: Event, vnode: VNode) {
|
||||
const name = event.type;
|
||||
const on = (vnode.data as VNodeData).on;
|
||||
|
||||
// call event handler(s) if exists
|
||||
if (on && on[name]) {
|
||||
invokeHandler(on[name], vnode, event);
|
||||
}
|
||||
}
|
||||
|
||||
function createListener() {
|
||||
return function handler(event: Event) {
|
||||
handleEvent(event, (handler as any).vnode);
|
||||
};
|
||||
}
|
||||
|
||||
function updateEventListeners(oldVnode: VNode, vnode?: VNode): void {
|
||||
const oldOn = (oldVnode.data as VNodeData).on;
|
||||
const oldListener = (oldVnode as any).listener;
|
||||
const oldElm: Element = oldVnode.elm as Element;
|
||||
const on = vnode && (vnode.data as VNodeData).on;
|
||||
const elm: Element = (vnode && vnode.elm) as Element;
|
||||
let name: string;
|
||||
|
||||
// optimization for reused immutable handlers
|
||||
if (oldOn === on) {
|
||||
return;
|
||||
}
|
||||
|
||||
// remove existing listeners which no longer used
|
||||
if (oldOn && oldListener) {
|
||||
// if element changed or deleted we remove all existing listeners unconditionally
|
||||
if (!on) {
|
||||
for (name in oldOn) {
|
||||
// remove listener if element was changed or existing listeners removed
|
||||
oldElm.removeEventListener(name, oldListener, false);
|
||||
}
|
||||
} else {
|
||||
for (name in oldOn) {
|
||||
// remove listener if existing listener removed
|
||||
if (!on[name]) {
|
||||
oldElm.removeEventListener(name, oldListener, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// add new listeners which has not already attached
|
||||
if (on) {
|
||||
// reuse existing listener or create new
|
||||
const listener = ((vnode as any).listener =
|
||||
(oldVnode as any).listener || createListener());
|
||||
// update vnode for listener
|
||||
listener.vnode = vnode;
|
||||
|
||||
// if element changed or added we add all needed listeners unconditionally
|
||||
if (!oldOn) {
|
||||
for (name in on) {
|
||||
// add listener if element was changed or new listeners added
|
||||
elm.addEventListener(name, listener, false);
|
||||
}
|
||||
} else {
|
||||
for (name in on) {
|
||||
// add listener if new listener added
|
||||
if (!oldOn[name]) {
|
||||
elm.addEventListener(name, listener, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const eventListenersModule: Module = {
|
||||
create: updateEventListeners,
|
||||
update: updateEventListeners,
|
||||
destroy: updateEventListeners,
|
||||
};
|
@ -0,0 +1,220 @@
|
||||
import { VNode, VNodeData } from "../vnode";
|
||||
import { Module } from "./module";
|
||||
|
||||
export type Hero = { id: string };
|
||||
|
||||
const raf =
|
||||
(typeof window !== "undefined" && window.requestAnimationFrame) || setTimeout;
|
||||
const nextFrame = function (fn: any) {
|
||||
raf(function () {
|
||||
raf(fn);
|
||||
});
|
||||
};
|
||||
|
||||
function setNextFrame(obj: any, prop: string, val: any): void {
|
||||
nextFrame(function () {
|
||||
obj[prop] = val;
|
||||
});
|
||||
}
|
||||
|
||||
function getTextNodeRect(textNode: Text): ClientRect | undefined {
|
||||
let rect: ClientRect | undefined;
|
||||
if (document.createRange) {
|
||||
const range = document.createRange();
|
||||
range.selectNodeContents(textNode);
|
||||
if (range.getBoundingClientRect) {
|
||||
rect = range.getBoundingClientRect();
|
||||
}
|
||||
}
|
||||
return rect;
|
||||
}
|
||||
|
||||
function calcTransformOrigin(
|
||||
isTextNode: boolean,
|
||||
textRect: ClientRect | undefined,
|
||||
boundingRect: ClientRect
|
||||
): string {
|
||||
if (isTextNode) {
|
||||
if (textRect) {
|
||||
// calculate pixels to center of text from left edge of bounding box
|
||||
const relativeCenterX =
|
||||
textRect.left + textRect.width / 2 - boundingRect.left;
|
||||
const relativeCenterY =
|
||||
textRect.top + textRect.height / 2 - boundingRect.top;
|
||||
return `${relativeCenterX}px ${relativeCenterY}px`;
|
||||
}
|
||||
}
|
||||
return "0 0"; // top left
|
||||
}
|
||||
|
||||
function getTextDx(
|
||||
oldTextRect: ClientRect | undefined,
|
||||
newTextRect: ClientRect | undefined
|
||||
): number {
|
||||
if (oldTextRect && newTextRect) {
|
||||
return (
|
||||
oldTextRect.left +
|
||||
oldTextRect.width / 2 -
|
||||
(newTextRect.left + newTextRect.width / 2)
|
||||
);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
function getTextDy(
|
||||
oldTextRect: ClientRect | undefined,
|
||||
newTextRect: ClientRect | undefined
|
||||
): number {
|
||||
if (oldTextRect && newTextRect) {
|
||||
return (
|
||||
oldTextRect.top +
|
||||
oldTextRect.height / 2 -
|
||||
(newTextRect.top + newTextRect.height / 2)
|
||||
);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
function isTextElement(elm: Element | Text): elm is Text {
|
||||
return elm.childNodes.length === 1 && elm.childNodes[0].nodeType === 3;
|
||||
}
|
||||
|
||||
let removed: any, created: any;
|
||||
|
||||
function pre() {
|
||||
removed = {};
|
||||
created = [];
|
||||
}
|
||||
|
||||
function create(oldVnode: VNode, vnode: VNode): void {
|
||||
const hero = (vnode.data as VNodeData).hero;
|
||||
if (hero && hero.id) {
|
||||
created.push(hero.id);
|
||||
created.push(vnode);
|
||||
}
|
||||
}
|
||||
|
||||
function destroy(vnode: VNode): void {
|
||||
const hero = (vnode.data as VNodeData).hero;
|
||||
if (hero && hero.id) {
|
||||
const elm = vnode.elm;
|
||||
(vnode as any).isTextNode = isTextElement(elm as Element | Text); // is this a text node?
|
||||
(vnode as any).boundingRect = (elm as Element).getBoundingClientRect(); // save the bounding rectangle to a new property on the vnode
|
||||
(vnode as any).textRect = (vnode as any).isTextNode
|
||||
? getTextNodeRect((elm as Element).childNodes[0] as Text)
|
||||
: null; // save bounding rect of inner text node
|
||||
const computedStyle = window.getComputedStyle(elm as Element, undefined); // get current styles (includes inherited properties)
|
||||
(vnode as any).savedStyle = JSON.parse(JSON.stringify(computedStyle)); // save a copy of computed style values
|
||||
removed[hero.id] = vnode;
|
||||
}
|
||||
}
|
||||
|
||||
function post() {
|
||||
let i: number,
|
||||
id: any,
|
||||
newElm: Element,
|
||||
oldVnode: VNode,
|
||||
oldElm: Element,
|
||||
hRatio: number,
|
||||
wRatio: number,
|
||||
oldRect: ClientRect,
|
||||
newRect: ClientRect,
|
||||
dx: number,
|
||||
dy: number,
|
||||
origTransform: string | null,
|
||||
origTransition: string | null,
|
||||
newStyle: CSSStyleDeclaration,
|
||||
oldStyle: CSSStyleDeclaration,
|
||||
newComputedStyle: CSSStyleDeclaration,
|
||||
isTextNode: boolean,
|
||||
newTextRect: ClientRect | undefined,
|
||||
oldTextRect: ClientRect | undefined;
|
||||
for (i = 0; i < created.length; i += 2) {
|
||||
id = created[i];
|
||||
newElm = created[i + 1].elm;
|
||||
oldVnode = removed[id];
|
||||
if (oldVnode) {
|
||||
isTextNode = (oldVnode as any).isTextNode && isTextElement(newElm); // Are old & new both text?
|
||||
newStyle = (newElm as HTMLElement).style;
|
||||
newComputedStyle = window.getComputedStyle(newElm, undefined); // get full computed style for new element
|
||||
oldElm = oldVnode.elm as Element;
|
||||
oldStyle = (oldElm as HTMLElement).style;
|
||||
// Overall element bounding boxes
|
||||
newRect = newElm.getBoundingClientRect();
|
||||
oldRect = (oldVnode as any).boundingRect; // previously saved bounding rect
|
||||
// Text node bounding boxes & distances
|
||||
if (isTextNode) {
|
||||
newTextRect = getTextNodeRect(newElm.childNodes[0] as Text);
|
||||
oldTextRect = (oldVnode as any).textRect;
|
||||
dx = getTextDx(oldTextRect, newTextRect);
|
||||
dy = getTextDy(oldTextRect, newTextRect);
|
||||
} else {
|
||||
// Calculate distances between old & new positions
|
||||
dx = oldRect.left - newRect.left;
|
||||
dy = oldRect.top - newRect.top;
|
||||
}
|
||||
hRatio = newRect.height / Math.max(oldRect.height, 1);
|
||||
wRatio = isTextNode ? hRatio : newRect.width / Math.max(oldRect.width, 1); // text scales based on hRatio
|
||||
// Animate new element
|
||||
origTransform = newStyle.transform;
|
||||
origTransition = newStyle.transition;
|
||||
if (newComputedStyle.display === "inline") {
|
||||
// inline elements cannot be transformed
|
||||
newStyle.display = "inline-block"; // this does not appear to have any negative side effects
|
||||
}
|
||||
newStyle.transition = origTransition + "transform 0s";
|
||||
newStyle.transformOrigin = calcTransformOrigin(
|
||||
isTextNode,
|
||||
newTextRect,
|
||||
newRect
|
||||
);
|
||||
newStyle.opacity = "0";
|
||||
newStyle.transform = `${origTransform}translate(${dx}px, ${dy}px) scale(${
|
||||
1 / wRatio
|
||||
}, ${1 / hRatio})`;
|
||||
setNextFrame(newStyle, "transition", origTransition);
|
||||
setNextFrame(newStyle, "transform", origTransform);
|
||||
setNextFrame(newStyle, "opacity", "1");
|
||||
// Animate old element
|
||||
for (const key in (oldVnode as any).savedStyle) {
|
||||
// re-apply saved inherited properties
|
||||
if (String(parseInt(key)) !== key) {
|
||||
const ms = key.substring(0, 2) === "ms";
|
||||
const moz = key.substring(0, 3) === "moz";
|
||||
const webkit = key.substring(0, 6) === "webkit";
|
||||
if (!ms && !moz && !webkit) {
|
||||
// ignore prefixed style properties
|
||||
(oldStyle as any)[key] = (oldVnode as any).savedStyle[key];
|
||||
}
|
||||
}
|
||||
}
|
||||
oldStyle.position = "absolute";
|
||||
oldStyle.top = `${oldRect.top}px`; // start at existing position
|
||||
oldStyle.left = `${oldRect.left}px`;
|
||||
oldStyle.width = `${oldRect.width}px`; // Needed for elements who were sized relative to their parents
|
||||
oldStyle.height = `${oldRect.height}px`; // Needed for elements who were sized relative to their parents
|
||||
oldStyle.margin = "0"; // Margin on hero element leads to incorrect positioning
|
||||
oldStyle.transformOrigin = calcTransformOrigin(
|
||||
isTextNode,
|
||||
oldTextRect,
|
||||
oldRect
|
||||
);
|
||||
oldStyle.transform = "";
|
||||
oldStyle.opacity = "1";
|
||||
document.body.appendChild(oldElm);
|
||||
setNextFrame(
|
||||
oldStyle,
|
||||
"transform",
|
||||
`translate(${-dx}px, ${-dy}px) scale(${wRatio}, ${hRatio})`
|
||||
); // scale must be on far right for translate to be correct
|
||||
setNextFrame(oldStyle, "opacity", "0");
|
||||
oldElm.addEventListener("transitionend", function (ev: TransitionEvent) {
|
||||
if (ev.propertyName === "transform") {
|
||||
document.body.removeChild(ev.target as Node);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
removed = created = undefined;
|
||||
}
|
||||
|
||||
export const heroModule: Module = { pre, create, destroy, post };
|
@ -0,0 +1,17 @@
|
||||
import {
|
||||
PreHook,
|
||||
CreateHook,
|
||||
UpdateHook,
|
||||
DestroyHook,
|
||||
RemoveHook,
|
||||
PostHook,
|
||||
} from "../hooks";
|
||||
|
||||
export type Module = Partial<{
|
||||
pre: PreHook;
|
||||
create: CreateHook;
|
||||
update: UpdateHook;
|
||||
destroy: DestroyHook;
|
||||
remove: RemoveHook;
|
||||
post: PostHook;
|
||||
}>;
|
@ -0,0 +1,28 @@
|
||||
import { VNode, VNodeData } from "../vnode";
|
||||
import { Module } from "./module";
|
||||
|
||||
export type Props = Record<string, any>;
|
||||
|
||||
function updateProps(oldVnode: VNode, vnode: VNode): void {
|
||||
let key: string;
|
||||
let cur: any;
|
||||
let old: any;
|
||||
const elm = vnode.elm;
|
||||
let oldProps = (oldVnode.data as VNodeData).props;
|
||||
let props = (vnode.data as VNodeData).props;
|
||||
|
||||
if (!oldProps && !props) return;
|
||||
if (oldProps === props) return;
|
||||
oldProps = oldProps || {};
|
||||
props = props || {};
|
||||
|
||||
for (key in props) {
|
||||
cur = props[key];
|
||||
old = oldProps[key];
|
||||
if (old !== cur && (key !== "value" || (elm as any)[key] !== cur)) {
|
||||
(elm as any)[key] = cur;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const propsModule: Module = { create: updateProps, update: updateProps };
|
@ -0,0 +1,124 @@
|
||||
import { VNode, VNodeData } from "../vnode";
|
||||
import { Module } from "./module";
|
||||
|
||||
export type VNodeStyle = Record<string, string> & {
|
||||
delayed?: Record<string, string>;
|
||||
remove?: Record<string, string>;
|
||||
};
|
||||
|
||||
// Bindig `requestAnimationFrame` like this fixes a bug in IE/Edge. See #360 and #409.
|
||||
const raf =
|
||||
(typeof window !== "undefined" &&
|
||||
window.requestAnimationFrame.bind(window)) ||
|
||||
setTimeout;
|
||||
const nextFrame = function (fn: any) {
|
||||
raf(function () {
|
||||
raf(fn);
|
||||
});
|
||||
};
|
||||
let reflowForced = false;
|
||||
|
||||
function setNextFrame(obj: any, prop: string, val: any): void {
|
||||
nextFrame(function () {
|
||||
obj[prop] = val;
|
||||
});
|
||||
}
|
||||
|
||||
function updateStyle(oldVnode: VNode, vnode: VNode): void {
|
||||
let cur: any;
|
||||
let name: string;
|
||||
const elm = vnode.elm;
|
||||
let oldStyle = (oldVnode.data as VNodeData).style;
|
||||
let style = (vnode.data as VNodeData).style;
|
||||
|
||||
if (!oldStyle && !style) return;
|
||||
if (oldStyle === style) return;
|
||||
oldStyle = oldStyle || {};
|
||||
style = style || {};
|
||||
const oldHasDel = "delayed" in oldStyle;
|
||||
|
||||
for (name in oldStyle) {
|
||||
if (!style[name]) {
|
||||
if (name[0] === "-" && name[1] === "-") {
|
||||
(elm as any).style.removeProperty(name);
|
||||
} else {
|
||||
(elm as any).style[name] = "";
|
||||
}
|
||||
}
|
||||
}
|
||||
for (name in style) {
|
||||
cur = style[name];
|
||||
if (name === "delayed" && style.delayed) {
|
||||
for (const name2 in style.delayed) {
|
||||
cur = style.delayed[name2];
|
||||
if (!oldHasDel || cur !== (oldStyle.delayed as any)[name2]) {
|
||||
setNextFrame((elm as any).style, name2, cur);
|
||||
}
|
||||
}
|
||||
} else if (name !== "remove" && cur !== oldStyle[name]) {
|
||||
if (name[0] === "-" && name[1] === "-") {
|
||||
(elm as any).style.setProperty(name, cur);
|
||||
} else {
|
||||
(elm as any).style[name] = cur;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function applyDestroyStyle(vnode: VNode): void {
|
||||
let style: any;
|
||||
let name: string;
|
||||
const elm = vnode.elm;
|
||||
const s = (vnode.data as VNodeData).style;
|
||||
if (!s || !(style = s.destroy)) return;
|
||||
for (name in style) {
|
||||
(elm as any).style[name] = style[name];
|
||||
}
|
||||
}
|
||||
|
||||
function applyRemoveStyle(vnode: VNode, rm: () => void): void {
|
||||
const s = (vnode.data as VNodeData).style;
|
||||
if (!s || !s.remove) {
|
||||
rm();
|
||||
return;
|
||||
}
|
||||
if (!reflowForced) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
|
||||
(vnode.elm as any).offsetLeft;
|
||||
reflowForced = true;
|
||||
}
|
||||
let name: string;
|
||||
const elm = vnode.elm;
|
||||
let i = 0;
|
||||
const style = s.remove;
|
||||
let amount = 0;
|
||||
const applied: string[] = [];
|
||||
for (name in style) {
|
||||
applied.push(name);
|
||||
(elm as any).style[name] = style[name];
|
||||
}
|
||||
const compStyle = getComputedStyle(elm as Element);
|
||||
const props = (compStyle as any)["transition-property"].split(", ");
|
||||
for (; i < props.length; ++i) {
|
||||
if (applied.indexOf(props[i]) !== -1) amount++;
|
||||
}
|
||||
(elm as Element).addEventListener(
|
||||
"transitionend",
|
||||
function (ev: TransitionEvent) {
|
||||
if (ev.target === elm) --amount;
|
||||
if (amount === 0) rm();
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function forceReflow() {
|
||||
reflowForced = false;
|
||||
}
|
||||
|
||||
export const styleModule: Module = {
|
||||
pre: forceReflow,
|
||||
create: updateStyle,
|
||||
update: updateStyle,
|
||||
destroy: applyDestroyStyle,
|
||||
remove: applyRemoveStyle,
|
||||
};
|
@ -1,62 +0,0 @@
|
||||
import { vnode, VNode, VNodeData } from './vnode'
|
||||
import * as is from './is'
|
||||
|
||||
export type VNodes = VNode[]
|
||||
export type VNodeChildElement = VNode | string | number | undefined | null
|
||||
export type ArrayOrElement<T> = T | T[]
|
||||
export type VNodeChildren = ArrayOrElement<VNodeChildElement>
|
||||
|
||||
function addNS (data: any, children: VNodes | undefined, sel: string | undefined): void {
|
||||
data.ns = 'http://www.w3.org/2000/svg'
|
||||
if (sel !== 'foreignObject' && children !== undefined) {
|
||||
for (let i = 0; i < children.length; ++i) {
|
||||
const childData = children[i].data
|
||||
if (childData !== undefined) {
|
||||
addNS(childData, (children[i] as VNode).children as VNodes, children[i].sel)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function h (sel: string): VNode
|
||||
export function h (sel: string, data: VNodeData | null): VNode
|
||||
export function h (sel: string, children: VNodeChildren): VNode
|
||||
export function h (sel: string, data: VNodeData | null, children: VNodeChildren): VNode
|
||||
export function h (sel: any, b?: any, c?: any): VNode {
|
||||
let data: VNodeData = {}
|
||||
let children: any
|
||||
let text: any
|
||||
let i: number
|
||||
if (c !== undefined) {
|
||||
if (b !== null) {
|
||||
data = b
|
||||
}
|
||||
if (is.array(c)) {
|
||||
children = c
|
||||
} else if (is.primitive(c)) {
|
||||
text = c
|
||||
} else if (c && c.sel) {
|
||||
children = [c]
|
||||
}
|
||||
} else if (b !== undefined && b !== null) {
|
||||
if (is.array(b)) {
|
||||
children = b
|
||||
} else if (is.primitive(b)) {
|
||||
text = b
|
||||
} else if (b && b.sel) {
|
||||
children = [b]
|
||||
} else { data = b }
|
||||
}
|
||||
if (children !== undefined) {
|
||||
for (i = 0; i < children.length; ++i) {
|
||||
if (is.primitive(children[i])) children[i] = vnode(undefined, undefined, undefined, children[i], undefined)
|
||||
}
|
||||
}
|
||||
if (
|
||||
sel[0] === 's' && sel[1] === 'v' && sel[2] === 'g' &&
|
||||
(sel.length === 3 || sel[3] === '.' || sel[3] === '#')
|
||||
) {
|
||||
addNS(data, children, sel)
|
||||
}
|
||||
return vnode(sel, data, children, text, undefined)
|
||||
};
|
@ -1,64 +0,0 @@
|
||||
import { VNode, VNodeData } from '../vnode'
|
||||
|
||||
export interface AttachData {
|
||||
[key: string]: any
|
||||
[i: number]: any
|
||||
placeholder?: any
|
||||
real?: Node
|
||||
}
|
||||
|
||||
interface VNodeDataWithAttach extends VNodeData {
|
||||
attachData: AttachData
|
||||
}
|
||||
|
||||
interface VNodeWithAttachData extends VNode {
|
||||
data: VNodeDataWithAttach
|
||||
}
|
||||
|
||||
function pre (vnode: VNodeWithAttachData, newVnode: VNodeWithAttachData): void {
|
||||
const attachData = vnode.data.attachData
|
||||
// Copy created placeholder and real element from old vnode
|
||||
newVnode.data.attachData.placeholder = attachData.placeholder
|
||||
newVnode.data.attachData.real = attachData.real
|
||||
// Mount real element in vnode so the patch process operates on it
|
||||
vnode.elm = vnode.data.attachData.real
|
||||
}
|
||||
|
||||
function post (_: any, vnode: VNodeWithAttachData): void {
|
||||
// Mount dummy placeholder in vnode so potential reorders use it
|
||||
vnode.elm = vnode.data.attachData.placeholder
|
||||
}
|
||||
|
||||
function destroy (vnode: VNodeWithAttachData): void {
|
||||
// Remove placeholder
|
||||
if (vnode.elm !== undefined) {
|
||||
(vnode.elm.parentNode as HTMLElement).removeChild(vnode.elm)
|
||||
}
|
||||
// Remove real element from where it was inserted
|
||||
vnode.elm = vnode.data.attachData.real
|
||||
}
|
||||
|
||||
function create (_: any, vnode: VNodeWithAttachData): void {
|
||||
const real = vnode.elm
|
||||
const attachData = vnode.data.attachData
|
||||
const placeholder = document.createElement('span')
|
||||
// Replace actual element with dummy placeholder
|
||||
// Snabbdom will then insert placeholder instead
|
||||
vnode.elm = placeholder
|
||||
attachData.target.appendChild(real)
|
||||
attachData.real = real
|
||||
attachData.placeholder = placeholder
|
||||
}
|
||||
|
||||
export function attachTo (target: Element, vnode: VNode): VNode {
|
||||
if (vnode.data === undefined) vnode.data = {}
|
||||
if (vnode.data.hook === undefined) vnode.data.hook = {}
|
||||
const data = vnode.data
|
||||
const hook = vnode.data.hook
|
||||
data.attachData = { target: target, placeholder: undefined, real: undefined }
|
||||
hook.create = create
|
||||
hook.prepatch = pre
|
||||
hook.postpatch = post
|
||||
hook.destroy = destroy
|
||||
return vnode
|
||||
};
|
@ -1,25 +0,0 @@
|
||||
import { VNode } from './vnode'
|
||||
|
||||
export type PreHook = () => any
|
||||
export type InitHook = (vNode: VNode) => any
|
||||
export type CreateHook = (emptyVNode: VNode, vNode: VNode) => any
|
||||
export type InsertHook = (vNode: VNode) => any
|
||||
export type PrePatchHook = (oldVNode: VNode, vNode: VNode) => any
|
||||
export type UpdateHook = (oldVNode: VNode, vNode: VNode) => any
|
||||
export type PostPatchHook = (oldVNode: VNode, vNode: VNode) => any
|
||||
export type DestroyHook = (vNode: VNode) => any
|
||||
export type RemoveHook = (vNode: VNode, removeCallback: () => void) => any
|
||||
export type PostHook = () => any
|
||||
|
||||
export interface Hooks {
|
||||
pre?: PreHook
|
||||
init?: InitHook
|
||||
create?: CreateHook
|
||||
insert?: InsertHook
|
||||
prepatch?: PrePatchHook
|
||||
update?: UpdateHook
|
||||
postpatch?: PostPatchHook
|
||||
destroy?: DestroyHook
|
||||
remove?: RemoveHook
|
||||
post?: PostHook
|
||||
}
|
@ -1,95 +0,0 @@
|
||||
export interface DOMAPI {
|
||||
createElement: (tagName: any, options?: ElementCreationOptions) => HTMLElement
|
||||
createElementNS: (namespaceURI: string, qualifiedName: string, options?: ElementCreationOptions) => Element
|
||||
createTextNode: (text: string) => Text
|
||||
createComment: (text: string) => Comment
|
||||
insertBefore: (parentNode: Node, newNode: Node, referenceNode: Node | null) => void
|
||||
removeChild: (node: Node, child: Node) => void
|
||||
appendChild: (node: Node, child: Node) => void
|
||||
parentNode: (node: Node) => Node | null
|
||||
nextSibling: (node: Node) => Node | null
|
||||
tagName: (elm: Element) => string
|
||||
setTextContent: (node: Node, text: string | null) => void
|
||||
getTextContent: (node: Node) => string | null
|
||||
isElement: (node: Node) => node is Element
|
||||
isText: (node: Node) => node is Text
|
||||
isComment: (node: Node) => node is Comment
|
||||
}
|
||||
|
||||
function createElement (tagName: any, options?: ElementCreationOptions): HTMLElement {
|
||||
return document.createElement(tagName, options)
|
||||
}
|
||||
|
||||
function createElementNS (namespaceURI: string, qualifiedName: string, options?: ElementCreationOptions): Element {
|
||||
return document.createElementNS(namespaceURI, qualifiedName, options)
|
||||
}
|
||||
|
||||
function createTextNode (text: string): Text {
|
||||
return document.createTextNode(text)
|
||||
}
|
||||
|
||||
function createComment (text: string): Comment {
|
||||
return document.createComment(text)
|
||||
}
|
||||
|
||||
function insertBefore (parentNode: Node, newNode: Node, referenceNode: Node | null): void {
|
||||
parentNode.insertBefore(newNode, referenceNode)
|
||||
}
|
||||
|
||||
function removeChild (node: Node, child: Node): void {
|
||||
node.removeChild(child)
|
||||
}
|
||||
|
||||
function appendChild (node: Node, child: Node): void {
|
||||
node.appendChild(child)
|
||||
}
|
||||
|
||||
function parentNode (node: Node): Node | null {
|
||||
return node.parentNode
|
||||
}
|
||||
|
||||
function nextSibling (node: Node): Node | null {
|
||||
return node.nextSibling
|
||||
}
|
||||
|
||||
function tagName (elm: Element): string {
|
||||
return elm.tagName
|
||||
}
|
||||
|
||||
function setTextContent (node: Node, text: string | null): void {
|
||||
node.textContent = text
|
||||
}
|
||||
|
||||
function getTextContent (node: Node): string | null {
|
||||
return node.textContent
|
||||
}
|
||||
|
||||
function isElement (node: Node): node is Element {
|
||||
return node.nodeType === 1
|
||||
}
|
||||
|
||||
function isText (node: Node): node is Text {
|
||||
return node.nodeType === 3
|
||||
}
|
||||
|
||||
function isComment (node: Node): node is Comment {
|
||||
return node.nodeType === 8
|
||||
}
|
||||
|
||||
export const htmlDomApi: DOMAPI = {
|
||||
createElement,
|
||||
createElementNS,
|
||||
createTextNode,
|
||||
createComment,
|
||||
insertBefore,
|
||||
removeChild,
|
||||
appendChild,
|
||||
parentNode,
|
||||
nextSibling,
|
||||
tagName,
|
||||
setTextContent,
|
||||
getTextContent,
|
||||
isElement,
|
||||
isText,
|
||||
isComment,
|
||||
}
|
@ -1,337 +0,0 @@
|
||||
import { Module } from './modules/module'
|
||||
import { vnode, VNode } from './vnode'
|
||||
import * as is from './is'
|
||||
import { htmlDomApi, DOMAPI } from './htmldomapi'
|
||||
|
||||
type NonUndefined<T> = T extends undefined ? never : T
|
||||
|
||||
function isUndef (s: any): boolean {
|
||||
return s === undefined
|
||||
}
|
||||
function isDef<A> (s: A): s is NonUndefined<A> {
|
||||
return s !== undefined
|
||||
}
|
||||
|
||||
type VNodeQueue = VNode[]
|
||||
|
||||
const emptyNode = vnode('', {}, [], undefined, undefined)
|
||||
|
||||
function sameVnode (vnode1: VNode, vnode2: VNode): boolean {
|
||||
const isSameKey = vnode1.key === vnode2.key
|
||||
const isSameIs = vnode1.data?.is === vnode2.data?.is
|
||||
const isSameSel = vnode1.sel === vnode2.sel
|
||||
|
||||
return isSameSel && isSameKey && isSameIs
|
||||
}
|
||||
|
||||
function isVnode (vnode: any): vnode is VNode {
|
||||
return vnode.sel !== undefined
|
||||
}
|
||||
|
||||
type KeyToIndexMap = {[key: string]: number}
|
||||
|
||||
type ArraysOf<T> = {
|
||||
[K in keyof T]: Array<T[K]>;
|
||||
}
|
||||
|
||||
type ModuleHooks = ArraysOf<Required<Module>>
|
||||
|
||||
function createKeyToOldIdx (children: VNode[], beginIdx: number, endIdx: number): KeyToIndexMap {
|
||||
const map: KeyToIndexMap = {}
|
||||
for (let i = beginIdx; i <= endIdx; ++i) {
|
||||
const key = children[i]?.key
|
||||
if (key !== undefined) {
|
||||
map[key] = i
|
||||
}
|
||||
}
|
||||
return map
|
||||
}
|
||||
|
||||
const hooks: Array<keyof Module> = ['create', 'update', 'remove', 'destroy', 'pre', 'post']
|
||||
|
||||
export function init (modules: Array<Partial<Module>>, domApi?: DOMAPI) {
|
||||
let i: number
|
||||
let j: number
|
||||
const cbs: ModuleHooks = {
|
||||
create: [],
|
||||
update: [],
|
||||
remove: [],
|
||||
destroy: [],
|
||||
pre: [],
|
||||
post: []
|
||||
}
|
||||
|
||||
const api: DOMAPI = domApi !== undefined ? domApi : htmlDomApi
|
||||
|
||||
for (i = 0; i < hooks.length; ++i) {
|
||||
cbs[hooks[i]] = []
|
||||
for (j = 0; j < modules.length; ++j) {
|
||||
const hook = modules[j][hooks[i]]
|
||||
if (hook !== undefined) {
|
||||
(cbs[hooks[i]] as any[]).push(hook)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function emptyNodeAt (elm: Element) {
|
||||
const id = elm.id ? '#' + elm.id : ''
|
||||
const c = elm.className ? '.' + elm.className.split(' ').join('.') : ''
|
||||
return vnode(api.tagName(elm).toLowerCase() + id + c, {}, [], undefined, elm)
|
||||
}
|
||||
|
||||
function createRmCb (childElm: Node, listeners: number) {
|
||||
return function rmCb () {
|
||||
if (--listeners === 0) {
|
||||
const parent = api.parentNode(childElm) as Node
|
||||
api.removeChild(parent, childElm)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function createElm (vnode: VNode, insertedVnodeQueue: VNodeQueue): Node {
|
||||
let i: any
|
||||
let data = vnode.data
|
||||
if (data !== undefined) {
|
||||
const init = data.hook?.init
|
||||
if (isDef(init)) {
|
||||
init(vnode)
|
||||
data = vnode.data
|
||||
}
|
||||
}
|
||||
const children = vnode.children
|
||||
const sel = vnode.sel
|
||||
if (sel === '!') {
|
||||
if (isUndef(vnode.text)) {
|
||||
vnode.text = ''
|
||||
}
|
||||
vnode.elm = api.createComment(vnode.text!)
|
||||
} else if (sel !== undefined) {
|
||||
// Parse selector
|
||||
const hashIdx = sel.indexOf('#')
|
||||
const dotIdx = sel.indexOf('.', hashIdx)
|
||||
const hash = hashIdx > 0 ? hashIdx : sel.length
|
||||
const dot = dotIdx > 0 ? dotIdx : sel.length
|
||||
const tag = hashIdx !== -1 || dotIdx !== -1 ? sel.slice(0, Math.min(hash, dot)) : sel
|
||||
const elm = vnode.elm = isDef(data) && isDef(i = data.ns)
|
||||
? api.createElementNS(i, tag, data)
|
||||
: api.createElement(tag, data)
|
||||
if (hash < dot) elm.setAttribute('id', sel.slice(hash + 1, dot))
|
||||
if (dotIdx > 0) elm.setAttribute('class', sel.slice(dot + 1).replace(/\./g, ' '))
|
||||
for (i = 0; i < cbs.create.length; ++i) cbs.create[i](emptyNode, vnode)
|
||||
if (is.array(children)) {
|
||||
for (i = 0; i < children.length; ++i) {
|
||||
const ch = children[i]
|
||||
if (ch != null) {
|
||||
api.appendChild(elm, createElm(ch as VNode, insertedVnodeQueue))
|
||||
}
|
||||
}
|
||||
} else if (is.primitive(vnode.text)) {
|
||||
api.appendChild(elm, api.createTextNode(vnode.text))
|
||||
}
|
||||
const hook = vnode.data!.hook
|
||||
if (isDef(hook)) {
|
||||
hook.create?.(emptyNode, vnode)
|
||||
if (hook.insert) {
|
||||
insertedVnodeQueue.push(vnode)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
vnode.elm = api.createTextNode(vnode.text!)
|
||||
}
|
||||
return vnode.elm
|
||||
}
|
||||
|
||||
function addVnodes (
|
||||
parentElm: Node,
|
||||
before: Node | null,
|
||||
vnodes: VNode[],
|
||||
startIdx: number,
|
||||
endIdx: number,
|
||||
insertedVnodeQueue: VNodeQueue
|
||||
) {
|
||||
for (; startIdx <= endIdx; ++startIdx) {
|
||||
const ch = vnodes[startIdx]
|
||||
if (ch != null) {
|
||||
api.insertBefore(parentElm, createElm(ch, insertedVnodeQueue), before)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function invokeDestroyHook (vnode: VNode) {
|
||||
const data = vnode.data
|
||||
if (data !== undefined) {
|
||||
data?.hook?.destroy?.(vnode)
|
||||
for (let i = 0; i < cbs.destroy.length; ++i) cbs.destroy[i](vnode)
|
||||
if (vnode.children !== undefined) {
|
||||
for (let j = 0; j < vnode.children.length; ++j) {
|
||||
const child = vnode.children[j]
|
||||
if (child != null && typeof child !== 'string') {
|
||||
invokeDestroyHook(child)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function removeVnodes (parentElm: Node,
|
||||
vnodes: VNode[],
|
||||
startIdx: number,
|
||||
endIdx: number): void {
|
||||
for (; startIdx <= endIdx; ++startIdx) {
|
||||
let listeners: number
|
||||
let rm: () => void
|
||||
const ch = vnodes[startIdx]
|
||||
if (ch != null) {
|
||||
if (isDef(ch.sel)) {
|
||||
invokeDestroyHook(ch)
|
||||
listeners = cbs.remove.length + 1
|
||||
rm = createRmCb(ch.elm!, listeners)
|
||||
for (let i = 0; i < cbs.remove.length; ++i) cbs.remove[i](ch, rm)
|
||||
const removeHook = ch?.data?.hook?.remove
|
||||
if (isDef(removeHook)) {
|
||||
removeHook(ch, rm)
|
||||
} else {
|
||||
rm()
|
||||
}
|
||||
} else { // Text node
|
||||
api.removeChild(parentElm, ch.elm!)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function updateChildren (parentElm: Node,
|
||||
oldCh: VNode[],
|
||||
newCh: VNode[],
|
||||
insertedVnodeQueue: VNodeQueue) {
|
||||
let oldStartIdx = 0
|
||||
let newStartIdx = 0
|
||||
let oldEndIdx = oldCh.length - 1
|
||||
let oldStartVnode = oldCh[0]
|
||||
let oldEndVnode = oldCh[oldEndIdx]
|
||||
let newEndIdx = newCh.length - 1
|
||||
let newStartVnode = newCh[0]
|
||||
let newEndVnode = newCh[newEndIdx]
|
||||
let oldKeyToIdx: KeyToIndexMap | undefined
|
||||
let idxInOld: number
|
||||
let elmToMove: VNode
|
||||
let before: any
|
||||
|
||||
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
|
||||
if (oldStartVnode == null) {
|
||||
oldStartVnode = oldCh[++oldStartIdx] // Vnode might have been moved left
|
||||
} else if (oldEndVnode == null) {
|
||||
oldEndVnode = oldCh[--oldEndIdx]
|
||||
} else if (newStartVnode == null) {
|
||||
newStartVnode = newCh[++newStartIdx]
|
||||
} else if (newEndVnode == null) {
|
||||
newEndVnode = newCh[--newEndIdx]
|
||||
} else if (sameVnode(oldStartVnode, newStartVnode)) {
|
||||
patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue)
|
||||
oldStartVnode = oldCh[++oldStartIdx]
|
||||
newStartVnode = newCh[++newStartIdx]
|
||||
} else if (sameVnode(oldEndVnode, newEndVnode)) {
|
||||
patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue)
|
||||
oldEndVnode = oldCh[--oldEndIdx]
|
||||
newEndVnode = newCh[--newEndIdx]
|
||||
} else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right
|
||||
patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue)
|
||||
api.insertBefore(parentElm, oldStartVnode.elm!, api.nextSibling(oldEndVnode.elm!))
|
||||
oldStartVnode = oldCh[++oldStartIdx]
|
||||
newEndVnode = newCh[--newEndIdx]
|
||||
} else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left
|
||||
patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue)
|
||||
api.insertBefore(parentElm, oldEndVnode.elm!, oldStartVnode.elm!)
|
||||
oldEndVnode = oldCh[--oldEndIdx]
|
||||
newStartVnode = newCh[++newStartIdx]
|
||||
} else {
|
||||
if (oldKeyToIdx === undefined) {
|
||||
oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
|
||||
}
|
||||
idxInOld = oldKeyToIdx[newStartVnode.key as string]
|
||||
if (isUndef(idxInOld)) { // New element
|
||||
api.insertBefore(parentElm, createElm(newStartVnode, insertedVnodeQueue), oldStartVnode.elm!)
|
||||
} else {
|
||||
elmToMove = oldCh[idxInOld]
|
||||
if (elmToMove.sel !== newStartVnode.sel) {
|
||||
api.insertBefore(parentElm, createElm(newStartVnode, insertedVnodeQueue), oldStartVnode.elm!)
|
||||
} else {
|
||||
patchVnode(elmToMove, newStartVnode, insertedVnodeQueue)
|
||||
oldCh[idxInOld] = undefined as any
|
||||
api.insertBefore(parentElm, elmToMove.elm!, oldStartVnode.elm!)
|
||||
}
|
||||
}
|
||||
newStartVnode = newCh[++newStartIdx]
|
||||
}
|
||||
}
|
||||
if (oldStartIdx <= oldEndIdx || newStartIdx <= newEndIdx) {
|
||||
if (oldStartIdx > oldEndIdx) {
|
||||
before = newCh[newEndIdx + 1] == null ? null : newCh[newEndIdx + 1].elm
|
||||
addVnodes(parentElm, before, newCh, newStartIdx, newEndIdx, insertedVnodeQueue)
|
||||
} else {
|
||||
removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function patchVnode (oldVnode: VNode, vnode: VNode, insertedVnodeQueue: VNodeQueue) {
|
||||
const hook = vnode.data?.hook
|
||||
hook?.prepatch?.(oldVnode, vnode)
|
||||
const elm = vnode.elm = oldVnode.elm!
|
||||
const oldCh = oldVnode.children as VNode[]
|
||||
const ch = vnode.children as VNode[]
|
||||
if (oldVnode === vnode) return
|
||||
if (vnode.data !== undefined) {
|
||||
for (let i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode)
|
||||
vnode.data.hook?.update?.(oldVnode, vnode)
|
||||
}
|
||||
if (isUndef(vnode.text)) {
|
||||
if (isDef(oldCh) && isDef(ch)) {
|
||||
if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue)
|
||||
} else if (isDef(ch)) {
|
||||
if (isDef(oldVnode.text)) api.setTextContent(elm, '')
|
||||
addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)
|
||||
} else if (isDef(oldCh)) {
|
||||
removeVnodes(elm, oldCh, 0, oldCh.length - 1)
|
||||
} else if (isDef(oldVnode.text)) {
|
||||
api.setTextContent(elm, '')
|
||||
}
|
||||
} else if (oldVnode.text !== vnode.text) {
|
||||
if (isDef(oldCh)) {
|
||||
removeVnodes(elm, oldCh, 0, oldCh.length - 1)
|
||||
}
|
||||
api.setTextContent(elm, vnode.text!)
|
||||
}
|
||||
hook?.postpatch?.(oldVnode, vnode)
|
||||
}
|
||||
|
||||
return function patch (oldVnode: VNode | Element, vnode: VNode): VNode {
|
||||
let i: number, elm: Node, parent: Node
|
||||
const insertedVnodeQueue: VNodeQueue = []
|
||||
for (i = 0; i < cbs.pre.length; ++i) cbs.pre[i]()
|
||||
|
||||
if (!isVnode(oldVnode)) {
|
||||
oldVnode = emptyNodeAt(oldVnode)
|
||||
}
|
||||
|
||||
if (sameVnode(oldVnode, vnode)) {
|
||||
patchVnode(oldVnode, vnode, insertedVnodeQueue)
|
||||
} else {
|
||||
elm = oldVnode.elm!
|
||||
parent = api.parentNode(elm) as Node
|
||||
|
||||
createElm(vnode, insertedVnodeQueue)
|
||||
|
||||
if (parent !== null) {
|
||||
api.insertBefore(parent, vnode.elm!, api.nextSibling(elm))
|
||||
removeVnodes(parent, [oldVnode], 0, 0)
|
||||
}
|
||||
}
|
||||
|
||||
for (i = 0; i < insertedVnodeQueue.length; ++i) {
|
||||
insertedVnodeQueue[i].data!.hook!.insert!(insertedVnodeQueue[i])
|
||||
}
|
||||
for (i = 0; i < cbs.post.length; ++i) cbs.post[i]()
|
||||
return vnode
|
||||
}
|
||||
}
|
@ -1,4 +0,0 @@
|
||||
export const array = Array.isArray
|
||||
export function primitive (s: any): s is (string | number) {
|
||||
return typeof s === 'string' || typeof s === 'number'
|
||||
}
|
@ -1,22 +0,0 @@
|
||||
import { VNode as _VNode, VNodeData as _VNodeData } from './vnode'
|
||||
|
||||
// workaround
|
||||
// https://github.com/typescript-eslint/typescript-eslint/issues/1596
|
||||
type VNode = _VNode
|
||||
type VNodeData = _VNodeData
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||
declare global {
|
||||
/**
|
||||
* opt-in jsx intrinsic global interfaces
|
||||
* see: https://www.typescriptlang.org/docs/handbook/jsx.html#type-checking
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-namespace
|
||||
namespace JSX {
|
||||
type Element = VNode
|
||||
interface IntrinsicElements {
|
||||
[elemName: string]: VNodeData
|
||||
}
|
||||
}
|
||||
}
|
||||
/* eslint-enable @typescript-eslint/no-unused-vars */
|
@ -1,43 +0,0 @@
|
||||
import { vnode, VNode, VNodeData } from './vnode'
|
||||
import { h, ArrayOrElement } from './h'
|
||||
|
||||
// for conditional rendering we support boolean child element e.g cond && <tag />
|
||||
export type JsxVNodeChild = VNode | string | number | boolean | undefined | null
|
||||
export type JsxVNodeChildren = ArrayOrElement<JsxVNodeChild>
|
||||
|
||||
export type FunctionComponent = (props: {[prop: string]: any} | null, children?: VNode[]) => VNode
|
||||
|
||||
function flattenAndFilter (children: JsxVNodeChildren[], flattened: VNode[]): VNode[] {
|
||||
for (const child of children) {
|
||||
// filter out falsey children, except 0 since zero can be a valid value e.g inside a chart
|
||||
if (child !== undefined && child !== null && child !== false && child !== '') {
|
||||
if (Array.isArray(child)) {
|
||||
flattenAndFilter(child, flattened)
|
||||
} else if (typeof child === 'string' || typeof child === 'number' || typeof child === 'boolean') {
|
||||
flattened.push(vnode(undefined, undefined, undefined, String(child), undefined))
|
||||
} else {
|
||||
flattened.push(child)
|
||||
}
|
||||
}
|
||||
}
|
||||
return flattened
|
||||
}
|
||||
|
||||
/**
|
||||
* jsx/tsx compatible factory function
|
||||
* see: https://www.typescriptlang.org/docs/handbook/jsx.html#factory-functions
|
||||
*/
|
||||
export function jsx (tag: string | FunctionComponent, data: VNodeData | null, ...children: JsxVNodeChildren[]): VNode {
|
||||
const flatChildren = flattenAndFilter(children, [])
|
||||
if (typeof tag === 'function') {
|
||||
// tag is a function component
|
||||
return tag(data, flatChildren)
|
||||
} else {
|
||||
if (flatChildren.length === 1 && !flatChildren[0].sel && flatChildren[0].text) {
|
||||
// only child is a simple text node, pass as text for a simpler vtree
|
||||
return h(tag, data, flatChildren[0].text)
|
||||
} else {
|
||||
return h(tag, data, flatChildren)
|
||||
}
|
||||
}
|
||||
}
|
@ -1,56 +0,0 @@
|
||||
import { VNode, VNodeData } from '../vnode'
|
||||
import { Module } from './module'
|
||||
|
||||
export type Attrs = Record<string, string | number | boolean>
|
||||
|
||||
const xlinkNS = 'http://www.w3.org/1999/xlink'
|
||||
const xmlNS = 'http://www.w3.org/XML/1998/namespace'
|
||||
const colonChar = 58
|
||||
const xChar = 120
|
||||
|
||||
function updateAttrs (oldVnode: VNode, vnode: VNode): void {
|
||||
let key: string
|
||||
const elm: Element = vnode.elm as Element
|
||||
let oldAttrs = (oldVnode.data as VNodeData).attrs
|
||||
let attrs = (vnode.data as VNodeData).attrs
|
||||
|
||||
if (!oldAttrs && !attrs) return
|
||||
if (oldAttrs === attrs) return
|
||||
oldAttrs = oldAttrs || {}
|
||||
attrs = attrs || {}
|
||||
|
||||
// update modified attributes, add new attributes
|
||||
for (key in attrs) {
|
||||
const cur = attrs[key]
|
||||
const old = oldAttrs[key]
|
||||
if (old !== cur) {
|
||||
if (cur === true) {
|
||||
elm.setAttribute(key, '')
|
||||
} else if (cur === false) {
|
||||
elm.removeAttribute(key)
|
||||
} else {
|
||||
if (key.charCodeAt(0) !== xChar) {
|
||||
elm.setAttribute(key, cur as any)
|
||||
} else if (key.charCodeAt(3) === colonChar) {
|
||||
// Assume xml namespace
|
||||
elm.setAttributeNS(xmlNS, key, cur as any)
|
||||
} else if (key.charCodeAt(5) === colonChar) {
|
||||
// Assume xlink namespace
|
||||
elm.setAttributeNS(xlinkNS, key, cur as any)
|
||||
} else {
|
||||
elm.setAttribute(key, cur as any)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// remove removed attributes
|
||||
// use `in` operator since the previous `for` iteration uses it (.i.e. add even attributes with undefined value)
|
||||
// the other option is to remove all attributes with value == undefined
|
||||
for (key in oldAttrs) {
|
||||
if (!(key in attrs)) {
|
||||
elm.removeAttribute(key)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const attributesModule: Module = { create: updateAttrs, update: updateAttrs }
|
@ -1,35 +0,0 @@
|
||||
import { VNode, VNodeData } from '../vnode'
|
||||
import { Module } from './module'
|
||||
|
||||
export type Classes = Record<string, boolean>
|
||||
|
||||
function updateClass (oldVnode: VNode, vnode: VNode): void {
|
||||
let cur: any
|
||||
let name: string
|
||||
const elm: Element = vnode.elm as Element
|
||||
let oldClass = (oldVnode.data as VNodeData).class
|
||||
let klass = (vnode.data as VNodeData).class
|
||||
|
||||
if (!oldClass && !klass) return
|
||||
if (oldClass === klass) return
|
||||
oldClass = oldClass || {}
|
||||
klass = klass || {}
|
||||
|
||||
for (name in oldClass) {
|
||||
if (
|
||||
oldClass[name] &&
|
||||
!Object.prototype.hasOwnProperty.call(klass, name)
|
||||
) {
|
||||
// was `true` and now not provided
|
||||
elm.classList.remove(name)
|
||||
}
|
||||
}
|
||||
for (name in klass) {
|
||||
cur = klass[name]
|
||||
if (cur !== oldClass[name]) {
|
||||
(elm.classList as any)[cur ? 'add' : 'remove'](name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const classModule: Module = { create: updateClass, update: updateClass }
|
@ -1,42 +0,0 @@
|
||||
import { VNode, VNodeData } from '../vnode'
|
||||
import { Module } from './module'
|
||||
|
||||
export type Dataset = Record<string, string>
|
||||
|
||||
const CAPS_REGEX = /[A-Z]/g
|
||||
|
||||
function updateDataset (oldVnode: VNode, vnode: VNode): void {
|
||||
const elm: HTMLElement = vnode.elm as HTMLElement
|
||||
let oldDataset = (oldVnode.data as VNodeData).dataset
|
||||
let dataset = (vnode.data as VNodeData).dataset
|
||||
let key: string
|
||||
|
||||
if (!oldDataset && !dataset) return
|
||||
if (oldDataset === dataset) return
|
||||
oldDataset = oldDataset || {}
|
||||
dataset = dataset || {}
|
||||
const d = elm.dataset
|
||||
|
||||
for (key in oldDataset) {
|
||||
if (!dataset[key]) {
|
||||
if (d) {
|
||||
if (key in d) {
|
||||
delete d[key]
|
||||
}
|
||||
} else {
|
||||
elm.removeAttribute('data-' + key.replace(CAPS_REGEX, '-$&').toLowerCase())
|
||||
}
|
||||
}
|
||||
}
|
||||
for (key in dataset) {
|
||||
if (oldDataset[key] !== dataset[key]) {
|
||||
if (d) {
|
||||
d[key] = dataset[key]
|
||||
} else {
|
||||
elm.setAttribute('data-' + key.replace(CAPS_REGEX, '-$&').toLowerCase(), dataset[key])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const datasetModule: Module = { create: updateDataset, update: updateDataset }
|
@ -1,101 +0,0 @@
|
||||
import { VNode, VNodeData } from '../vnode'
|
||||
import { Module } from './module'
|
||||
|
||||
type Listener<T> = (this: VNode, ev: T, vnode: VNode) => void
|
||||
|
||||
export type On = {
|
||||
[N in keyof HTMLElementEventMap]?: Listener<HTMLElementEventMap[N]> | Array<Listener<HTMLElementEventMap[N]>>
|
||||
} & {
|
||||
[event: string]: Listener<any> | Array<Listener<any>>
|
||||
}
|
||||
|
||||
type SomeListener<N extends keyof HTMLElementEventMap> = Listener<HTMLElementEventMap[N]> | Listener<any>
|
||||
|
||||
function invokeHandler<N extends keyof HTMLElementEventMap> (handler: SomeListener<N> | Array<SomeListener<N>>, vnode: VNode, event?: Event): void {
|
||||
if (typeof handler === 'function') {
|
||||
// call function handler
|
||||
handler.call(vnode, event, vnode)
|
||||
} else if (typeof handler === 'object') {
|
||||
// call multiple handlers
|
||||
for (let i = 0; i < handler.length; i++) {
|
||||
invokeHandler(handler[i], vnode, event)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function handleEvent (event: Event, vnode: VNode) {
|
||||
const name = event.type
|
||||
const on = (vnode.data as VNodeData).on
|
||||
|
||||
// call event handler(s) if exists
|
||||
if (on && on[name]) {
|
||||
invokeHandler(on[name], vnode, event)
|
||||
}
|
||||
}
|
||||
|
||||
function createListener () {
|
||||
return function handler (event: Event) {
|
||||
handleEvent(event, (handler as any).vnode)
|
||||
}
|
||||
}
|
||||
|
||||
function updateEventListeners (oldVnode: VNode, vnode?: VNode): void {
|
||||
const oldOn = (oldVnode.data as VNodeData).on
|
||||
const oldListener = (oldVnode as any).listener
|
||||
const oldElm: Element = oldVnode.elm as Element
|
||||
const on = vnode && (vnode.data as VNodeData).on
|
||||
const elm: Element = (vnode && vnode.elm) as Element
|
||||
let name: string
|
||||
|
||||
// optimization for reused immutable handlers
|
||||
if (oldOn === on) {
|
||||
return
|
||||
}
|
||||
|
||||
// remove existing listeners which no longer used
|
||||
if (oldOn && oldListener) {
|
||||
// if element changed or deleted we remove all existing listeners unconditionally
|
||||
if (!on) {
|
||||
for (name in oldOn) {
|
||||
// remove listener if element was changed or existing listeners removed
|
||||
oldElm.removeEventListener(name, oldListener, false)
|
||||
}
|
||||
} else {
|
||||
for (name in oldOn) {
|
||||
// remove listener if existing listener removed
|
||||
if (!on[name]) {
|
||||
oldElm.removeEventListener(name, oldListener, false)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// add new listeners which has not already attached
|
||||
if (on) {
|
||||
// reuse existing listener or create new
|
||||
const listener = (vnode as any).listener = (oldVnode as any).listener || createListener()
|
||||
// update vnode for listener
|
||||
listener.vnode = vnode
|
||||
|
||||
// if element changed or added we add all needed listeners unconditionally
|
||||
if (!oldOn) {
|
||||
for (name in on) {
|
||||
// add listener if element was changed or new listeners added
|
||||
elm.addEventListener(name, listener, false)
|
||||
}
|
||||
} else {
|
||||
for (name in on) {
|
||||
// add listener if new listener added
|
||||
if (!oldOn[name]) {
|
||||
elm.addEventListener(name, listener, false)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const eventListenersModule: Module = {
|
||||
create: updateEventListeners,
|
||||
update: updateEventListeners,
|
||||
destroy: updateEventListeners
|
||||
}
|
@ -1,182 +0,0 @@
|
||||
import { VNode, VNodeData } from '../vnode'
|
||||
import { Module } from './module'
|
||||
|
||||
export type Hero = { id: string }
|
||||
|
||||
const raf = (typeof window !== 'undefined' && window.requestAnimationFrame) || setTimeout
|
||||
const nextFrame = function (fn: any) {
|
||||
raf(function () {
|
||||
raf(fn)
|
||||
})
|
||||
}
|
||||
|
||||
function setNextFrame (obj: any, prop: string, val: any): void {
|
||||
nextFrame(function () {
|
||||
obj[prop] = val
|
||||
})
|
||||
}
|
||||
|
||||
function getTextNodeRect (textNode: Text): ClientRect | undefined {
|
||||
let rect: ClientRect | undefined
|
||||
if (document.createRange) {
|
||||
const range = document.createRange()
|
||||
range.selectNodeContents(textNode)
|
||||
if (range.getBoundingClientRect) {
|
||||
rect = range.getBoundingClientRect()
|
||||
}
|
||||
}
|
||||
return rect
|
||||
}
|
||||
|
||||
function calcTransformOrigin (
|
||||
isTextNode: boolean,
|
||||
textRect: ClientRect | undefined,
|
||||
boundingRect: ClientRect
|
||||
): string {
|
||||
if (isTextNode) {
|
||||
if (textRect) {
|
||||
// calculate pixels to center of text from left edge of bounding box
|
||||
const relativeCenterX = textRect.left + textRect.width / 2 - boundingRect.left
|
||||
const relativeCenterY = textRect.top + textRect.height / 2 - boundingRect.top
|
||||
return relativeCenterX + 'px ' + relativeCenterY + 'px'
|
||||
}
|
||||
}
|
||||
return '0 0' // top left
|
||||
}
|
||||
|
||||
function getTextDx (
|
||||
oldTextRect: ClientRect | undefined,
|
||||
newTextRect: ClientRect | undefined
|
||||
): number {
|
||||
if (oldTextRect && newTextRect) {
|
||||
return ((oldTextRect.left + oldTextRect.width / 2) - (newTextRect.left + newTextRect.width / 2))
|
||||
}
|
||||
return 0
|
||||
}
|
||||
function getTextDy (
|
||||
oldTextRect: ClientRect | undefined,
|
||||
newTextRect: ClientRect | undefined
|
||||
): number {
|
||||
if (oldTextRect && newTextRect) {
|
||||
return ((oldTextRect.top + oldTextRect.height / 2) - (newTextRect.top + newTextRect.height / 2))
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
function isTextElement (elm: Element | Text): elm is Text {
|
||||
return elm.childNodes.length === 1 && elm.childNodes[0].nodeType === 3
|
||||
}
|
||||
|
||||
let removed: any, created: any
|
||||
|
||||
function pre () {
|
||||
removed = {}
|
||||
created = []
|
||||
}
|
||||
|
||||
function create (oldVnode: VNode, vnode: VNode): void {
|
||||
const hero = (vnode.data as VNodeData).hero
|
||||
if (hero && hero.id) {
|
||||
created.push(hero.id)
|
||||
created.push(vnode)
|
||||
}
|
||||
}
|
||||
|
||||
function destroy (vnode: VNode): void {
|
||||
const hero = (vnode.data as VNodeData).hero
|
||||
if (hero && hero.id) {
|
||||
const elm = vnode.elm;
|
||||
(vnode as any).isTextNode = isTextElement(elm as Element | Text); // is this a text node?
|
||||
(vnode as any).boundingRect = (elm as Element).getBoundingClientRect(); // save the bounding rectangle to a new property on the vnode
|
||||
(vnode as any).textRect = (vnode as any).isTextNode ? getTextNodeRect((elm as Element).childNodes[0] as Text) : null // save bounding rect of inner text node
|
||||
const computedStyle = window.getComputedStyle(elm as Element, undefined); // get current styles (includes inherited properties)
|
||||
(vnode as any).savedStyle = JSON.parse(JSON.stringify(computedStyle)) // save a copy of computed style values
|
||||
removed[hero.id] = vnode
|
||||
}
|
||||
}
|
||||
|
||||
function post () {
|
||||
let i: number, id: any, newElm: Element, oldVnode: VNode, oldElm: Element,
|
||||
hRatio: number, wRatio: number,
|
||||
oldRect: ClientRect, newRect: ClientRect, dx: number, dy: number,
|
||||
origTransform: string | null, origTransition: string | null,
|
||||
newStyle: CSSStyleDeclaration, oldStyle: CSSStyleDeclaration,
|
||||
newComputedStyle: CSSStyleDeclaration, isTextNode: boolean,
|
||||
newTextRect: ClientRect | undefined, oldTextRect: ClientRect | undefined
|
||||
for (i = 0; i < created.length; i += 2) {
|
||||
id = created[i]
|
||||
newElm = created[i + 1].elm
|
||||
oldVnode = removed[id]
|
||||
if (oldVnode) {
|
||||
isTextNode = (oldVnode as any).isTextNode && isTextElement(newElm) // Are old & new both text?
|
||||
newStyle = (newElm as HTMLElement).style
|
||||
newComputedStyle = window.getComputedStyle(newElm, undefined) // get full computed style for new element
|
||||
oldElm = oldVnode.elm as Element
|
||||
oldStyle = (oldElm as HTMLElement).style
|
||||
// Overall element bounding boxes
|
||||
newRect = newElm.getBoundingClientRect()
|
||||
oldRect = (oldVnode as any).boundingRect // previously saved bounding rect
|
||||
// Text node bounding boxes & distances
|
||||
if (isTextNode) {
|
||||
newTextRect = getTextNodeRect(newElm.childNodes[0] as Text)
|
||||
oldTextRect = (oldVnode as any).textRect
|
||||
dx = getTextDx(oldTextRect, newTextRect)
|
||||
dy = getTextDy(oldTextRect, newTextRect)
|
||||
} else {
|
||||
// Calculate distances between old & new positions
|
||||
dx = oldRect.left - newRect.left
|
||||
dy = oldRect.top - newRect.top
|
||||
}
|
||||
hRatio = newRect.height / (Math.max(oldRect.height, 1))
|
||||
wRatio = isTextNode ? hRatio : newRect.width / (Math.max(oldRect.width, 1)) // text scales based on hRatio
|
||||
// Animate new element
|
||||
origTransform = newStyle.transform
|
||||
origTransition = newStyle.transition
|
||||
if (newComputedStyle.display === 'inline') {
|
||||
// inline elements cannot be transformed
|
||||
newStyle.display = 'inline-block' // this does not appear to have any negative side effects
|
||||
}
|
||||
newStyle.transition = origTransition + 'transform 0s'
|
||||
newStyle.transformOrigin = calcTransformOrigin(isTextNode, newTextRect, newRect)
|
||||
newStyle.opacity = '0'
|
||||
newStyle.transform = origTransform +
|
||||
'translate(' + dx + 'px, ' + dy + 'px) ' +
|
||||
'scale(' + 1 / wRatio + ', ' + 1 / hRatio + ')'
|
||||
setNextFrame(newStyle, 'transition', origTransition)
|
||||
setNextFrame(newStyle, 'transform', origTransform)
|
||||
setNextFrame(newStyle, 'opacity', '1')
|
||||
// Animate old element
|
||||
for (const key in (oldVnode as any).savedStyle) { // re-apply saved inherited properties
|
||||
if (String(parseInt(key)) !== key) {
|
||||
const ms = key.substring(0, 2) === 'ms'
|
||||
const moz = key.substring(0, 3) === 'moz'
|
||||
const webkit = key.substring(0, 6) === 'webkit'
|
||||
if (!ms && !moz && !webkit) {
|
||||
// ignore prefixed style properties
|
||||
(oldStyle as any)[key] = (oldVnode as any).savedStyle[key]
|
||||
}
|
||||
}
|
||||
}
|
||||
oldStyle.position = 'absolute'
|
||||
oldStyle.top = oldRect.top + 'px' // start at existing position
|
||||
oldStyle.left = oldRect.left + 'px'
|
||||
oldStyle.width = oldRect.width + 'px' // Needed for elements who were sized relative to their parents
|
||||
oldStyle.height = oldRect.height + 'px' // Needed for elements who were sized relative to their parents
|
||||
oldStyle.margin = '0' // Margin on hero element leads to incorrect positioning
|
||||
oldStyle.transformOrigin = calcTransformOrigin(isTextNode, oldTextRect, oldRect)
|
||||
oldStyle.transform = ''
|
||||
oldStyle.opacity = '1'
|
||||
document.body.appendChild(oldElm)
|
||||
setNextFrame(oldStyle, 'transform', 'translate(' + -dx + 'px, ' + -dy + 'px) scale(' + wRatio + ', ' + hRatio + ')') // scale must be on far right for translate to be correct
|
||||
setNextFrame(oldStyle, 'opacity', '0')
|
||||
oldElm.addEventListener('transitionend', function (ev: TransitionEvent) {
|
||||
if (ev.propertyName === 'transform') {
|
||||
document.body.removeChild(ev.target as Node)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
removed = created = undefined
|
||||
}
|
||||
|
||||
export const heroModule: Module = { pre, create, destroy, post }
|
@ -1,10 +0,0 @@
|
||||
import { PreHook, CreateHook, UpdateHook, DestroyHook, RemoveHook, PostHook } from '../hooks'
|
||||
|
||||
export type Module = Partial<{
|
||||
pre: PreHook
|
||||
create: CreateHook
|
||||
update: UpdateHook
|
||||
destroy: DestroyHook
|
||||
remove: RemoveHook
|
||||
post: PostHook
|
||||
}>
|
@ -1,28 +0,0 @@
|
||||
import { VNode, VNodeData } from '../vnode'
|
||||
import { Module } from './module'
|
||||
|
||||
export type Props = Record<string, any>
|
||||
|
||||
function updateProps (oldVnode: VNode, vnode: VNode): void {
|
||||
let key: string
|
||||
let cur: any
|
||||
let old: any
|
||||
const elm = vnode.elm
|
||||
let oldProps = (oldVnode.data as VNodeData).props
|
||||
let props = (vnode.data as VNodeData).props
|
||||
|
||||
if (!oldProps && !props) return
|
||||
if (oldProps === props) return
|
||||
oldProps = oldProps || {}
|
||||
props = props || {}
|
||||
|
||||
for (key in props) {
|
||||
cur = props[key]
|
||||
old = oldProps[key]
|
||||
if (old !== cur && (key !== 'value' || (elm as any)[key] !== cur)) {
|
||||
(elm as any)[key] = cur
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const propsModule: Module = { create: updateProps, update: updateProps }
|
@ -1,118 +0,0 @@
|
||||
import { VNode, VNodeData } from '../vnode'
|
||||
import { Module } from './module'
|
||||
|
||||
export type VNodeStyle = Record<string, string> & {
|
||||
delayed?: Record<string, string>
|
||||
remove?: Record<string, string>
|
||||
}
|
||||
|
||||
// Bindig `requestAnimationFrame` like this fixes a bug in IE/Edge. See #360 and #409.
|
||||
const raf = (typeof window !== 'undefined' && (window.requestAnimationFrame).bind(window)) || setTimeout
|
||||
const nextFrame = function (fn: any) {
|
||||
raf(function () {
|
||||
raf(fn)
|
||||
})
|
||||
}
|
||||
let reflowForced = false
|
||||
|
||||
function setNextFrame (obj: any, prop: string, val: any): void {
|
||||
nextFrame(function () {
|
||||
obj[prop] = val
|
||||
})
|
||||
}
|
||||
|
||||
function updateStyle (oldVnode: VNode, vnode: VNode): void {
|
||||
let cur: any
|
||||
let name: string
|
||||
const elm = vnode.elm
|
||||
let oldStyle = (oldVnode.data as VNodeData).style
|
||||
let style = (vnode.data as VNodeData).style
|
||||
|
||||
if (!oldStyle && !style) return
|
||||
if (oldStyle === style) return
|
||||
oldStyle = oldStyle || {}
|
||||
style = style || {}
|
||||
const oldHasDel = 'delayed' in oldStyle
|
||||
|
||||
for (name in oldStyle) {
|
||||
if (!style[name]) {
|
||||
if (name[0] === '-' && name[1] === '-') {
|
||||
(elm as any).style.removeProperty(name)
|
||||
} else {
|
||||
(elm as any).style[name] = ''
|
||||
}
|
||||
}
|
||||
}
|
||||
for (name in style) {
|
||||
cur = style[name]
|
||||
if (name === 'delayed' && style.delayed) {
|
||||
for (const name2 in style.delayed) {
|
||||
cur = style.delayed[name2]
|
||||
if (!oldHasDel || cur !== (oldStyle.delayed as any)[name2]) {
|
||||
setNextFrame((elm as any).style, name2, cur)
|
||||
}
|
||||
}
|
||||
} else if (name !== 'remove' && cur !== oldStyle[name]) {
|
||||
if (name[0] === '-' && name[1] === '-') {
|
||||
(elm as any).style.setProperty(name, cur)
|
||||
} else {
|
||||
(elm as any).style[name] = cur
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function applyDestroyStyle (vnode: VNode): void {
|
||||
let style: any
|
||||
let name: string
|
||||
const elm = vnode.elm
|
||||
const s = (vnode.data as VNodeData).style
|
||||
if (!s || !(style = s.destroy)) return
|
||||
for (name in style) {
|
||||
(elm as any).style[name] = style[name]
|
||||
}
|
||||
}
|
||||
|
||||
function applyRemoveStyle (vnode: VNode, rm: () => void): void {
|
||||
const s = (vnode.data as VNodeData).style
|
||||
if (!s || !s.remove) {
|
||||
rm()
|
||||
return
|
||||
}
|
||||
if (!reflowForced) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
|
||||
(vnode.elm as any).offsetLeft
|
||||
reflowForced = true
|
||||
}
|
||||
let name: string
|
||||
const elm = vnode.elm
|
||||
let i = 0
|
||||
const style = s.remove
|
||||
let amount = 0
|
||||
const applied: string[] = []
|
||||
for (name in style) {
|
||||
applied.push(name);
|
||||
(elm as any).style[name] = style[name]
|
||||
}
|
||||
const compStyle = getComputedStyle(elm as Element)
|
||||
const props = (compStyle as any)['transition-property'].split(', ')
|
||||
for (; i < props.length; ++i) {
|
||||
if (applied.indexOf(props[i]) !== -1) amount++
|
||||
}
|
||||
(elm as Element).addEventListener('transitionend', function (ev: TransitionEvent) {
|
||||
if (ev.target === elm) --amount
|
||||
if (amount === 0) rm()
|
||||
})
|
||||
}
|
||||
|
||||
function forceReflow () {
|
||||
reflowForced = false
|
||||
}
|
||||
|
||||
export const styleModule: Module = {
|
||||
pre: forceReflow,
|
||||
create: updateStyle,
|
||||
update: updateStyle,
|
||||
destroy: applyDestroyStyle,
|
||||
remove: applyRemoveStyle
|
||||
}
|
@ -1,64 +0,0 @@
|
||||
import { VNode, VNodeData } from './vnode'
|
||||
import { h } from './h'
|
||||
|
||||
export interface ThunkData extends VNodeData {
|
||||
fn: () => VNode
|
||||
args: any[]
|
||||
}
|
||||
|
||||
export interface Thunk extends VNode {
|
||||
data: ThunkData
|
||||
}
|
||||
|
||||
export interface ThunkFn {
|
||||
(sel: string, fn: Function, args: any[]): Thunk
|
||||
(sel: string, key: any, fn: Function, args: any[]): Thunk
|
||||
}
|
||||
|
||||
function copyToThunk (vnode: VNode, thunk: VNode): void {
|
||||
(vnode.data as VNodeData).fn = (thunk.data as VNodeData).fn;
|
||||
(vnode.data as VNodeData).args = (thunk.data as VNodeData).args
|
||||
thunk.data = vnode.data
|
||||
thunk.children = vnode.children
|
||||
thunk.text = vnode.text
|
||||
thunk.elm = vnode.elm
|
||||
}
|
||||
|
||||
function init (thunk: VNode): void {
|
||||
const cur = thunk.data as VNodeData
|
||||
const vnode = (cur.fn as any).apply(undefined, cur.args)
|
||||
copyToThunk(vnode, thunk)
|
||||
}
|
||||
|
||||
function prepatch (oldVnode: VNode, thunk: VNode): void {
|
||||
let i: number
|
||||
const old = oldVnode.data as VNodeData
|
||||
const cur = thunk.data as VNodeData
|
||||
const oldArgs = old.args
|
||||
const args = cur.args
|
||||
if (old.fn !== cur.fn || (oldArgs as any).length !== (args as any).length) {
|
||||
copyToThunk((cur.fn as any).apply(undefined, args), thunk)
|
||||
return
|
||||
}
|
||||
for (i = 0; i < (args as any).length; ++i) {
|
||||
if ((oldArgs as any)[i] !== (args as any)[i]) {
|
||||
copyToThunk((cur.fn as any).apply(undefined, args), thunk)
|
||||
return
|
||||
}
|
||||
}
|
||||
copyToThunk(oldVnode, thunk)
|
||||
}
|
||||
|
||||
export const thunk = function thunk (sel: string, key?: any, fn?: any, args?: any): VNode {
|
||||
if (args === undefined) {
|
||||
args = fn
|
||||
fn = key
|
||||
key = undefined
|
||||
}
|
||||
return h(sel, {
|
||||
key: key,
|
||||
hook: { init, prepatch },
|
||||
fn: fn,
|
||||
args: args
|
||||
})
|
||||
} as ThunkFn
|
@ -1,37 +0,0 @@
|
||||
import { vnode, VNode } from './vnode'
|
||||
import { htmlDomApi, DOMAPI } from './htmldomapi'
|
||||
|
||||
export function toVNode (node: Node, domApi?: DOMAPI): VNode {
|
||||
const api: DOMAPI = domApi !== undefined ? domApi : htmlDomApi
|
||||
let text: string
|
||||
if (api.isElement(node)) {
|
||||
const id = node.id ? '#' + node.id : ''
|
||||
const cn = node.getAttribute('class')
|
||||
const c = cn ? '.' + cn.split(' ').join('.') : ''
|
||||
const sel = api.tagName(node).toLowerCase() + id + c
|
||||
const attrs: any = {}
|
||||
const children: VNode[] = []
|
||||
let name: string
|
||||
let i: number, n: number
|
||||
const elmAttrs = node.attributes
|
||||
const elmChildren = node.childNodes
|
||||
for (i = 0, n = elmAttrs.length; i < n; i++) {
|
||||
name = elmAttrs[i].nodeName
|
||||
if (name !== 'id' && name !== 'class') {
|
||||
attrs[name] = elmAttrs[i].nodeValue
|
||||
}
|
||||
}
|
||||
for (i = 0, n = elmChildren.length; i < n; i++) {
|
||||
children.push(toVNode(elmChildren[i], domApi))
|
||||
}
|
||||
return vnode(sel, { attrs }, children, undefined, node)
|
||||
} else if (api.isText(node)) {
|
||||
text = api.getTextContent(node) as string
|
||||
return vnode(undefined, undefined, undefined, text, node)
|
||||
} else if (api.isComment(node)) {
|
||||
text = api.getTextContent(node) as string
|
||||
return vnode('!', {}, [], text, node as any)
|
||||
} else {
|
||||
return vnode('', {}, [], undefined, node as any)
|
||||
}
|
||||
}
|
@ -1,47 +0,0 @@
|
||||
import { Hooks } from './hooks'
|
||||
import { AttachData } from './helpers/attachto'
|
||||
import { VNodeStyle } from './modules/style'
|
||||
import { On } from './modules/eventlisteners'
|
||||
import { Attrs } from './modules/attributes'
|
||||
import { Classes } from './modules/class'
|
||||
import { Props } from './modules/props'
|
||||
import { Dataset } from './modules/dataset'
|
||||
import { Hero } from './modules/hero'
|
||||
|
||||
export type Key = string | number
|
||||
|
||||
export interface VNode {
|
||||
sel: string | undefined
|
||||
data: VNodeData | undefined
|
||||
children: Array<VNode | string> | undefined
|
||||
elm: Node | undefined
|
||||
text: string | undefined
|
||||
key: Key | undefined
|
||||
}
|
||||
|
||||
export interface VNodeData {
|
||||
props?: Props
|
||||
attrs?: Attrs
|
||||
class?: Classes
|
||||
style?: VNodeStyle
|
||||
dataset?: Dataset
|
||||
on?: On
|
||||
hero?: Hero
|
||||
attachData?: AttachData
|
||||
hook?: Hooks
|
||||
key?: Key
|
||||
ns?: string // for SVGs
|
||||
fn?: () => VNode // for thunks
|
||||
args?: any[] // for thunks
|
||||
is?: string // for custom elements v1
|
||||
[key: string]: any // for any other 3rd party module
|
||||
}
|
||||
|
||||
export function vnode (sel: string | undefined,
|
||||
data: any | undefined,
|
||||
children: Array<VNode | string> | undefined,
|
||||
text: string | undefined,
|
||||
elm: Element | Text | undefined): VNode {
|
||||
const key = data === undefined ? undefined : data.key
|
||||
return { sel, data, children, text, elm, key }
|
||||
}
|
@ -1,160 +0,0 @@
|
||||
import 'core-js/stable/array/fill.js'
|
||||
import faker from 'faker'
|
||||
import { VNode } from '../../package/vnode'
|
||||
import { h } from '../../package/h'
|
||||
import { init as curInit } from '../../package/init'
|
||||
import { init as refInit } from 'latest-snabbdom-release/init'
|
||||
import { assert } from 'chai'
|
||||
import pReduce from 'p-reduce'
|
||||
import pMapSeries from 'p-map-series'
|
||||
import { std, mean } from 'mathjs'
|
||||
|
||||
const RUNS = 5
|
||||
const PATCHES_PER_RUN = 100
|
||||
const WARM_UP_RUNS = 1
|
||||
const REQUEST_ANIMATION_FRAME_EVERY_N_PATCHES = 1
|
||||
const BENCHMARK_TIMEOUT_MINUTES = 10
|
||||
const REQUIRED_PRECISION = 0.02
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||
declare global {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
const __karma__: {
|
||||
info(info: unknown): void
|
||||
}
|
||||
}
|
||||
/* eslint-enable @typescript-eslint/no-unused-vars */
|
||||
|
||||
const ALLOWED_REGRESSION = 0.03
|
||||
describe('core benchmark', () => {
|
||||
it('does not regress', async function Benchmark () {
|
||||
this.timeout(BENCHMARK_TIMEOUT_MINUTES * 1000 * 60)
|
||||
|
||||
faker.seed(0)
|
||||
const inputs = Array(PATCHES_PER_RUN).fill(null).map(() => {
|
||||
return new Array(faker.random.number(20))
|
||||
.fill(null)
|
||||
.map(() => ({
|
||||
name: faker.company.companyName(),
|
||||
catchPhrase: faker.company.catchPhrase(),
|
||||
suffix: faker.company.companySuffix(),
|
||||
products: Array(faker.random.number(3))
|
||||
.fill(null)
|
||||
.map(() => ({
|
||||
name: faker.commerce.productName(),
|
||||
color: faker.commerce.color(),
|
||||
price: faker.commerce.price() + faker.finance.currencySymbol(),
|
||||
})),
|
||||
founded: faker.date.past()
|
||||
}))
|
||||
})
|
||||
|
||||
type Input = (typeof inputs)[0]
|
||||
|
||||
const view = (companies: Input): VNode => h('table', [
|
||||
h('caption', ['Companies']),
|
||||
h('thead', [
|
||||
h('tr', [
|
||||
'Details',
|
||||
'Products',
|
||||
].map((th) => h('th', [th])))
|
||||
]),
|
||||
h('tbody', companies.map(function companyView (company) {
|
||||
return h('tr', [
|
||||
h('td', [
|
||||
h('div', [
|
||||
h('b', [company.name]),
|
||||
company.suffix && `\xa0${company.suffix}`
|
||||
]),
|
||||
h('div', h('i', [company.catchPhrase])),
|
||||
h('td', [
|
||||
h('dt', ['Founded']),
|
||||
h('dd', [company.founded.toLocaleDateString()])
|
||||
])
|
||||
]),
|
||||
h('td', [h('ul', company.products.map(function productView (product) {
|
||||
return h('li', [h('dl', [
|
||||
h('dt', ['Name']),
|
||||
h('dd', [product.name]),
|
||||
h('dt', ['Color']),
|
||||
h('dd', [product.color]),
|
||||
h('dt', ['Price']),
|
||||
h('dd', [product.price]),
|
||||
])])
|
||||
}))])
|
||||
])
|
||||
}))
|
||||
])
|
||||
|
||||
type Patcher = ReturnType<typeof refInit | typeof curInit>
|
||||
|
||||
interface SingleRunResult {
|
||||
i: number
|
||||
cur: number
|
||||
ref: number
|
||||
}
|
||||
|
||||
const subjectToResult = async (subject: Patcher, subjectId: string): Promise<number> => {
|
||||
await new Promise((resolve) => {
|
||||
requestAnimationFrame(resolve)
|
||||
})
|
||||
const markName = `mark:${subjectId}`
|
||||
const measureName = `measure:${subjectId}`
|
||||
performance.mark(markName)
|
||||
const lastVnode = await pReduce(
|
||||
inputs,
|
||||
async function subjectToResultReducer (acc: HTMLElement | VNode, input, i) {
|
||||
const vnode = view(input)
|
||||
subject(acc, vnode)
|
||||
if (i % REQUEST_ANIMATION_FRAME_EVERY_N_PATCHES === 0) {
|
||||
await new Promise((resolve) => {
|
||||
requestAnimationFrame(resolve)
|
||||
})
|
||||
}
|
||||
return vnode
|
||||
},
|
||||
document.body.appendChild(document.createElement('section')),
|
||||
)
|
||||
performance.measure(measureName, markName)
|
||||
if (!('elm' in lastVnode)) throw new Error()
|
||||
if (!lastVnode.elm) throw new Error()
|
||||
document.body.removeChild(lastVnode.elm)
|
||||
const measure = performance.getEntriesByName(measureName)[0]
|
||||
performance.clearMarks(markName)
|
||||
performance.clearMeasures(measureName)
|
||||
return measure.duration
|
||||
}
|
||||
|
||||
const singleRun = async (_: null, runI: number): Promise<SingleRunResult> => {
|
||||
const cur = await subjectToResult(curInit([]), `cur:${runI}`)
|
||||
const ref = await subjectToResult(refInit([]), `ref:${runI}`)
|
||||
|
||||
return { i: runI, cur, ref }
|
||||
}
|
||||
|
||||
const runResults = (await pMapSeries(Array(RUNS + WARM_UP_RUNS).fill(null), singleRun))
|
||||
.slice(WARM_UP_RUNS)
|
||||
|
||||
__karma__.info({ benchmark: runResults })
|
||||
|
||||
const results = {
|
||||
ref: runResults.map((result) => result.ref),
|
||||
cur: runResults.map((result) => result.cur),
|
||||
}
|
||||
const means = {
|
||||
ref: mean(results.ref),
|
||||
cur: mean(results.cur),
|
||||
}
|
||||
const stds = {
|
||||
ref: std(results.ref, 'uncorrected'),
|
||||
cur: std(results.cur, 'uncorrected'),
|
||||
}
|
||||
|
||||
;(['ref', 'cur'] as const).forEach((subject) => {
|
||||
const stdRatio = stds[subject] / means[subject]
|
||||
assert.isAtMost(stdRatio, REQUIRED_PRECISION, `${subject} not precise enough`)
|
||||
})
|
||||
|
||||
assert.isAtMost(means.cur, means.ref * (1 + ALLOWED_REGRESSION))
|
||||
})
|
||||
})
|
@ -1,40 +0,0 @@
|
||||
{
|
||||
"extends": "tsconfigs/browser-executable",
|
||||
"references": [
|
||||
{ "path": "../package/tsconfig.json" }
|
||||
],
|
||||
"compilerOptions": {
|
||||
"types": [
|
||||
"mocha"
|
||||
],
|
||||
"strictFunctionTypes": true,
|
||||
"target": "ES2015",
|
||||
"outDir": "../../build/test",
|
||||
"baseUrl": "../..",
|
||||
"paths": {
|
||||
"latest-snabbdom-release/*": ["node_modules/latest-snabbdom-release/build/package/*"]
|
||||
},
|
||||
"plugins": [
|
||||
{
|
||||
"transform": "../ts-transform-js-extension.cjs",
|
||||
"import": "transform",
|
||||
"after": true,
|
||||
"type": "raw"
|
||||
}
|
||||
],
|
||||
"jsx": "react",
|
||||
"jsxFactory": "jsx"
|
||||
},
|
||||
"files": [
|
||||
"benchmark/core.ts",
|
||||
"unit/attachto.ts",
|
||||
"unit/attributes.ts",
|
||||
"unit/core.ts",
|
||||
"unit/dataset.ts",
|
||||
"unit/eventlisteners.ts",
|
||||
"unit/htmldomapi.ts",
|
||||
"unit/jsx.tsx",
|
||||
"unit/style.ts",
|
||||
"unit/thunk.ts"
|
||||
]
|
||||
}
|
@ -1,100 +0,0 @@
|
||||
import { assert } from 'chai'
|
||||
import { init } from '../../package/init'
|
||||
import { RemoveHook } from '../../package/hooks'
|
||||
import { attachTo } from '../../package/helpers/attachto'
|
||||
import { h } from '../../package/h'
|
||||
|
||||
const patch = init([])
|
||||
|
||||
describe('attachTo', function () {
|
||||
let elm: any, vnode0: any
|
||||
beforeEach(function () {
|
||||
elm = document.createElement('div')
|
||||
vnode0 = elm
|
||||
})
|
||||
it('adds element to target', function () {
|
||||
const vnode1 = h('div', [
|
||||
h('div#wrapper', [
|
||||
h('div', 'Some element'),
|
||||
attachTo(elm, h('div#attached', 'Test')),
|
||||
]),
|
||||
])
|
||||
elm = patch(vnode0, vnode1).elm
|
||||
assert.strictEqual(elm.children.length, 2)
|
||||
})
|
||||
it('updates element at target', function () {
|
||||
const vnode1 = h('div', [
|
||||
h('div#wrapper', [
|
||||
h('div', 'Some element'),
|
||||
attachTo(elm, h('div#attached', 'First text')),
|
||||
]),
|
||||
])
|
||||
const vnode2 = h('div', [
|
||||
h('div#wrapper', [
|
||||
h('div', 'Some element'),
|
||||
attachTo(elm, h('div#attached', 'New text')),
|
||||
]),
|
||||
])
|
||||
elm = patch(vnode0, vnode1).elm
|
||||
assert.strictEqual(elm.children[0].innerHTML, 'First text')
|
||||
elm = patch(vnode1, vnode2).elm
|
||||
assert.strictEqual(elm.children[0].innerHTML, 'New text')
|
||||
})
|
||||
it('element can be inserted before modal', function () {
|
||||
const vnode1 = h('div', [
|
||||
h('div#wrapper', [
|
||||
h('div', 'Some element'),
|
||||
attachTo(elm, h('div#attached', 'Text')),
|
||||
]),
|
||||
])
|
||||
const vnode2 = h('div', [
|
||||
h('div#wrapper', [
|
||||
h('div', 'Some element'),
|
||||
h('div', 'A new element'),
|
||||
attachTo(elm, h('div#attached', 'Text')),
|
||||
]),
|
||||
])
|
||||
elm = patch(vnode0, vnode1).elm
|
||||
assert.strictEqual(elm.children[0].innerHTML, 'Text')
|
||||
elm = patch(vnode1, vnode2).elm
|
||||
assert.strictEqual(elm.children[0].innerHTML, 'Text')
|
||||
})
|
||||
it('removes element at target', function () {
|
||||
const vnode1 = h('div', [
|
||||
h('div#wrapper', [
|
||||
h('div', 'Some element'),
|
||||
attachTo(elm, h('div#attached', 'First text')),
|
||||
]),
|
||||
])
|
||||
const vnode2 = h('div', [
|
||||
h('div#wrapper', [
|
||||
h('div', 'Some element'),
|
||||
]),
|
||||
])
|
||||
elm = patch(vnode0, vnode1).elm
|
||||
assert.strictEqual(elm.children[0].innerHTML, 'First text')
|
||||
elm = patch(vnode1, vnode2).elm
|
||||
assert.strictEqual(elm.children.length, 1)
|
||||
})
|
||||
it('remove hook receives real element', function () {
|
||||
const rm: RemoveHook = (vnode, cb) => {
|
||||
const elm = vnode.elm as HTMLDivElement
|
||||
assert.strictEqual(elm.tagName, 'DIV')
|
||||
assert.strictEqual(elm.innerHTML, 'First text')
|
||||
cb()
|
||||
}
|
||||
const vnode1 = h('div', [
|
||||
h('div#wrapper', [
|
||||
h('div', 'Some element'),
|
||||
attachTo(elm, h('div#attached', { hook: { remove: rm } }, 'First text')),
|
||||
]),
|
||||
])
|
||||
const vnode2 = h('div', [
|
||||
h('div#wrapper', [
|
||||
h('div', 'Some element'),
|
||||
]),
|
||||
])
|
||||
elm = patch(vnode0, vnode1).elm
|
||||
elm = patch(vnode1, vnode2).elm
|
||||
})
|
||||
})
|
@ -1,98 +0,0 @@
|
||||
import { assert } from 'chai'
|
||||
import { init } from '../../package/init'
|
||||
import { attributesModule } from '../../package/modules/attributes'
|
||||
import { h } from '../../package/h'
|
||||
|
||||
const patch = init([
|
||||
attributesModule
|
||||
])
|
||||
|
||||
describe('attributes', function () {
|
||||
let elm: any, vnode0: any
|
||||
beforeEach(function () {
|
||||
elm = document.createElement('div')
|
||||
vnode0 = elm
|
||||
})
|
||||
it('have their provided values', function () {
|
||||
const vnode1 = h('div', { attrs: { href: '/foo', minlength: 1, selected: true, disabled: false } })
|
||||
elm = patch(vnode0, vnode1).elm
|
||||
assert.strictEqual(elm.getAttribute('href'), '/foo')
|
||||
assert.strictEqual(elm.getAttribute('minlength'), '1')
|
||||
assert.strictEqual(elm.hasAttribute('selected'), true)
|
||||
assert.strictEqual(elm.getAttribute('selected'), '')
|
||||
assert.strictEqual(elm.hasAttribute('disabled'), false)
|
||||
})
|
||||
it('can be memoized', function () {
|
||||
const cachedAttrs = { href: '/foo', minlength: 1, selected: true }
|
||||
const vnode1 = h('div', { attrs: cachedAttrs })
|
||||
const vnode2 = h('div', { attrs: cachedAttrs })
|
||||
elm = patch(vnode0, vnode1).elm
|
||||
assert.strictEqual(elm.getAttribute('href'), '/foo')
|
||||
assert.strictEqual(elm.getAttribute('minlength'), '1')
|
||||
assert.strictEqual(elm.getAttribute('selected'), '')
|
||||
elm = patch(vnode1, vnode2).elm
|
||||
assert.strictEqual(elm.getAttribute('href'), '/foo')
|
||||
assert.strictEqual(elm.getAttribute('minlength'), '1')
|
||||
assert.strictEqual(elm.getAttribute('selected'), '')
|
||||
})
|
||||
it('are not omitted when falsy values are provided', function () {
|
||||
const vnode1 = h('div', { attrs: { href: null as any, minlength: 0, value: '', title: 'undefined' } })
|
||||
elm = patch(vnode0, vnode1).elm
|
||||
assert.ok(elm.hasAttribute('href'))
|
||||
assert.ok(elm.hasAttribute('minlength'))
|
||||
assert.ok(elm.hasAttribute('value'))
|
||||
assert.ok(elm.hasAttribute('title'))
|
||||
})
|
||||
it('are set correctly when namespaced', function () {
|
||||
const vnode1 = h('div', { attrs: { 'xlink:href': '#foo' } })
|
||||
elm = patch(vnode0, vnode1).elm
|
||||
assert.strictEqual(elm.getAttributeNS('http://www.w3.org/1999/xlink', 'href'), '#foo')
|
||||
})
|
||||
it('should not touch class nor id fields', function () {
|
||||
elm = document.createElement('div')
|
||||
elm.id = 'myId'
|
||||
elm.className = 'myClass'
|
||||
vnode0 = elm
|
||||
const vnode1 = h('div#myId.myClass', { attrs: {} }, ['Hello'])
|
||||
elm = patch(vnode0, vnode1).elm
|
||||
assert.strictEqual(elm.tagName, 'DIV')
|
||||
assert.strictEqual(elm.id, 'myId')
|
||||
assert.strictEqual(elm.className, 'myClass')
|
||||
assert.strictEqual(elm.textContent, 'Hello')
|
||||
})
|
||||
describe('boolean attribute', function () {
|
||||
it('is present and empty string if the value is truthy', function () {
|
||||
const vnode1 = h('div', { attrs: { required: true, readonly: 1, noresize: 'truthy' } })
|
||||
elm = patch(vnode0, vnode1).elm
|
||||
assert.strictEqual(elm.hasAttribute('required'), true)
|
||||
assert.strictEqual(elm.getAttribute('required'), '')
|
||||
assert.strictEqual(elm.hasAttribute('readonly'), true)
|
||||
assert.strictEqual(elm.getAttribute('readonly'), '1')
|
||||
assert.strictEqual(elm.hasAttribute('noresize'), true)
|
||||
assert.strictEqual(elm.getAttribute('noresize'), 'truthy')
|
||||
})
|
||||
it('is omitted if the value is false', function () {
|
||||
const vnode1 = h('div', { attrs: { required: false } })
|
||||
elm = patch(vnode0, vnode1).elm
|
||||
assert.strictEqual(elm.hasAttribute('required'), false)
|
||||
assert.strictEqual(elm.getAttribute('required'), null)
|
||||
})
|
||||
it('is not omitted if the value is falsy', function () {
|
||||
const vnode1 = h('div', { attrs: { readonly: 0, noresize: null as any } })
|
||||
elm = patch(vnode0, vnode1).elm
|
||||
assert.ok(elm.hasAttribute('readonly'))
|
||||
assert.ok(elm.hasAttribute('noresize'))
|
||||
})
|
||||
})
|
||||
describe('Object.prototype property', function () {
|
||||
it('is not considered as a boolean attribute and shouldn\'t be omitted', function () {
|
||||
const vnode1 = h('div', { attrs: { constructor: true } })
|
||||
elm = patch(vnode0, vnode1).elm
|
||||
assert.strictEqual(elm.hasAttribute('constructor'), true)
|
||||
assert.strictEqual(elm.getAttribute('constructor'), '')
|
||||
const vnode2 = h('div', { attrs: { constructor: false } })
|
||||
elm = patch(vnode0, vnode2).elm
|
||||
assert.strictEqual(elm.hasAttribute('constructor'), false)
|
||||
})
|
||||
})
|
||||
})
|
File diff suppressed because it is too large
Load Diff
@ -1,59 +0,0 @@
|
||||
import { assert } from 'chai'
|
||||
|
||||
import { datasetModule } from '../../package/modules/dataset'
|
||||
import { init } from '../../package/init'
|
||||
import { h } from '../../package/h'
|
||||
|
||||
const patch = init([
|
||||
datasetModule
|
||||
])
|
||||
|
||||
describe('dataset', function () {
|
||||
before(function () {
|
||||
if (!Object.hasOwnProperty.call(HTMLElement.prototype, 'dataset')) {
|
||||
this.skip()
|
||||
}
|
||||
})
|
||||
|
||||
let elm: any, vnode0: any
|
||||
beforeEach(function () {
|
||||
elm = document.createElement('div')
|
||||
vnode0 = elm
|
||||
})
|
||||
it('is set on initial element creation', function () {
|
||||
elm = patch(vnode0, h('div', { dataset: { foo: 'foo' } })).elm
|
||||
assert.strictEqual(elm.dataset.foo, 'foo')
|
||||
})
|
||||
it('updates dataset', function () {
|
||||
const vnode1 = h('i', { dataset: { foo: 'foo', bar: 'bar' } })
|
||||
const vnode2 = h('i', { dataset: { baz: 'baz' } })
|
||||
elm = patch(vnode0, vnode1).elm
|
||||
assert.strictEqual(elm.dataset.foo, 'foo')
|
||||
assert.strictEqual(elm.dataset.bar, 'bar')
|
||||
elm = patch(vnode1, vnode2).elm
|
||||
assert.strictEqual(elm.dataset.baz, 'baz')
|
||||
assert.strictEqual(elm.dataset.foo, undefined)
|
||||
})
|
||||
it('can be memoized', function () {
|
||||
const cachedDataset = { foo: 'foo', bar: 'bar' }
|
||||
const vnode1 = h('i', { dataset: cachedDataset })
|
||||
const vnode2 = h('i', { dataset: cachedDataset })
|
||||
elm = patch(vnode0, vnode1).elm
|
||||
assert.strictEqual(elm.dataset.foo, 'foo')
|
||||
assert.strictEqual(elm.dataset.bar, 'bar')
|
||||
elm = patch(vnode1, vnode2).elm
|
||||
assert.strictEqual(elm.dataset.foo, 'foo')
|
||||
assert.strictEqual(elm.dataset.bar, 'bar')
|
||||
})
|
||||
it('handles string conversions', function () {
|
||||
const vnode1 = h('i', { dataset: { empty: '', dash: '-', dashed: 'foo-bar', camel: 'fooBar', integer: 0 as any, float: 0.1 as any } })
|
||||
elm = patch(vnode0, vnode1).elm
|
||||
|
||||
assert.strictEqual(elm.dataset.empty, '')
|
||||
assert.strictEqual(elm.dataset.dash, '-')
|
||||
assert.strictEqual(elm.dataset.dashed, 'foo-bar')
|
||||
assert.strictEqual(elm.dataset.camel, 'fooBar')
|
||||
assert.strictEqual(elm.dataset.integer, '0')
|
||||
assert.strictEqual(elm.dataset.float, '0.1')
|
||||
})
|
||||
})
|
@ -1,126 +0,0 @@
|
||||
import { assert } from 'chai'
|
||||
import { VNode } from '../../package/vnode'
|
||||
|
||||
import { init } from '../../package/init'
|
||||
import { eventListenersModule } from '../../package/modules/eventlisteners'
|
||||
import { h } from '../../package/h'
|
||||
|
||||
const patch = init([
|
||||
eventListenersModule
|
||||
])
|
||||
|
||||
describe('event listeners', function () {
|
||||
let elm: any, vnode0: any
|
||||
beforeEach(function () {
|
||||
elm = document.createElement('div')
|
||||
vnode0 = elm
|
||||
})
|
||||
it('attaches click event handler to element', function () {
|
||||
const result = []
|
||||
function clicked (ev: Event) {
|
||||
result.push(ev)
|
||||
}
|
||||
const vnode = h('div', { on: { click: clicked } }, [
|
||||
h('a', 'Click my parent'),
|
||||
])
|
||||
elm = patch(vnode0, vnode).elm
|
||||
elm.click()
|
||||
assert.strictEqual(1, result.length)
|
||||
})
|
||||
it('does not attach new listener', function () {
|
||||
const result: number[] = []
|
||||
// function clicked(ev) { result.push(ev); }
|
||||
const vnode1 = h('div', {
|
||||
on: {
|
||||
click: function (ev) {
|
||||
result.push(1)
|
||||
}
|
||||
}
|
||||
}, [
|
||||
h('a', 'Click my parent'),
|
||||
])
|
||||
const vnode2 = h('div', {
|
||||
on: {
|
||||
click: function (ev) {
|
||||
result.push(2)
|
||||
}
|
||||
}
|
||||
}, [
|
||||
h('a', 'Click my parent'),
|
||||
])
|
||||
elm = patch(vnode0, vnode1).elm
|
||||
elm.click()
|
||||
elm = patch(vnode1, vnode2).elm
|
||||
elm.click()
|
||||
assert.deepEqual(result, [1, 2])
|
||||
})
|
||||
it('detach attached click event handler to element', function () {
|
||||
const result: Event[] = []
|
||||
function clicked (ev: Event) {
|
||||
result.push(ev)
|
||||
}
|
||||
const vnode1 = h('div', { on: { click: clicked } }, [
|
||||
h('a', 'Click my parent'),
|
||||
])
|
||||
elm = patch(vnode0, vnode1).elm
|
||||
elm.click()
|
||||
assert.strictEqual(1, result.length)
|
||||
const vnode2 = h('div', { on: {} }, [
|
||||
h('a', 'Click my parent'),
|
||||
])
|
||||
elm = patch(vnode1, vnode2).elm
|
||||
elm.click()
|
||||
assert.strictEqual(1, result.length)
|
||||
})
|
||||
it('multiple event handlers for same event on same element', function () {
|
||||
let called = 0
|
||||
function clicked (ev: Event, vnode: VNode) {
|
||||
++called
|
||||
// Check that the first argument is an event
|
||||
assert.strictEqual(true, 'target' in ev)
|
||||
// Check that the second argument was a vnode
|
||||
assert.strictEqual(vnode.sel, 'div')
|
||||
}
|
||||
const vnode1 = h('div', { on: { click: [clicked, clicked, clicked] } }, [
|
||||
h('a', 'Click my parent'),
|
||||
])
|
||||
elm = patch(vnode0, vnode1).elm
|
||||
elm.click()
|
||||
assert.strictEqual(3, called)
|
||||
const vnode2 = h('div', { on: { click: [clicked, clicked] } }, [
|
||||
h('a', 'Click my parent'),
|
||||
])
|
||||
elm = patch(vnode1, vnode2).elm
|
||||
elm.click()
|
||||
assert.strictEqual(5, called)
|
||||
})
|
||||
it('access to virtual node in event handler', function () {
|
||||
const result: VNode[] = []
|
||||
function clicked (this: VNode, ev: Event, vnode: VNode) {
|
||||
result.push(this)
|
||||
result.push(vnode)
|
||||
}
|
||||
const vnode1 = h('div', { on: { click: clicked } }, [
|
||||
h('a', 'Click my parent'),
|
||||
])
|
||||
elm = patch(vnode0, vnode1).elm
|
||||
elm.click()
|
||||
assert.strictEqual(2, result.length)
|
||||
assert.strictEqual(vnode1, result[0])
|
||||
assert.strictEqual(vnode1, result[1])
|
||||
})
|
||||
it('shared handlers in parent and child nodes', function () {
|
||||
const result = []
|
||||
const sharedHandlers = {
|
||||
click: function (ev: Event) { result.push(ev) }
|
||||
}
|
||||
const vnode1 = h('div', { on: sharedHandlers }, [
|
||||
h('a', { on: sharedHandlers }, 'Click my parent'),
|
||||
])
|
||||
elm = patch(vnode0, vnode1).elm
|
||||
elm.click()
|
||||
assert.strictEqual(1, result.length)
|
||||
elm.firstChild.click()
|
||||
assert.strictEqual(3, result.length)
|
||||
})
|
||||
})
|
@ -1,55 +0,0 @@
|
||||
import { assert } from 'chai'
|
||||
|
||||
import { init } from '../../package/init'
|
||||
import { h } from '../../package/h'
|
||||
import { attributesModule } from '../../package/modules/attributes'
|
||||
|
||||
const patch = init([
|
||||
attributesModule
|
||||
])
|
||||
|
||||
describe('svg', function () {
|
||||
let elm: any, vnode0: any
|
||||
beforeEach(function () {
|
||||
elm = document.createElement('svg')
|
||||
vnode0 = elm
|
||||
})
|
||||
|
||||
it('removes child svg elements', function () {
|
||||
const a = h('svg', {}, [
|
||||
h('g'),
|
||||
h('g')
|
||||
])
|
||||
const b = h('svg', {}, [
|
||||
h('g')
|
||||
])
|
||||
const result = patch(patch(vnode0, a), b).elm as SVGElement
|
||||
assert.strictEqual(result.childNodes.length, 1)
|
||||
})
|
||||
|
||||
it('adds correctly xlink namespaced attribute', function () {
|
||||
const xlinkNS = 'http://www.w3.org/1999/xlink'
|
||||
const testUrl = '/test'
|
||||
const a = h('svg', {}, [
|
||||
h('use', {
|
||||
attrs: { 'xlink:href': testUrl }
|
||||
}, [])
|
||||
])
|
||||
|
||||
const result = patch(vnode0, a).elm as SVGElement
|
||||
assert.strictEqual(result.childNodes.length, 1)
|
||||
const child = result.childNodes[0] as SVGUseElement
|
||||
assert.strictEqual(child.getAttribute('xlink:href'), testUrl)
|
||||
assert.strictEqual(child.getAttributeNS(xlinkNS, 'href'), testUrl)
|
||||
})
|
||||
|
||||
it('adds correctly xml namespaced attribute', function () {
|
||||
const xmlNS = 'http://www.w3.org/XML/1998/namespace'
|
||||
const testAttrValue = 'und'
|
||||
const a = h('svg', { attrs: { 'xml:lang': testAttrValue } }, [])
|
||||
|
||||
const result = patch(vnode0, a).elm as SVGElement
|
||||
assert.strictEqual(result.getAttributeNS(xmlNS, 'lang'), testAttrValue)
|
||||
assert.strictEqual(result.getAttribute('xml:lang'), testAttrValue)
|
||||
})
|
||||
})
|
@ -1,170 +0,0 @@
|
||||
import { assert } from 'chai'
|
||||
|
||||
import { init } from '../../package/init'
|
||||
import { styleModule } from '../../package/modules/style'
|
||||
import { h } from '../../package/h'
|
||||
import { toVNode } from '../../package/tovnode'
|
||||
|
||||
const patch = init([
|
||||
styleModule
|
||||
])
|
||||
|
||||
const featureDiscoveryElm = document.createElement('div')
|
||||
featureDiscoveryElm.style.setProperty('--foo', 'foo')
|
||||
const hasCssVariables = featureDiscoveryElm.style.getPropertyValue('--foo') === 'foo'
|
||||
|
||||
describe('style', function () {
|
||||
let elm: any, vnode0: any
|
||||
beforeEach(function () {
|
||||
elm = document.createElement('div')
|
||||
vnode0 = elm
|
||||
})
|
||||
it('is being styled', function () {
|
||||
elm = patch(vnode0, h('div', { style: { fontSize: '12px' } })).elm
|
||||
assert.strictEqual(elm.style.fontSize, '12px')
|
||||
})
|
||||
it('can be memoized', function () {
|
||||
const cachedStyles = { fontSize: '14px', display: 'inline' }
|
||||
const vnode1 = h('i', { style: cachedStyles })
|
||||
const vnode2 = h('i', { style: cachedStyles })
|
||||
elm = patch(vnode0, vnode1).elm
|
||||
assert.strictEqual(elm.style.fontSize, '14px')
|
||||
assert.strictEqual(elm.style.display, 'inline')
|
||||
elm = patch(vnode1, vnode2).elm
|
||||
assert.strictEqual(elm.style.fontSize, '14px')
|
||||
assert.strictEqual(elm.style.display, 'inline')
|
||||
})
|
||||
it('updates styles', function () {
|
||||
const vnode1 = h('i', { style: { fontSize: '14px', display: 'inline' } })
|
||||
const vnode2 = h('i', { style: { fontSize: '12px', display: 'block' } })
|
||||
const vnode3 = h('i', { style: { fontSize: '10px', display: 'block' } })
|
||||
elm = patch(vnode0, vnode1).elm
|
||||
assert.strictEqual(elm.style.fontSize, '14px')
|
||||
assert.strictEqual(elm.style.display, 'inline')
|
||||
elm = patch(vnode1, vnode2).elm
|
||||
assert.strictEqual(elm.style.fontSize, '12px')
|
||||
assert.strictEqual(elm.style.display, 'block')
|
||||
elm = patch(vnode2, vnode3).elm
|
||||
assert.strictEqual(elm.style.fontSize, '10px')
|
||||
assert.strictEqual(elm.style.display, 'block')
|
||||
})
|
||||
it('explicialy removes styles', function () {
|
||||
const vnode1 = h('i', { style: { fontSize: '14px' } })
|
||||
const vnode2 = h('i', { style: { fontSize: '' } })
|
||||
const vnode3 = h('i', { style: { fontSize: '10px' } })
|
||||
elm = patch(vnode0, vnode1).elm
|
||||
assert.strictEqual(elm.style.fontSize, '14px')
|
||||
patch(vnode1, vnode2)
|
||||
assert.strictEqual(elm.style.fontSize, '')
|
||||
patch(vnode2, vnode3)
|
||||
assert.strictEqual(elm.style.fontSize, '10px')
|
||||
})
|
||||
it('implicially removes styles from element', function () {
|
||||
const vnode1 = h('div', [h('i', { style: { fontSize: '14px' } })])
|
||||
const vnode2 = h('div', [h('i')])
|
||||
const vnode3 = h('div', [h('i', { style: { fontSize: '10px' } })])
|
||||
patch(vnode0, vnode1)
|
||||
assert.strictEqual(elm.firstChild.style.fontSize, '14px')
|
||||
patch(vnode1, vnode2)
|
||||
assert.strictEqual(elm.firstChild.style.fontSize, '')
|
||||
patch(vnode2, vnode3)
|
||||
assert.strictEqual(elm.firstChild.style.fontSize, '10px')
|
||||
})
|
||||
it('updates css variables', function () {
|
||||
if (!hasCssVariables) {
|
||||
this.skip()
|
||||
} else {
|
||||
const vnode1 = h('div', { style: { '--myVar': 1 as any } })
|
||||
const vnode2 = h('div', { style: { '--myVar': 2 as any } })
|
||||
const vnode3 = h('div', { style: { '--myVar': 3 as any } })
|
||||
elm = patch(vnode0, vnode1).elm
|
||||
assert.strictEqual(elm.style.getPropertyValue('--myVar'), '1')
|
||||
elm = patch(vnode1, vnode2).elm
|
||||
assert.strictEqual(elm.style.getPropertyValue('--myVar'), '2')
|
||||
elm = patch(vnode2, vnode3).elm
|
||||
assert.strictEqual(elm.style.getPropertyValue('--myVar'), '3')
|
||||
}
|
||||
})
|
||||
it('explicialy removes css variables', function () {
|
||||
if (!hasCssVariables) {
|
||||
this.skip()
|
||||
} else {
|
||||
const vnode1 = h('i', { style: { '--myVar': 1 as any } })
|
||||
const vnode2 = h('i', { style: { '--myVar': '' } })
|
||||
const vnode3 = h('i', { style: { '--myVar': 2 as any } })
|
||||
elm = patch(vnode0, vnode1).elm
|
||||
assert.strictEqual(elm.style.getPropertyValue('--myVar'), '1')
|
||||
patch(vnode1, vnode2)
|
||||
assert.strictEqual(elm.style.getPropertyValue('--myVar'), '')
|
||||
patch(vnode2, vnode3)
|
||||
assert.strictEqual(elm.style.getPropertyValue('--myVar'), '2')
|
||||
}
|
||||
})
|
||||
it('implicially removes css variables from element', function () {
|
||||
if (!hasCssVariables) {
|
||||
this.skip()
|
||||
} else {
|
||||
const vnode1 = h('div', [h('i', { style: { '--myVar': 1 as any } })])
|
||||
const vnode2 = h('div', [h('i')])
|
||||
const vnode3 = h('div', [h('i', { style: { '--myVar': 2 as any } })])
|
||||
patch(vnode0, vnode1)
|
||||
assert.strictEqual(elm.firstChild.style.getPropertyValue('--myVar'), '1')
|
||||
patch(vnode1, vnode2)
|
||||
assert.strictEqual(elm.firstChild.style.getPropertyValue('--myVar'), '')
|
||||
patch(vnode2, vnode3)
|
||||
assert.strictEqual(elm.firstChild.style.getPropertyValue('--myVar'), '2')
|
||||
}
|
||||
})
|
||||
it('updates delayed styles in next frame', function (done) {
|
||||
const vnode1 = h('i', { style: { fontSize: '14px', delayed: { fontSize: '16px' } as any } })
|
||||
const vnode2 = h('i', { style: { fontSize: '18px', delayed: { fontSize: '20px' } as any } })
|
||||
elm = patch(vnode0, vnode1).elm
|
||||
assert.strictEqual(elm.style.fontSize, '14px')
|
||||
requestAnimationFrame(() => {
|
||||
requestAnimationFrame(() => {
|
||||
assert.strictEqual(elm.style.fontSize, '16px')
|
||||
elm = patch(vnode1, vnode2).elm
|
||||
assert.strictEqual(elm.style.fontSize, '18px')
|
||||
requestAnimationFrame(() => {
|
||||
requestAnimationFrame(() => {
|
||||
assert.strictEqual(elm.style.fontSize, '20px')
|
||||
done()
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
it('applies tranform as transition on remove', function (done) {
|
||||
const btn = h('button', {
|
||||
style: {
|
||||
transition: 'transform 0.1s',
|
||||
remove: { transform: 'translateY(100%)' } as any
|
||||
}
|
||||
}, ['A button'])
|
||||
const vnode1 = h('div.parent', {}, [btn])
|
||||
const vnode2 = h('div.parent', {}, [null])
|
||||
document.body.appendChild(vnode0)
|
||||
patch(vnode0, vnode1)
|
||||
patch(vnode1, vnode2)
|
||||
const button = document.querySelector('button') as HTMLButtonElement
|
||||
assert.notStrictEqual(button, null)
|
||||
button.addEventListener('transitionend', function () {
|
||||
assert.strictEqual(document.querySelector('button'), null)
|
||||
done()
|
||||
})
|
||||
})
|
||||
describe('using toVNode()', function () {
|
||||
it('handles (ignoring) comment nodes', function () {
|
||||
const comment = document.createComment('yolo')
|
||||
const prevElm = document.createElement('div')
|
||||
prevElm.appendChild(comment)
|
||||
const nextVNode = h('div', [h('span', 'Hi')])
|
||||
elm = patch(toVNode(prevElm), nextVNode).elm
|
||||
assert.strictEqual(elm, prevElm)
|
||||
assert.strictEqual(elm.tagName, 'DIV')
|
||||
assert.strictEqual(elm.childNodes.length, 1)
|
||||
assert.strictEqual(elm.childNodes[0].tagName, 'SPAN')
|
||||
assert.strictEqual(elm.childNodes[0].textContent, 'Hi')
|
||||
})
|
||||
})
|
||||
})
|
@ -1,234 +0,0 @@
|
||||
import { assert } from 'chai'
|
||||
|
||||
import { init } from '../../package/init'
|
||||
import { h } from '../../package/h'
|
||||
import { thunk } from '../../package/thunk'
|
||||
import { VNode } from '../../package/vnode'
|
||||
|
||||
const patch = init([
|
||||
])
|
||||
|
||||
describe('thunk', function () {
|
||||
let elm: any, vnode0: any
|
||||
beforeEach(function () {
|
||||
elm = vnode0 = document.createElement('div')
|
||||
})
|
||||
it('returns vnode with data and render function', function () {
|
||||
function numberInSpan (n: number) {
|
||||
return h('span', 'Number is ' + n)
|
||||
}
|
||||
const vnode = thunk('span', 'num', numberInSpan, [22])
|
||||
assert.deepEqual(vnode.sel, 'span')
|
||||
assert.deepEqual(vnode.data.key, 'num')
|
||||
assert.deepEqual(vnode.data.args, [22])
|
||||
})
|
||||
it('calls render function once on data change', function () {
|
||||
let called = 0
|
||||
function numberInSpan (n: number) {
|
||||
called++
|
||||
return h('span', { key: 'num' }, 'Number is ' + n)
|
||||
}
|
||||
const vnode1 = h('div', [
|
||||
thunk('span', 'num', numberInSpan, [1])
|
||||
])
|
||||
const vnode2 = h('div', [
|
||||
thunk('span', 'num', numberInSpan, [2])
|
||||
])
|
||||
patch(vnode0, vnode1)
|
||||
assert.strictEqual(called, 1)
|
||||
patch(vnode1, vnode2)
|
||||
assert.strictEqual(called, 2)
|
||||
})
|
||||
it('does not call render function on data unchanged', function () {
|
||||
let called = 0
|
||||
function numberInSpan (n: number) {
|
||||
called++
|
||||
return h('span', { key: 'num' }, 'Number is ' + n)
|
||||
}
|
||||
const vnode1 = h('div', [
|
||||
thunk('span', 'num', numberInSpan, [1])
|
||||
])
|
||||
const vnode2 = h('div', [
|
||||
thunk('span', 'num', numberInSpan, [1])
|
||||
])
|
||||
patch(vnode0, vnode1)
|
||||
assert.strictEqual(called, 1)
|
||||
patch(vnode1, vnode2)
|
||||
assert.strictEqual(called, 1)
|
||||
})
|
||||
it('calls render function once on data-length change', function () {
|
||||
let called = 0
|
||||
function numberInSpan (n: number) {
|
||||
called++
|
||||
return h('span', { key: 'num' }, 'Number is ' + n)
|
||||
}
|
||||
const vnode1 = h('div', [
|
||||
thunk('span', 'num', numberInSpan, [1])
|
||||
])
|
||||
const vnode2 = h('div', [
|
||||
thunk('span', 'num', numberInSpan, [1, 2])
|
||||
])
|
||||
patch(vnode0, vnode1)
|
||||
assert.strictEqual(called, 1)
|
||||
patch(vnode1, vnode2)
|
||||
assert.strictEqual(called, 2)
|
||||
})
|
||||
it('calls render function once on function change', function () {
|
||||
let called = 0
|
||||
function numberInSpan (n: number) {
|
||||
called++
|
||||
return h('span', { key: 'num' }, 'Number is ' + n)
|
||||
}
|
||||
function numberInSpan2 (n: number) {
|
||||
called++
|
||||
return h('span', { key: 'num' }, 'Number really is ' + n)
|
||||
}
|
||||
const vnode1 = h('div', [
|
||||
thunk('span', 'num', numberInSpan, [1])
|
||||
])
|
||||
const vnode2 = h('div', [
|
||||
thunk('span', 'num', numberInSpan2, [1])
|
||||
])
|
||||
patch(vnode0, vnode1)
|
||||
assert.strictEqual(called, 1)
|
||||
patch(vnode1, vnode2)
|
||||
assert.strictEqual(called, 2)
|
||||
})
|
||||
it('renders correctly', function () {
|
||||
let called = 0
|
||||
function numberInSpan (n: number) {
|
||||
called++
|
||||
return h('span', { key: 'num' }, 'Number is ' + n)
|
||||
}
|
||||
const vnode1 = h('div', [
|
||||
thunk('span', 'num', numberInSpan, [1])
|
||||
])
|
||||
const vnode2 = h('div', [
|
||||
thunk('span', 'num', numberInSpan, [1])
|
||||
])
|
||||
const vnode3 = h('div', [
|
||||
thunk('span', 'num', numberInSpan, [2])
|
||||
])
|
||||
elm = patch(vnode0, vnode1).elm
|
||||
assert.strictEqual(elm.firstChild.tagName.toLowerCase(), 'span')
|
||||
assert.strictEqual(elm.firstChild.innerHTML, 'Number is 1')
|
||||
elm = patch(vnode1, vnode2).elm
|
||||
assert.strictEqual(elm.firstChild.tagName.toLowerCase(), 'span')
|
||||
assert.strictEqual(elm.firstChild.innerHTML, 'Number is 1')
|
||||
elm = patch(vnode2, vnode3).elm
|
||||
assert.strictEqual(elm.firstChild.tagName.toLowerCase(), 'span')
|
||||
assert.strictEqual(elm.firstChild.innerHTML, 'Number is 2')
|
||||
assert.strictEqual(called, 2)
|
||||
})
|
||||
it('supports leaving out the `key` argument', function () {
|
||||
function vnodeFn (s: string) {
|
||||
return h('span.number', 'Hello ' + s)
|
||||
}
|
||||
const vnode1 = thunk('span.number', vnodeFn, ['World!'])
|
||||
elm = patch(vnode0, vnode1).elm
|
||||
assert.strictEqual(elm.innerText, 'Hello World!')
|
||||
})
|
||||
it('renders correctly when root', function () {
|
||||
let called = 0
|
||||
function numberInSpan (n: number) {
|
||||
called++
|
||||
return h('span', { key: 'num' }, 'Number is ' + n)
|
||||
}
|
||||
const vnode1 = thunk('span', 'num', numberInSpan, [1])
|
||||
const vnode2 = thunk('span', 'num', numberInSpan, [1])
|
||||
const vnode3 = thunk('span', 'num', numberInSpan, [2])
|
||||
|
||||
elm = patch(vnode0, vnode1).elm
|
||||
assert.strictEqual(elm.tagName.toLowerCase(), 'span')
|
||||
assert.strictEqual(elm.innerHTML, 'Number is 1')
|
||||
|
||||
elm = patch(vnode1, vnode2).elm
|
||||
assert.strictEqual(elm.tagName.toLowerCase(), 'span')
|
||||
assert.strictEqual(elm.innerHTML, 'Number is 1')
|
||||
|
||||
elm = patch(vnode2, vnode3).elm
|
||||
assert.strictEqual(elm.tagName.toLowerCase(), 'span')
|
||||
assert.strictEqual(elm.innerHTML, 'Number is 2')
|
||||
assert.strictEqual(called, 2)
|
||||
})
|
||||
it('can be replaced and removed', function () {
|
||||
function numberInSpan (n: number) {
|
||||
return h('span', { key: 'num' }, 'Number is ' + n)
|
||||
}
|
||||
function oddEven (n: number): VNode {
|
||||
const prefix = (n % 2) === 0 ? 'Even' : 'Odd'
|
||||
return h('div', { key: oddEven as any }, prefix + ': ' + n)
|
||||
}
|
||||
const vnode1 = h('div', [thunk('span', 'num', numberInSpan, [1])])
|
||||
const vnode2 = h('div', [thunk('div', 'oddEven', oddEven, [4])])
|
||||
|
||||
elm = patch(vnode0, vnode1).elm
|
||||
assert.strictEqual(elm.firstChild.tagName.toLowerCase(), 'span')
|
||||
assert.strictEqual(elm.firstChild.innerHTML, 'Number is 1')
|
||||
|
||||
elm = patch(vnode1, vnode2).elm
|
||||
assert.strictEqual(elm.firstChild.tagName.toLowerCase(), 'div')
|
||||
assert.strictEqual(elm.firstChild.innerHTML, 'Even: 4')
|
||||
})
|
||||
it('can be replaced and removed when root', function () {
|
||||
function numberInSpan (n: number) {
|
||||
return h('span', { key: 'num' }, 'Number is ' + n)
|
||||
}
|
||||
function oddEven (n: number): VNode {
|
||||
const prefix = (n % 2) === 0 ? 'Even' : 'Odd'
|
||||
return h('div', { key: oddEven as any }, prefix + ': ' + n)
|
||||
}
|
||||
const vnode1 = thunk('span', 'num', numberInSpan, [1])
|
||||
const vnode2 = thunk('div', 'oddEven', oddEven, [4])
|
||||
|
||||
elm = patch(vnode0, vnode1).elm
|
||||
assert.strictEqual(elm.tagName.toLowerCase(), 'span')
|
||||
assert.strictEqual(elm.innerHTML, 'Number is 1')
|
||||
|
||||
elm = patch(vnode1, vnode2).elm
|
||||
assert.strictEqual(elm.tagName.toLowerCase(), 'div')
|
||||
assert.strictEqual(elm.innerHTML, 'Even: 4')
|
||||
})
|
||||
it('invokes destroy hook on thunks', function () {
|
||||
let called = 0
|
||||
function destroyHook () {
|
||||
called++
|
||||
}
|
||||
function numberInSpan (n: number) {
|
||||
return h('span', { key: 'num', hook: { destroy: destroyHook } }, 'Number is ' + n)
|
||||
}
|
||||
const vnode1 = h('div', [
|
||||
h('div', 'Foo'),
|
||||
thunk('span', 'num', numberInSpan, [1]),
|
||||
h('div', 'Foo')
|
||||
])
|
||||
const vnode2 = h('div', [
|
||||
h('div', 'Foo'),
|
||||
h('div', 'Foo')
|
||||
])
|
||||
patch(vnode0, vnode1)
|
||||
patch(vnode1, vnode2)
|
||||
assert.strictEqual(called, 1)
|
||||
})
|
||||
it('invokes remove hook on thunks', function () {
|
||||
let called = 0
|
||||
function hook () {
|
||||
called++
|
||||
}
|
||||
function numberInSpan (n: number) {
|
||||
return h('span', { key: 'num', hook: { remove: hook } }, 'Number is ' + n)
|
||||
}
|
||||
const vnode1 = h('div', [
|
||||
h('div', 'Foo'),
|
||||
thunk('span', 'num', numberInSpan, [1]),
|
||||
h('div', 'Foo')
|
||||
])
|
||||
const vnode2 = h('div', [
|
||||
h('div', 'Foo'),
|
||||
h('div', 'Foo')
|
||||
])
|
||||
patch(vnode0, vnode1)
|
||||
patch(vnode1, vnode2)
|
||||
assert.strictEqual(called, 1)
|
||||
})
|
||||
})
|
@ -0,0 +1,69 @@
|
||||
import { VNode, VNodeData } from "./vnode";
|
||||
import { h } from "./h";
|
||||
|
||||
export interface ThunkData extends VNodeData {
|
||||
fn: () => VNode;
|
||||
args: any[];
|
||||
}
|
||||
|
||||
export interface Thunk extends VNode {
|
||||
data: ThunkData;
|
||||
}
|
||||
|
||||
export interface ThunkFn {
|
||||
(sel: string, fn: (...args: any[]) => any, args: any[]): Thunk;
|
||||
(sel: string, key: any, fn: (...args: any[]) => any, args: any[]): Thunk;
|
||||
}
|
||||
|
||||
function copyToThunk(vnode: VNode, thunk: VNode): void {
|
||||
(vnode.data as VNodeData).fn = (thunk.data as VNodeData).fn;
|
||||
(vnode.data as VNodeData).args = (thunk.data as VNodeData).args;
|
||||
thunk.data = vnode.data;
|
||||
thunk.children = vnode.children;
|
||||
thunk.text = vnode.text;
|
||||
thunk.elm = vnode.elm;
|
||||
}
|
||||
|
||||
function init(thunk: VNode): void {
|
||||
const cur = thunk.data as VNodeData;
|
||||
const vnode = (cur.fn as any)(...cur.args!);
|
||||
copyToThunk(vnode, thunk);
|
||||
}
|
||||
|
||||
function prepatch(oldVnode: VNode, thunk: VNode): void {
|
||||
let i: number;
|
||||
const old = oldVnode.data as VNodeData;
|
||||
const cur = thunk.data as VNodeData;
|
||||
const oldArgs = old.args;
|
||||
const args = cur.args;
|
||||
if (old.fn !== cur.fn || (oldArgs as any).length !== (args as any).length) {
|
||||
copyToThunk((cur.fn as any)(...args!), thunk);
|
||||
return;
|
||||
}
|
||||
for (i = 0; i < (args as any).length; ++i) {
|
||||
if ((oldArgs as any)[i] !== (args as any)[i]) {
|
||||
copyToThunk((cur.fn as any)(...args!), thunk);
|
||||
return;
|
||||
}
|
||||
}
|
||||
copyToThunk(oldVnode, thunk);
|
||||
}
|
||||
|
||||
export const thunk = function thunk(
|
||||
sel: string,
|
||||
key?: any,
|
||||
fn?: any,
|
||||
args?: any
|
||||
): VNode {
|
||||
if (args === undefined) {
|
||||
args = fn;
|
||||
fn = key;
|
||||
key = undefined;
|
||||
}
|
||||
return h(sel, {
|
||||
key: key,
|
||||
hook: { init, prepatch },
|
||||
fn: fn,
|
||||
args: args,
|
||||
});
|
||||
} as ThunkFn;
|
@ -0,0 +1,37 @@
|
||||
import { vnode, VNode } from "./vnode";
|
||||
import { htmlDomApi, DOMAPI } from "./htmldomapi";
|
||||
|
||||
export function toVNode(node: Node, domApi?: DOMAPI): VNode {
|
||||
const api: DOMAPI = domApi !== undefined ? domApi : htmlDomApi;
|
||||
let text: string;
|
||||
if (api.isElement(node)) {
|
||||
const id = node.id ? "#" + node.id : "";
|
||||
const cn = node.getAttribute("class");
|
||||
const c = cn ? "." + cn.split(" ").join(".") : "";
|
||||
const sel = api.tagName(node).toLowerCase() + id + c;
|
||||
const attrs: any = {};
|
||||
const children: VNode[] = [];
|
||||
let name: string;
|
||||
let i: number, n: number;
|
||||
const elmAttrs = node.attributes;
|
||||
const elmChildren = node.childNodes;
|
||||
for (i = 0, n = elmAttrs.length; i < n; i++) {
|
||||
name = elmAttrs[i].nodeName;
|
||||
if (name !== "id" && name !== "class") {
|
||||
attrs[name] = elmAttrs[i].nodeValue;
|
||||
}
|
||||
}
|
||||
for (i = 0, n = elmChildren.length; i < n; i++) {
|
||||
children.push(toVNode(elmChildren[i], domApi));
|
||||
}
|
||||
return vnode(sel, { attrs }, children, undefined, node);
|
||||
} else if (api.isText(node)) {
|
||||
text = api.getTextContent(node) as string;
|
||||
return vnode(undefined, undefined, undefined, text, node);
|
||||
} else if (api.isComment(node)) {
|
||||
text = api.getTextContent(node) as string;
|
||||
return vnode("!", {}, [], text, node as any);
|
||||
} else {
|
||||
return vnode("", {}, [], undefined, node as any);
|
||||
}
|
||||
}
|
@ -1,32 +0,0 @@
|
||||
const ts = require('typescript')
|
||||
|
||||
module.exports.transform = (ctx) => (sf) => ts.visitNode(sf, (node) => {
|
||||
const visitor = (node) => {
|
||||
const originalPath = (
|
||||
ts.isImportDeclaration(node) ||
|
||||
ts.isExportDeclaration(node)) &&
|
||||
node.moduleSpecifier
|
||||
? node.moduleSpecifier.getText(sf).slice(1, -1)
|
||||
: (
|
||||
ts.isImportTypeNode(node) &&
|
||||
ts.isLiteralTypeNode(node.argument) &&
|
||||
ts.isStringLiteral(node.argument.literal)
|
||||
)
|
||||
? node.argument.literal.text
|
||||
: null
|
||||
|
||||
if (originalPath === null) return node
|
||||
if (!originalPath.startsWith('.')) return node
|
||||
const pathWithExtension = originalPath.endsWith('.js')
|
||||
? originalPath
|
||||
: originalPath + '.js'
|
||||
const newNode = ts.getMutableClone(node)
|
||||
if (ts.isImportDeclaration(node) || ts.isExportDeclaration(node)) {
|
||||
newNode.moduleSpecifier = ts.createLiteral(pathWithExtension)
|
||||
} else if (ts.isImportTypeNode(node)) {
|
||||
newNode.argument = ts.createLiteralTypeNode(pathWithExtension)
|
||||
}
|
||||
return newNode
|
||||
}
|
||||
return ts.visitEachChild(node, visitor, ctx)
|
||||
})
|
@ -0,0 +1,49 @@
|
||||
import { Hooks } from "./hooks";
|
||||
import { AttachData } from "./helpers/attachto";
|
||||
import { VNodeStyle } from "./modules/style";
|
||||
import { On } from "./modules/eventlisteners";
|
||||
import { Attrs } from "./modules/attributes";
|
||||
import { Classes } from "./modules/class";
|
||||
import { Props } from "./modules/props";
|
||||
import { Dataset } from "./modules/dataset";
|
||||
import { Hero } from "./modules/hero";
|
||||
|
||||
export type Key = string | number;
|
||||
|
||||
export interface VNode {
|
||||
sel: string | undefined;
|
||||
data: VNodeData | undefined;
|
||||
children: Array<VNode | string> | undefined;
|
||||
elm: Node | undefined;
|
||||
text: string | undefined;
|
||||
key: Key | undefined;
|
||||
}
|
||||
|
||||
export interface VNodeData {
|
||||
props?: Props;
|
||||
attrs?: Attrs;
|
||||
class?: Classes;
|
||||
style?: VNodeStyle;
|
||||
dataset?: Dataset;
|
||||
on?: On;
|
||||
hero?: Hero;
|
||||
attachData?: AttachData;
|
||||
hook?: Hooks;
|
||||
key?: Key;
|
||||
ns?: string; // for SVGs
|
||||
fn?: () => VNode; // for thunks
|
||||
args?: any[]; // for thunks
|
||||
is?: string; // for custom elements v1
|
||||
[key: string]: any; // for any other 3rd party module
|
||||
}
|
||||
|
||||
export function vnode(
|
||||
sel: string | undefined,
|
||||
data: any | undefined,
|
||||
children: Array<VNode | string> | undefined,
|
||||
text: string | undefined,
|
||||
elm: Element | Text | undefined
|
||||
): VNode {
|
||||
const key = data === undefined ? undefined : data.key;
|
||||
return { sel, data, children, text, elm, key };
|
||||
}
|
@ -0,0 +1,184 @@
|
||||
import "core-js/stable/array/fill.js";
|
||||
import faker from "faker";
|
||||
import { VNode, h, init as curInit } from "../../src/index";
|
||||
import { init as refInit } from "latest-snabbdom-release/build/package/init";
|
||||
import { assert } from "chai";
|
||||
import pReduce from "p-reduce";
|
||||
import pMapSeries from "p-map-series";
|
||||
import { std, mean } from "mathjs";
|
||||
|
||||
const RUNS = 5;
|
||||
const PATCHES_PER_RUN = 100;
|
||||
const WARM_UP_RUNS = 1;
|
||||
const REQUEST_ANIMATION_FRAME_EVERY_N_PATCHES = 1;
|
||||
const BENCHMARK_TIMEOUT_MINUTES = 10;
|
||||
const REQUIRED_PRECISION = 0.02;
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||
declare global {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
const __karma__: {
|
||||
info(info: unknown): void;
|
||||
};
|
||||
}
|
||||
/* eslint-enable @typescript-eslint/no-unused-vars */
|
||||
|
||||
const ALLOWED_REGRESSION = 0.03;
|
||||
describe("core benchmark", () => {
|
||||
it("does not regress", async function Benchmark() {
|
||||
this.timeout(BENCHMARK_TIMEOUT_MINUTES * 1000 * 60);
|
||||
|
||||
faker.seed(0);
|
||||
const inputs = Array(PATCHES_PER_RUN)
|
||||
.fill(null)
|
||||
.map(() => {
|
||||
return new Array(faker.random.number(20)).fill(null).map(() => ({
|
||||
name: faker.company.companyName(),
|
||||
catchPhrase: faker.company.catchPhrase(),
|
||||
suffix: faker.company.companySuffix(),
|
||||
products: Array(faker.random.number(3))
|
||||
.fill(null)
|
||||
.map(() => ({
|
||||
name: faker.commerce.productName(),
|
||||
color: faker.commerce.color(),
|
||||
price: faker.commerce.price() + faker.finance.currencySymbol(),
|
||||
})),
|
||||
founded: faker.date.past(),
|
||||
}));
|
||||
});
|
||||
|
||||
type Input = typeof inputs[0];
|
||||
|
||||
const view = (companies: Input): VNode =>
|
||||
h("table", [
|
||||
h("caption", ["Companies"]),
|
||||
h("thead", [
|
||||
h(
|
||||
"tr",
|
||||
["Details", "Products"].map((th) => h("th", [th]))
|
||||
),
|
||||
]),
|
||||
h(
|
||||
"tbody",
|
||||
companies.map(function companyView(company) {
|
||||
return h("tr", [
|
||||
h("td", [
|
||||
h("div", [
|
||||
h("b", [company.name]),
|
||||
company.suffix && `\xa0${company.suffix}`,
|
||||
]),
|
||||
h("div", h("i", [company.catchPhrase])),
|
||||
h("td", [
|
||||
h("dt", ["Founded"]),
|
||||
h("dd", [company.founded.toLocaleDateString()]),
|
||||
]),
|
||||
]),
|
||||
h("td", [
|
||||
h(
|
||||
"ul",
|
||||
company.products.map(function productView(product) {
|
||||
return h("li", [
|
||||
h("dl", [
|
||||
h("dt", ["Name"]),
|
||||
h("dd", [product.name]),
|
||||
h("dt", ["Color"]),
|
||||
h("dd", [product.color]),
|
||||
h("dt", ["Price"]),
|
||||
h("dd", [product.price]),
|
||||
]),
|
||||
]);
|
||||
})
|
||||
),
|
||||
]),
|
||||
]);
|
||||
})
|
||||
),
|
||||
]);
|
||||
|
||||
type Patcher = ReturnType<typeof refInit | typeof curInit>;
|
||||
|
||||
interface SingleRunResult {
|
||||
i: number;
|
||||
cur: number;
|
||||
ref: number;
|
||||
}
|
||||
|
||||
const subjectToResult = async (
|
||||
subject: Patcher,
|
||||
subjectId: string
|
||||
): Promise<number> => {
|
||||
await new Promise((resolve) => {
|
||||
requestAnimationFrame(resolve);
|
||||
});
|
||||
const markName = `mark:${subjectId}`;
|
||||
const measureName = `measure:${subjectId}`;
|
||||
performance.mark(markName);
|
||||
const lastVnode = await pReduce(
|
||||
inputs,
|
||||
async function subjectToResultReducer(
|
||||
acc: HTMLElement | VNode,
|
||||
input,
|
||||
i
|
||||
) {
|
||||
const vnode = view(input);
|
||||
subject(acc, vnode);
|
||||
if (i % REQUEST_ANIMATION_FRAME_EVERY_N_PATCHES === 0) {
|
||||
await new Promise((resolve) => {
|
||||
requestAnimationFrame(resolve);
|
||||
});
|
||||
}
|
||||
return vnode;
|
||||
},
|
||||
document.body.appendChild(document.createElement("section"))
|
||||
);
|
||||
performance.measure(measureName, markName);
|
||||
if (!("elm" in lastVnode)) throw new Error();
|
||||
if (!lastVnode.elm) throw new Error();
|
||||
document.body.removeChild(lastVnode.elm);
|
||||
const measure = performance.getEntriesByName(measureName)[0];
|
||||
performance.clearMarks(markName);
|
||||
performance.clearMeasures(measureName);
|
||||
return measure.duration;
|
||||
};
|
||||
|
||||
const singleRun = async (
|
||||
_: null,
|
||||
runI: number
|
||||
): Promise<SingleRunResult> => {
|
||||
const cur = await subjectToResult(curInit([]), `cur:${runI}`);
|
||||
const ref = await subjectToResult(refInit([]), `ref:${runI}`);
|
||||
|
||||
return { i: runI, cur, ref };
|
||||
};
|
||||
|
||||
const runResults = (
|
||||
await pMapSeries(Array(RUNS + WARM_UP_RUNS).fill(null), singleRun)
|
||||
).slice(WARM_UP_RUNS);
|
||||
|
||||
__karma__.info({ benchmark: runResults });
|
||||
|
||||
const results = {
|
||||
ref: runResults.map((result) => result.ref),
|
||||
cur: runResults.map((result) => result.cur),
|
||||
};
|
||||
const means = {
|
||||
ref: mean(results.ref),
|
||||
cur: mean(results.cur),
|
||||
};
|
||||
const stds = {
|
||||
ref: std(results.ref, "uncorrected"),
|
||||
cur: std(results.cur, "uncorrected"),
|
||||
};
|
||||
|
||||
(["ref", "cur"] as const).forEach((subject) => {
|
||||
const stdRatio = stds[subject] / means[subject];
|
||||
assert.isAtMost(
|
||||
stdRatio,
|
||||
REQUIRED_PRECISION,
|
||||
`${subject} not precise enough`
|
||||
);
|
||||
});
|
||||
|
||||
assert.isAtMost(means.cur, means.ref * (1 + ALLOWED_REGRESSION));
|
||||
});
|
||||
});
|
@ -0,0 +1,9 @@
|
||||
{
|
||||
"extends": "../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"esModuleInterop": true,
|
||||
"jsx": "react",
|
||||
"jsxFactory": "jsx"
|
||||
},
|
||||
"include": ["./**/*.ts", "./unit/*.tsx", "../src/index.ts"]
|
||||
}
|
@ -0,0 +1,93 @@
|
||||
import { assert } from "chai";
|
||||
|
||||
import { init, RemoveHook, attachTo, h } from "../../src/index";
|
||||
|
||||
const patch = init([]);
|
||||
|
||||
describe("attachTo", function () {
|
||||
let elm: any, vnode0: any;
|
||||
beforeEach(function () {
|
||||
elm = document.createElement("div");
|
||||
vnode0 = elm;
|
||||
});
|
||||
it("adds element to target", function () {
|
||||
const vnode1 = h("div", [
|
||||
h("div#wrapper", [
|
||||
h("div", "Some element"),
|
||||
attachTo(elm, h("div#attached", "Test")),
|
||||
]),
|
||||
]);
|
||||
elm = patch(vnode0, vnode1).elm;
|
||||
assert.strictEqual(elm.children.length, 2);
|
||||
});
|
||||
it("updates element at target", function () {
|
||||
const vnode1 = h("div", [
|
||||
h("div#wrapper", [
|
||||
h("div", "Some element"),
|
||||
attachTo(elm, h("div#attached", "First text")),
|
||||
]),
|
||||
]);
|
||||
const vnode2 = h("div", [
|
||||
h("div#wrapper", [
|
||||
h("div", "Some element"),
|
||||
attachTo(elm, h("div#attached", "New text")),
|
||||
]),
|
||||
]);
|
||||
elm = patch(vnode0, vnode1).elm;
|
||||
assert.strictEqual(elm.children[0].innerHTML, "First text");
|
||||
elm = patch(vnode1, vnode2).elm;
|
||||
assert.strictEqual(elm.children[0].innerHTML, "New text");
|
||||
});
|
||||
it("element can be inserted before modal", function () {
|
||||
const vnode1 = h("div", [
|
||||
h("div#wrapper", [
|
||||
h("div", "Some element"),
|
||||
attachTo(elm, h("div#attached", "Text")),
|
||||
]),
|
||||
]);
|
||||
const vnode2 = h("div", [
|
||||
h("div#wrapper", [
|
||||
h("div", "Some element"),
|
||||
h("div", "A new element"),
|
||||
attachTo(elm, h("div#attached", "Text")),
|
||||
]),
|
||||
]);
|
||||
elm = patch(vnode0, vnode1).elm;
|
||||
assert.strictEqual(elm.children[0].innerHTML, "Text");
|
||||
elm = patch(vnode1, vnode2).elm;
|
||||
assert.strictEqual(elm.children[0].innerHTML, "Text");
|
||||
});
|
||||
it("removes element at target", function () {
|
||||
const vnode1 = h("div", [
|
||||
h("div#wrapper", [
|
||||
h("div", "Some element"),
|
||||
attachTo(elm, h("div#attached", "First text")),
|
||||
]),
|
||||
]);
|
||||
const vnode2 = h("div", [h("div#wrapper", [h("div", "Some element")])]);
|
||||
elm = patch(vnode0, vnode1).elm;
|
||||
assert.strictEqual(elm.children[0].innerHTML, "First text");
|
||||
elm = patch(vnode1, vnode2).elm;
|
||||
assert.strictEqual(elm.children.length, 1);
|
||||
});
|
||||
it("remove hook receives real element", function () {
|
||||
const rm: RemoveHook = (vnode, cb) => {
|
||||
const elm = vnode.elm as HTMLDivElement;
|
||||
assert.strictEqual(elm.tagName, "DIV");
|
||||
assert.strictEqual(elm.innerHTML, "First text");
|
||||
cb();
|
||||
};
|
||||
const vnode1 = h("div", [
|
||||
h("div#wrapper", [
|
||||
h("div", "Some element"),
|
||||
attachTo(
|
||||
elm,
|
||||
h("div#attached", { hook: { remove: rm } }, "First text")
|
||||
),
|
||||
]),
|
||||
]);
|
||||
const vnode2 = h("div", [h("div#wrapper", [h("div", "Some element")])]);
|
||||
elm = patch(vnode0, vnode1).elm;
|
||||
elm = patch(vnode1, vnode2).elm;
|
||||
});
|
||||
});
|
@ -0,0 +1,106 @@
|
||||
import { assert } from "chai";
|
||||
|
||||
import { init, attributesModule, h } from "../../src/index";
|
||||
|
||||
const patch = init([attributesModule]);
|
||||
|
||||
describe("attributes", function () {
|
||||
let elm: any, vnode0: any;
|
||||
beforeEach(function () {
|
||||
elm = document.createElement("div");
|
||||
vnode0 = elm;
|
||||
});
|
||||
it("have their provided values", function () {
|
||||
const vnode1 = h("div", {
|
||||
attrs: { href: "/foo", minlength: 1, selected: true, disabled: false },
|
||||
});
|
||||
elm = patch(vnode0, vnode1).elm;
|
||||
assert.strictEqual(elm.getAttribute("href"), "/foo");
|
||||
assert.strictEqual(elm.getAttribute("minlength"), "1");
|
||||
assert.strictEqual(elm.hasAttribute("selected"), true);
|
||||
assert.strictEqual(elm.getAttribute("selected"), "");
|
||||
assert.strictEqual(elm.hasAttribute("disabled"), false);
|
||||
});
|
||||
it("can be memoized", function () {
|
||||
const cachedAttrs = { href: "/foo", minlength: 1, selected: true };
|
||||
const vnode1 = h("div", { attrs: cachedAttrs });
|
||||
const vnode2 = h("div", { attrs: cachedAttrs });
|
||||
elm = patch(vnode0, vnode1).elm;
|
||||
assert.strictEqual(elm.getAttribute("href"), "/foo");
|
||||
assert.strictEqual(elm.getAttribute("minlength"), "1");
|
||||
assert.strictEqual(elm.getAttribute("selected"), "");
|
||||
elm = patch(vnode1, vnode2).elm;
|
||||
assert.strictEqual(elm.getAttribute("href"), "/foo");
|
||||
assert.strictEqual(elm.getAttribute("minlength"), "1");
|
||||
assert.strictEqual(elm.getAttribute("selected"), "");
|
||||
});
|
||||
it("are not omitted when falsy values are provided", function () {
|
||||
const vnode1 = h("div", {
|
||||
attrs: { href: null as any, minlength: 0, value: "", title: "undefined" },
|
||||
});
|
||||
elm = patch(vnode0, vnode1).elm;
|
||||
assert.ok(elm.hasAttribute("href"));
|
||||
assert.ok(elm.hasAttribute("minlength"));
|
||||
assert.ok(elm.hasAttribute("value"));
|
||||
assert.ok(elm.hasAttribute("title"));
|
||||
});
|
||||
it("are set correctly when namespaced", function () {
|
||||
const vnode1 = h("div", { attrs: { "xlink:href": "#foo" } });
|
||||
elm = patch(vnode0, vnode1).elm;
|
||||
assert.strictEqual(
|
||||
elm.getAttributeNS("http://www.w3.org/1999/xlink", "href"),
|
||||
"#foo"
|
||||
);
|
||||
});
|
||||
it("should not touch class nor id fields", function () {
|
||||
elm = document.createElement("div");
|
||||
elm.id = "myId";
|
||||
elm.className = "myClass";
|
||||
vnode0 = elm;
|
||||
const vnode1 = h("div#myId.myClass", { attrs: {} }, ["Hello"]);
|
||||
elm = patch(vnode0, vnode1).elm;
|
||||
assert.strictEqual(elm.tagName, "DIV");
|
||||
assert.strictEqual(elm.id, "myId");
|
||||
assert.strictEqual(elm.className, "myClass");
|
||||
assert.strictEqual(elm.textContent, "Hello");
|
||||
});
|
||||
describe("boolean attribute", function () {
|
||||
it("is present and empty string if the value is truthy", function () {
|
||||
const vnode1 = h("div", {
|
||||
attrs: { required: true, readonly: 1, noresize: "truthy" },
|
||||
});
|
||||
elm = patch(vnode0, vnode1).elm;
|
||||
assert.strictEqual(elm.hasAttribute("required"), true);
|
||||
assert.strictEqual(elm.getAttribute("required"), "");
|
||||
assert.strictEqual(elm.hasAttribute("readonly"), true);
|
||||
assert.strictEqual(elm.getAttribute("readonly"), "1");
|
||||
assert.strictEqual(elm.hasAttribute("noresize"), true);
|
||||
assert.strictEqual(elm.getAttribute("noresize"), "truthy");
|
||||
});
|
||||
it("is omitted if the value is false", function () {
|
||||
const vnode1 = h("div", { attrs: { required: false } });
|
||||
elm = patch(vnode0, vnode1).elm;
|
||||
assert.strictEqual(elm.hasAttribute("required"), false);
|
||||
assert.strictEqual(elm.getAttribute("required"), null);
|
||||
});
|
||||
it("is not omitted if the value is falsy", function () {
|
||||
const vnode1 = h("div", {
|
||||
attrs: { readonly: 0, noresize: null as any },
|
||||
});
|
||||
elm = patch(vnode0, vnode1).elm;
|
||||
assert.ok(elm.hasAttribute("readonly"));
|
||||
assert.ok(elm.hasAttribute("noresize"));
|
||||
});
|
||||
});
|
||||
describe("Object.prototype property", function () {
|
||||
it("is not considered as a boolean attribute and shouldn't be omitted", function () {
|
||||
const vnode1 = h("div", { attrs: { constructor: true } });
|
||||
elm = patch(vnode0, vnode1).elm;
|
||||
assert.strictEqual(elm.hasAttribute("constructor"), true);
|
||||
assert.strictEqual(elm.getAttribute("constructor"), "");
|
||||
const vnode2 = h("div", { attrs: { constructor: false } });
|
||||
elm = patch(vnode0, vnode2).elm;
|
||||
assert.strictEqual(elm.hasAttribute("constructor"), false);
|
||||
});
|
||||
});
|
||||
});
|
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,64 @@
|
||||
import { assert } from "chai";
|
||||
|
||||
import { datasetModule, init, h } from "../../src/index";
|
||||
|
||||
const patch = init([datasetModule]);
|
||||
|
||||
describe("dataset", function () {
|
||||
before(function () {
|
||||
if (!Object.hasOwnProperty.call(HTMLElement.prototype, "dataset")) {
|
||||
this.skip();
|
||||
}
|
||||
});
|
||||
|
||||
let elm: any, vnode0: any;
|
||||
beforeEach(function () {
|
||||
elm = document.createElement("div");
|
||||
vnode0 = elm;
|
||||
});
|
||||
it("is set on initial element creation", function () {
|
||||
elm = patch(vnode0, h("div", { dataset: { foo: "foo" } })).elm;
|
||||
assert.strictEqual(elm.dataset.foo, "foo");
|
||||
});
|
||||
it("updates dataset", function () {
|
||||
const vnode1 = h("i", { dataset: { foo: "foo", bar: "bar" } });
|
||||
const vnode2 = h("i", { dataset: { baz: "baz" } });
|
||||
elm = patch(vnode0, vnode1).elm;
|
||||
assert.strictEqual(elm.dataset.foo, "foo");
|
||||
assert.strictEqual(elm.dataset.bar, "bar");
|
||||
elm = patch(vnode1, vnode2).elm;
|
||||
assert.strictEqual(elm.dataset.baz, "baz");
|
||||
assert.strictEqual(elm.dataset.foo, undefined);
|
||||
});
|
||||
it("can be memoized", function () {
|
||||
const cachedDataset = { foo: "foo", bar: "bar" };
|
||||
const vnode1 = h("i", { dataset: cachedDataset });
|
||||
const vnode2 = h("i", { dataset: cachedDataset });
|
||||
elm = patch(vnode0, vnode1).elm;
|
||||
assert.strictEqual(elm.dataset.foo, "foo");
|
||||
assert.strictEqual(elm.dataset.bar, "bar");
|
||||
elm = patch(vnode1, vnode2).elm;
|
||||
assert.strictEqual(elm.dataset.foo, "foo");
|
||||
assert.strictEqual(elm.dataset.bar, "bar");
|
||||
});
|
||||
it("handles string conversions", function () {
|
||||
const vnode1 = h("i", {
|
||||
dataset: {
|
||||
empty: "",
|
||||
dash: "-",
|
||||
dashed: "foo-bar",
|
||||
camel: "fooBar",
|
||||
integer: 0 as any,
|
||||
float: 0.1 as any,
|
||||
},
|
||||
});
|
||||
elm = patch(vnode0, vnode1).elm;
|
||||
|
||||
assert.strictEqual(elm.dataset.empty, "");
|
||||
assert.strictEqual(elm.dataset.dash, "-");
|
||||
assert.strictEqual(elm.dataset.dashed, "foo-bar");
|
||||
assert.strictEqual(elm.dataset.camel, "fooBar");
|
||||
assert.strictEqual(elm.dataset.integer, "0");
|
||||
assert.strictEqual(elm.dataset.float, "0.1");
|
||||
});
|
||||
});
|
@ -0,0 +1,125 @@
|
||||
import { assert } from "chai";
|
||||
|
||||
import { VNode, init, eventListenersModule, h } from "../../src/index";
|
||||
|
||||
const patch = init([eventListenersModule]);
|
||||
|
||||
describe("event listeners", function () {
|
||||
let elm: any, vnode0: any;
|
||||
beforeEach(function () {
|
||||
elm = document.createElement("div");
|
||||
vnode0 = elm;
|
||||
});
|
||||
it("attaches click event handler to element", function () {
|
||||
const result = [];
|
||||
function clicked(ev: Event) {
|
||||
result.push(ev);
|
||||
}
|
||||
const vnode = h("div", { on: { click: clicked } }, [
|
||||
h("a", "Click my parent"),
|
||||
]);
|
||||
elm = patch(vnode0, vnode).elm;
|
||||
elm.click();
|
||||
assert.strictEqual(1, result.length);
|
||||
});
|
||||
it("does not attach new listener", function () {
|
||||
const result: number[] = [];
|
||||
// function clicked(ev) { result.push(ev); }
|
||||
const vnode1 = h(
|
||||
"div",
|
||||
{
|
||||
on: {
|
||||
click: function () {
|
||||
result.push(1);
|
||||
},
|
||||
},
|
||||
},
|
||||
[h("a", "Click my parent")]
|
||||
);
|
||||
const vnode2 = h(
|
||||
"div",
|
||||
{
|
||||
on: {
|
||||
click: function () {
|
||||
result.push(2);
|
||||
},
|
||||
},
|
||||
},
|
||||
[h("a", "Click my parent")]
|
||||
);
|
||||
elm = patch(vnode0, vnode1).elm;
|
||||
elm.click();
|
||||
elm = patch(vnode1, vnode2).elm;
|
||||
elm.click();
|
||||
assert.deepEqual(result, [1, 2]);
|
||||
});
|
||||
it("detach attached click event handler to element", function () {
|
||||
const result: Event[] = [];
|
||||
function clicked(ev: Event) {
|
||||
result.push(ev);
|
||||
}
|
||||
const vnode1 = h("div", { on: { click: clicked } }, [
|
||||
h("a", "Click my parent"),
|
||||
]);
|
||||
elm = patch(vnode0, vnode1).elm;
|
||||
elm.click();
|
||||
assert.strictEqual(1, result.length);
|
||||
const vnode2 = h("div", { on: {} }, [h("a", "Click my parent")]);
|
||||
elm = patch(vnode1, vnode2).elm;
|
||||
elm.click();
|
||||
assert.strictEqual(1, result.length);
|
||||
});
|
||||
it("multiple event handlers for same event on same element", function () {
|
||||
let called = 0;
|
||||
function clicked(ev: Event, vnode: VNode) {
|
||||
++called;
|
||||
// Check that the first argument is an event
|
||||
assert.strictEqual(true, "target" in ev);
|
||||
// Check that the second argument was a vnode
|
||||
assert.strictEqual(vnode.sel, "div");
|
||||
}
|
||||
const vnode1 = h("div", { on: { click: [clicked, clicked, clicked] } }, [
|
||||
h("a", "Click my parent"),
|
||||
]);
|
||||
elm = patch(vnode0, vnode1).elm;
|
||||
elm.click();
|
||||
assert.strictEqual(3, called);
|
||||
const vnode2 = h("div", { on: { click: [clicked, clicked] } }, [
|
||||
h("a", "Click my parent"),
|
||||
]);
|
||||
elm = patch(vnode1, vnode2).elm;
|
||||
elm.click();
|
||||
assert.strictEqual(5, called);
|
||||
});
|
||||
it("access to virtual node in event handler", function () {
|
||||
const result: VNode[] = [];
|
||||
function clicked(this: VNode, ev: Event, vnode: VNode) {
|
||||
result.push(this);
|
||||
result.push(vnode);
|
||||
}
|
||||
const vnode1 = h("div", { on: { click: clicked } }, [
|
||||
h("a", "Click my parent"),
|
||||
]);
|
||||
elm = patch(vnode0, vnode1).elm;
|
||||
elm.click();
|
||||
assert.strictEqual(2, result.length);
|
||||
assert.strictEqual(vnode1, result[0]);
|
||||
assert.strictEqual(vnode1, result[1]);
|
||||
});
|
||||
it("shared handlers in parent and child nodes", function () {
|
||||
const result = [];
|
||||
const sharedHandlers = {
|
||||
click: function (ev: Event) {
|
||||
result.push(ev);
|
||||
},
|
||||
};
|
||||
const vnode1 = h("div", { on: sharedHandlers }, [
|
||||
h("a", { on: sharedHandlers }, "Click my parent"),
|
||||
]);
|
||||
elm = patch(vnode0, vnode1).elm;
|
||||
elm.click();
|
||||
assert.strictEqual(1, result.length);
|
||||
elm.firstChild.click();
|
||||
assert.strictEqual(3, result.length);
|
||||
});
|
||||
});
|
@ -0,0 +1,50 @@
|
||||
import { assert } from "chai";
|
||||
|
||||
import { init, h, attributesModule } from "../../src/index";
|
||||
|
||||
const patch = init([attributesModule]);
|
||||
|
||||
describe("svg", function () {
|
||||
let elm: any, vnode0: any;
|
||||
beforeEach(function () {
|
||||
elm = document.createElement("svg");
|
||||
vnode0 = elm;
|
||||
});
|
||||
|
||||
it("removes child svg elements", function () {
|
||||
const a = h("svg", {}, [h("g"), h("g")]);
|
||||
const b = h("svg", {}, [h("g")]);
|
||||
const result = patch(patch(vnode0, a), b).elm as SVGElement;
|
||||
assert.strictEqual(result.childNodes.length, 1);
|
||||
});
|
||||
|
||||
it("adds correctly xlink namespaced attribute", function () {
|
||||
const xlinkNS = "http://www.w3.org/1999/xlink";
|
||||
const testUrl = "/test";
|
||||
const a = h("svg", {}, [
|
||||
h(
|
||||
"use",
|
||||
{
|
||||
attrs: { "xlink:href": testUrl },
|
||||
},
|
||||
[]
|
||||
),
|
||||
]);
|
||||
|
||||
const result = patch(vnode0, a).elm as SVGElement;
|
||||
assert.strictEqual(result.childNodes.length, 1);
|
||||
const child = result.childNodes[0] as SVGUseElement;
|
||||
assert.strictEqual(child.getAttribute("xlink:href"), testUrl);
|
||||
assert.strictEqual(child.getAttributeNS(xlinkNS, "href"), testUrl);
|
||||
});
|
||||
|
||||
it("adds correctly xml namespaced attribute", function () {
|
||||
const xmlNS = "http://www.w3.org/XML/1998/namespace";
|
||||
const testAttrValue = "und";
|
||||
const a = h("svg", { attrs: { "xml:lang": testAttrValue } }, []);
|
||||
|
||||
const result = patch(vnode0, a).elm as SVGElement;
|
||||
assert.strictEqual(result.getAttributeNS(xmlNS, "lang"), testAttrValue);
|
||||
assert.strictEqual(result.getAttribute("xml:lang"), testAttrValue);
|
||||
});
|
||||
});
|
@ -0,0 +1,174 @@
|
||||
import { assert } from "chai";
|
||||
|
||||
import { init, styleModule, h, toVNode } from "../../src/index";
|
||||
|
||||
const patch = init([styleModule]);
|
||||
|
||||
const featureDiscoveryElm = document.createElement("div");
|
||||
featureDiscoveryElm.style.setProperty("--foo", "foo");
|
||||
const hasCssVariables =
|
||||
featureDiscoveryElm.style.getPropertyValue("--foo") === "foo";
|
||||
|
||||
describe("style", function () {
|
||||
let elm: any, vnode0: any;
|
||||
beforeEach(function () {
|
||||
elm = document.createElement("div");
|
||||
vnode0 = elm;
|
||||
});
|
||||
it("is being styled", function () {
|
||||
elm = patch(vnode0, h("div", { style: { fontSize: "12px" } })).elm;
|
||||
assert.strictEqual(elm.style.fontSize, "12px");
|
||||
});
|
||||
it("can be memoized", function () {
|
||||
const cachedStyles = { fontSize: "14px", display: "inline" };
|
||||
const vnode1 = h("i", { style: cachedStyles });
|
||||
const vnode2 = h("i", { style: cachedStyles });
|
||||
elm = patch(vnode0, vnode1).elm;
|
||||
assert.strictEqual(elm.style.fontSize, "14px");
|
||||
assert.strictEqual(elm.style.display, "inline");
|
||||
elm = patch(vnode1, vnode2).elm;
|
||||
assert.strictEqual(elm.style.fontSize, "14px");
|
||||
assert.strictEqual(elm.style.display, "inline");
|
||||
});
|
||||
it("updates styles", function () {
|
||||
const vnode1 = h("i", { style: { fontSize: "14px", display: "inline" } });
|
||||
const vnode2 = h("i", { style: { fontSize: "12px", display: "block" } });
|
||||
const vnode3 = h("i", { style: { fontSize: "10px", display: "block" } });
|
||||
elm = patch(vnode0, vnode1).elm;
|
||||
assert.strictEqual(elm.style.fontSize, "14px");
|
||||
assert.strictEqual(elm.style.display, "inline");
|
||||
elm = patch(vnode1, vnode2).elm;
|
||||
assert.strictEqual(elm.style.fontSize, "12px");
|
||||
assert.strictEqual(elm.style.display, "block");
|
||||
elm = patch(vnode2, vnode3).elm;
|
||||
assert.strictEqual(elm.style.fontSize, "10px");
|
||||
assert.strictEqual(elm.style.display, "block");
|
||||
});
|
||||
it("explicialy removes styles", function () {
|
||||
const vnode1 = h("i", { style: { fontSize: "14px" } });
|
||||
const vnode2 = h("i", { style: { fontSize: "" } });
|
||||
const vnode3 = h("i", { style: { fontSize: "10px" } });
|
||||
elm = patch(vnode0, vnode1).elm;
|
||||
assert.strictEqual(elm.style.fontSize, "14px");
|
||||
patch(vnode1, vnode2);
|
||||
assert.strictEqual(elm.style.fontSize, "");
|
||||
patch(vnode2, vnode3);
|
||||
assert.strictEqual(elm.style.fontSize, "10px");
|
||||
});
|
||||
it("implicially removes styles from element", function () {
|
||||
const vnode1 = h("div", [h("i", { style: { fontSize: "14px" } })]);
|
||||
const vnode2 = h("div", [h("i")]);
|
||||
const vnode3 = h("div", [h("i", { style: { fontSize: "10px" } })]);
|
||||
patch(vnode0, vnode1);
|
||||
assert.strictEqual(elm.firstChild.style.fontSize, "14px");
|
||||
patch(vnode1, vnode2);
|
||||
assert.strictEqual(elm.firstChild.style.fontSize, "");
|
||||
patch(vnode2, vnode3);
|
||||
assert.strictEqual(elm.firstChild.style.fontSize, "10px");
|
||||
});
|
||||
it("updates css variables", function () {
|
||||
if (!hasCssVariables) {
|
||||
this.skip();
|
||||
} else {
|
||||
const vnode1 = h("div", { style: { "--myVar": 1 as any } });
|
||||
const vnode2 = h("div", { style: { "--myVar": 2 as any } });
|
||||
const vnode3 = h("div", { style: { "--myVar": 3 as any } });
|
||||
elm = patch(vnode0, vnode1).elm;
|
||||
assert.strictEqual(elm.style.getPropertyValue("--myVar"), "1");
|
||||
elm = patch(vnode1, vnode2).elm;
|
||||
assert.strictEqual(elm.style.getPropertyValue("--myVar"), "2");
|
||||
elm = patch(vnode2, vnode3).elm;
|
||||
assert.strictEqual(elm.style.getPropertyValue("--myVar"), "3");
|
||||
}
|
||||
});
|
||||
it("explicialy removes css variables", function () {
|
||||
if (!hasCssVariables) {
|
||||
this.skip();
|
||||
} else {
|
||||
const vnode1 = h("i", { style: { "--myVar": 1 as any } });
|
||||
const vnode2 = h("i", { style: { "--myVar": "" } });
|
||||
const vnode3 = h("i", { style: { "--myVar": 2 as any } });
|
||||
elm = patch(vnode0, vnode1).elm;
|
||||
assert.strictEqual(elm.style.getPropertyValue("--myVar"), "1");
|
||||
patch(vnode1, vnode2);
|
||||
assert.strictEqual(elm.style.getPropertyValue("--myVar"), "");
|
||||
patch(vnode2, vnode3);
|
||||
assert.strictEqual(elm.style.getPropertyValue("--myVar"), "2");
|
||||
}
|
||||
});
|
||||
it("implicially removes css variables from element", function () {
|
||||
if (!hasCssVariables) {
|
||||
this.skip();
|
||||
} else {
|
||||
const vnode1 = h("div", [h("i", { style: { "--myVar": 1 as any } })]);
|
||||
const vnode2 = h("div", [h("i")]);
|
||||
const vnode3 = h("div", [h("i", { style: { "--myVar": 2 as any } })]);
|
||||
patch(vnode0, vnode1);
|
||||
assert.strictEqual(elm.firstChild.style.getPropertyValue("--myVar"), "1");
|
||||
patch(vnode1, vnode2);
|
||||
assert.strictEqual(elm.firstChild.style.getPropertyValue("--myVar"), "");
|
||||
patch(vnode2, vnode3);
|
||||
assert.strictEqual(elm.firstChild.style.getPropertyValue("--myVar"), "2");
|
||||
}
|
||||
});
|
||||
it("updates delayed styles in next frame", function (done) {
|
||||
const vnode1 = h("i", {
|
||||
style: { fontSize: "14px", delayed: { fontSize: "16px" } as any },
|
||||
});
|
||||
const vnode2 = h("i", {
|
||||
style: { fontSize: "18px", delayed: { fontSize: "20px" } as any },
|
||||
});
|
||||
elm = patch(vnode0, vnode1).elm;
|
||||
assert.strictEqual(elm.style.fontSize, "14px");
|
||||
requestAnimationFrame(() => {
|
||||
requestAnimationFrame(() => {
|
||||
assert.strictEqual(elm.style.fontSize, "16px");
|
||||
elm = patch(vnode1, vnode2).elm;
|
||||
assert.strictEqual(elm.style.fontSize, "18px");
|
||||
requestAnimationFrame(() => {
|
||||
requestAnimationFrame(() => {
|
||||
assert.strictEqual(elm.style.fontSize, "20px");
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
it("applies tranform as transition on remove", function (done) {
|
||||
const btn = h(
|
||||
"button",
|
||||
{
|
||||
style: {
|
||||
transition: "transform 0.1s",
|
||||
remove: { transform: "translateY(100%)" } as any,
|
||||
},
|
||||
},
|
||||
["A button"]
|
||||
);
|
||||
const vnode1 = h("div.parent", {}, [btn]);
|
||||
const vnode2 = h("div.parent", {}, [null]);
|
||||
document.body.appendChild(vnode0);
|
||||
patch(vnode0, vnode1);
|
||||
patch(vnode1, vnode2);
|
||||
const button = document.querySelector("button") as HTMLButtonElement;
|
||||
assert.notStrictEqual(button, null);
|
||||
button.addEventListener("transitionend", function () {
|
||||
assert.strictEqual(document.querySelector("button"), null);
|
||||
done();
|
||||
});
|
||||
});
|
||||
describe("using toVNode()", function () {
|
||||
it("handles (ignoring) comment nodes", function () {
|
||||
const comment = document.createComment("yolo");
|
||||
const prevElm = document.createElement("div");
|
||||
prevElm.appendChild(comment);
|
||||
const nextVNode = h("div", [h("span", "Hi")]);
|
||||
elm = patch(toVNode(prevElm), nextVNode).elm;
|
||||
assert.strictEqual(elm, prevElm);
|
||||
assert.strictEqual(elm.tagName, "DIV");
|
||||
assert.strictEqual(elm.childNodes.length, 1);
|
||||
assert.strictEqual(elm.childNodes[0].tagName, "SPAN");
|
||||
assert.strictEqual(elm.childNodes[0].textContent, "Hi");
|
||||
});
|
||||
});
|
||||
});
|
@ -0,0 +1,210 @@
|
||||
import { assert } from "chai";
|
||||
|
||||
import { init, h, thunk, VNode } from "../../src/index";
|
||||
|
||||
const patch = init([]);
|
||||
|
||||
describe("thunk", function () {
|
||||
let elm: any, vnode0: any;
|
||||
beforeEach(function () {
|
||||
elm = vnode0 = document.createElement("div");
|
||||
});
|
||||
it("returns vnode with data and render function", function () {
|
||||
function numberInSpan(n: number) {
|
||||
return h("span", `Number is ${n}`);
|
||||
}
|
||||
const vnode = thunk("span", "num", numberInSpan, [22]);
|
||||
assert.deepEqual(vnode.sel, "span");
|
||||
assert.deepEqual(vnode.data.key, "num");
|
||||
assert.deepEqual(vnode.data.args, [22]);
|
||||
});
|
||||
it("calls render function once on data change", function () {
|
||||
let called = 0;
|
||||
function numberInSpan(n: number) {
|
||||
called++;
|
||||
return h("span", { key: "num" }, `Number is ${n}`);
|
||||
}
|
||||
const vnode1 = h("div", [thunk("span", "num", numberInSpan, [1])]);
|
||||
const vnode2 = h("div", [thunk("span", "num", numberInSpan, [2])]);
|
||||
patch(vnode0, vnode1);
|
||||
assert.strictEqual(called, 1);
|
||||
patch(vnode1, vnode2);
|
||||
assert.strictEqual(called, 2);
|
||||
});
|
||||
it("does not call render function on data unchanged", function () {
|
||||
let called = 0;
|
||||
function numberInSpan(n: number) {
|
||||
called++;
|
||||
return h("span", { key: "num" }, `Number is ${n}`);
|
||||
}
|
||||
const vnode1 = h("div", [thunk("span", "num", numberInSpan, [1])]);
|
||||
const vnode2 = h("div", [thunk("span", "num", numberInSpan, [1])]);
|
||||
patch(vnode0, vnode1);
|
||||
assert.strictEqual(called, 1);
|
||||
patch(vnode1, vnode2);
|
||||
assert.strictEqual(called, 1);
|
||||
});
|
||||
it("calls render function once on data-length change", function () {
|
||||
let called = 0;
|
||||
function numberInSpan(n: number) {
|
||||
called++;
|
||||
return h("span", { key: "num" }, `Number is ${n}`);
|
||||
}
|
||||
const vnode1 = h("div", [thunk("span", "num", numberInSpan, [1])]);
|
||||
const vnode2 = h("div", [thunk("span", "num", numberInSpan, [1, 2])]);
|
||||
patch(vnode0, vnode1);
|
||||
assert.strictEqual(called, 1);
|
||||
patch(vnode1, vnode2);
|
||||
assert.strictEqual(called, 2);
|
||||
});
|
||||
it("calls render function once on function change", function () {
|
||||
let called = 0;
|
||||
function numberInSpan(n: number) {
|
||||
called++;
|
||||
return h("span", { key: "num" }, `Number is ${n}`);
|
||||
}
|
||||
function numberInSpan2(n: number) {
|
||||
called++;
|
||||
return h("span", { key: "num" }, `Number really is ${n}`);
|
||||
}
|
||||
const vnode1 = h("div", [thunk("span", "num", numberInSpan, [1])]);
|
||||
const vnode2 = h("div", [thunk("span", "num", numberInSpan2, [1])]);
|
||||
patch(vnode0, vnode1);
|
||||
assert.strictEqual(called, 1);
|
||||
patch(vnode1, vnode2);
|
||||
assert.strictEqual(called, 2);
|
||||
});
|
||||
it("renders correctly", function () {
|
||||
let called = 0;
|
||||
function numberInSpan(n: number) {
|
||||
called++;
|
||||
return h("span", { key: "num" }, `Number is ${n}`);
|
||||
}
|
||||
const vnode1 = h("div", [thunk("span", "num", numberInSpan, [1])]);
|
||||
const vnode2 = h("div", [thunk("span", "num", numberInSpan, [1])]);
|
||||
const vnode3 = h("div", [thunk("span", "num", numberInSpan, [2])]);
|
||||
elm = patch(vnode0, vnode1).elm;
|
||||
assert.strictEqual(elm.firstChild.tagName.toLowerCase(), "span");
|
||||
assert.strictEqual(elm.firstChild.innerHTML, "Number is 1");
|
||||
elm = patch(vnode1, vnode2).elm;
|
||||
assert.strictEqual(elm.firstChild.tagName.toLowerCase(), "span");
|
||||
assert.strictEqual(elm.firstChild.innerHTML, "Number is 1");
|
||||
elm = patch(vnode2, vnode3).elm;
|
||||
assert.strictEqual(elm.firstChild.tagName.toLowerCase(), "span");
|
||||
assert.strictEqual(elm.firstChild.innerHTML, "Number is 2");
|
||||
assert.strictEqual(called, 2);
|
||||
});
|
||||
it("supports leaving out the `key` argument", function () {
|
||||
function vnodeFn(s: string) {
|
||||
return h("span.number", "Hello " + s);
|
||||
}
|
||||
const vnode1 = thunk("span.number", vnodeFn, ["World!"]);
|
||||
elm = patch(vnode0, vnode1).elm;
|
||||
assert.strictEqual(elm.innerText, "Hello World!");
|
||||
});
|
||||
it("renders correctly when root", function () {
|
||||
let called = 0;
|
||||
function numberInSpan(n: number) {
|
||||
called++;
|
||||
return h("span", { key: "num" }, `Number is ${n}`);
|
||||
}
|
||||
const vnode1 = thunk("span", "num", numberInSpan, [1]);
|
||||
const vnode2 = thunk("span", "num", numberInSpan, [1]);
|
||||
const vnode3 = thunk("span", "num", numberInSpan, [2]);
|
||||
|
||||
elm = patch(vnode0, vnode1).elm;
|
||||
assert.strictEqual(elm.tagName.toLowerCase(), "span");
|
||||
assert.strictEqual(elm.innerHTML, "Number is 1");
|
||||
|
||||
elm = patch(vnode1, vnode2).elm;
|
||||
assert.strictEqual(elm.tagName.toLowerCase(), "span");
|
||||
assert.strictEqual(elm.innerHTML, "Number is 1");
|
||||
|
||||
elm = patch(vnode2, vnode3).elm;
|
||||
assert.strictEqual(elm.tagName.toLowerCase(), "span");
|
||||
assert.strictEqual(elm.innerHTML, "Number is 2");
|
||||
assert.strictEqual(called, 2);
|
||||
});
|
||||
it("can be replaced and removed", function () {
|
||||
function numberInSpan(n: number) {
|
||||
return h("span", { key: "num" }, `Number is ${n}`);
|
||||
}
|
||||
function oddEven(n: number): VNode {
|
||||
const prefix = n % 2 === 0 ? "Even" : "Odd";
|
||||
return h("div", { key: oddEven as any }, `${prefix}: ${n}`);
|
||||
}
|
||||
const vnode1 = h("div", [thunk("span", "num", numberInSpan, [1])]);
|
||||
const vnode2 = h("div", [thunk("div", "oddEven", oddEven, [4])]);
|
||||
|
||||
elm = patch(vnode0, vnode1).elm;
|
||||
assert.strictEqual(elm.firstChild.tagName.toLowerCase(), "span");
|
||||
assert.strictEqual(elm.firstChild.innerHTML, "Number is 1");
|
||||
|
||||
elm = patch(vnode1, vnode2).elm;
|
||||
assert.strictEqual(elm.firstChild.tagName.toLowerCase(), "div");
|
||||
assert.strictEqual(elm.firstChild.innerHTML, "Even: 4");
|
||||
});
|
||||
it("can be replaced and removed when root", function () {
|
||||
function numberInSpan(n: number) {
|
||||
return h("span", { key: "num" }, `Number is ${n}`);
|
||||
}
|
||||
function oddEven(n: number): VNode {
|
||||
const prefix = n % 2 === 0 ? "Even" : "Odd";
|
||||
return h("div", { key: oddEven as any }, `${prefix}: ${n}`);
|
||||
}
|
||||
const vnode1 = thunk("span", "num", numberInSpan, [1]);
|
||||
const vnode2 = thunk("div", "oddEven", oddEven, [4]);
|
||||
|
||||
elm = patch(vnode0, vnode1).elm;
|
||||
assert.strictEqual(elm.tagName.toLowerCase(), "span");
|
||||
assert.strictEqual(elm.innerHTML, "Number is 1");
|
||||
|
||||
elm = patch(vnode1, vnode2).elm;
|
||||
assert.strictEqual(elm.tagName.toLowerCase(), "div");
|
||||
assert.strictEqual(elm.innerHTML, "Even: 4");
|
||||
});
|
||||
it("invokes destroy hook on thunks", function () {
|
||||
let called = 0;
|
||||
function destroyHook() {
|
||||
called++;
|
||||
}
|
||||
function numberInSpan(n: number) {
|
||||
return h(
|
||||
"span",
|
||||
{ key: "num", hook: { destroy: destroyHook } },
|
||||
`Number is ${n}`
|
||||
);
|
||||
}
|
||||
const vnode1 = h("div", [
|
||||
h("div", "Foo"),
|
||||
thunk("span", "num", numberInSpan, [1]),
|
||||
h("div", "Foo"),
|
||||
]);
|
||||
const vnode2 = h("div", [h("div", "Foo"), h("div", "Foo")]);
|
||||
patch(vnode0, vnode1);
|
||||
patch(vnode1, vnode2);
|
||||
assert.strictEqual(called, 1);
|
||||
});
|
||||
it("invokes remove hook on thunks", function () {
|
||||
let called = 0;
|
||||
function hook() {
|
||||
called++;
|
||||
}
|
||||
function numberInSpan(n: number) {
|
||||
return h(
|
||||
"span",
|
||||
{ key: "num", hook: { remove: hook } },
|
||||
`Number is ${n}`
|
||||
);
|
||||
}
|
||||
const vnode1 = h("div", [
|
||||
h("div", "Foo"),
|
||||
thunk("span", "num", numberInSpan, [1]),
|
||||
h("div", "Foo"),
|
||||
]);
|
||||
const vnode2 = h("div", [h("div", "Foo"), h("div", "Foo")]);
|
||||
patch(vnode0, vnode1);
|
||||
patch(vnode1, vnode2);
|
||||
assert.strictEqual(called, 1);
|
||||
});
|
||||
});
|
@ -1,40 +0,0 @@
|
||||
const path = require('path')
|
||||
const isPathInside = require('is-path-inside')
|
||||
const globby = require('globby')
|
||||
|
||||
const outputPath = path.resolve(__dirname, 'build/test')
|
||||
const makeTestsWebpackConfig = async () => ({
|
||||
mode: 'development',
|
||||
target: 'browserslist:defaults',
|
||||
entry: Object.fromEntries(
|
||||
(await globby(path.resolve(outputPath, '**/*.js')))
|
||||
.map((item) => [path.relative(outputPath, item), item])
|
||||
),
|
||||
output: {
|
||||
path: path.resolve(__dirname, 'test-bundles'),
|
||||
filename: ({ chunk }) => chunk.name
|
||||
},
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
exclude: (input) => isPathInside(input, path.resolve(__dirname, 'node_modules')),
|
||||
test: /\.m?js$/,
|
||||
use: {
|
||||
loader: 'babel-loader',
|
||||
options: {
|
||||
presets: [
|
||||
[
|
||||
'@babel/preset-env',
|
||||
{
|
||||
useBuiltIns: 'usage',
|
||||
corejs: 3,
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
})
|
||||
module.exports = makeTestsWebpackConfig
|
@ -0,0 +1,13 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"removeComments": false,
|
||||
"sourceMap": true,
|
||||
"declaration": true,
|
||||
"strict": true,
|
||||
"strictFunctionTypes": false,
|
||||
"moduleResolution": "node",
|
||||
"target": "es6",
|
||||
"outDir": "build"
|
||||
},
|
||||
"include": ["./src/index.ts"]
|
||||
}
|
Loading…
Reference in New Issue