Initial commit
parent
43ca871505
commit
7f2452e9bb
|
@ -0,0 +1,17 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="user-scalable=no, width=device-width, initial-scale=1, maximum-scale=1">
|
||||
<title>Vite + TS</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<div id="custom-keyboard"></div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
|
@ -0,0 +1,574 @@
|
|||
{
|
||||
"name": "input-test",
|
||||
"version": "0.0.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "input-test",
|
||||
"version": "0.0.0",
|
||||
"dependencies": {
|
||||
"@types/hammerjs": "^2.0.41",
|
||||
"hammerjs": "^2.0.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5.0.2",
|
||||
"vite": "^4.3.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-arm": {
|
||||
"version": "0.17.18",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.17.18.tgz",
|
||||
"integrity": "sha512-EmwL+vUBZJ7mhFCs5lA4ZimpUH3WMAoqvOIYhVQwdIgSpHC8ImHdsRyhHAVxpDYUSm0lWvd63z0XH1IlImS2Qw==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-arm64": {
|
||||
"version": "0.17.18",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.17.18.tgz",
|
||||
"integrity": "sha512-/iq0aK0eeHgSC3z55ucMAHO05OIqmQehiGay8eP5l/5l+iEr4EIbh4/MI8xD9qRFjqzgkc0JkX0LculNC9mXBw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-x64": {
|
||||
"version": "0.17.18",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.17.18.tgz",
|
||||
"integrity": "sha512-x+0efYNBF3NPW2Xc5bFOSFW7tTXdAcpfEg2nXmxegm4mJuVeS+i109m/7HMiOQ6M12aVGGFlqJX3RhNdYM2lWg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/darwin-arm64": {
|
||||
"version": "0.17.18",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.17.18.tgz",
|
||||
"integrity": "sha512-6tY+djEAdF48M1ONWnQb1C+6LiXrKjmqjzPNPWXhu/GzOHTHX2nh8Mo2ZAmBFg0kIodHhciEgUBtcYCAIjGbjQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/darwin-x64": {
|
||||
"version": "0.17.18",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.17.18.tgz",
|
||||
"integrity": "sha512-Qq84ykvLvya3dO49wVC9FFCNUfSrQJLbxhoQk/TE1r6MjHo3sFF2tlJCwMjhkBVq3/ahUisj7+EpRSz0/+8+9A==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/freebsd-arm64": {
|
||||
"version": "0.17.18",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.17.18.tgz",
|
||||
"integrity": "sha512-fw/ZfxfAzuHfaQeMDhbzxp9mc+mHn1Y94VDHFHjGvt2Uxl10mT4CDavHm+/L9KG441t1QdABqkVYwakMUeyLRA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/freebsd-x64": {
|
||||
"version": "0.17.18",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.17.18.tgz",
|
||||
"integrity": "sha512-FQFbRtTaEi8ZBi/A6kxOC0V0E9B/97vPdYjY9NdawyLd4Qk5VD5g2pbWN2VR1c0xhzcJm74HWpObPszWC+qTew==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-arm": {
|
||||
"version": "0.17.18",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.17.18.tgz",
|
||||
"integrity": "sha512-jW+UCM40LzHcouIaqv3e/oRs0JM76JfhHjCavPxMUti7VAPh8CaGSlS7cmyrdpzSk7A+8f0hiedHqr/LMnfijg==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-arm64": {
|
||||
"version": "0.17.18",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.17.18.tgz",
|
||||
"integrity": "sha512-R7pZvQZFOY2sxUG8P6A21eq6q+eBv7JPQYIybHVf1XkQYC+lT7nDBdC7wWKTrbvMXKRaGudp/dzZCwL/863mZQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-ia32": {
|
||||
"version": "0.17.18",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.17.18.tgz",
|
||||
"integrity": "sha512-ygIMc3I7wxgXIxk6j3V00VlABIjq260i967Cp9BNAk5pOOpIXmd1RFQJQX9Io7KRsthDrQYrtcx7QCof4o3ZoQ==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-loong64": {
|
||||
"version": "0.17.18",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.17.18.tgz",
|
||||
"integrity": "sha512-bvPG+MyFs5ZlwYclCG1D744oHk1Pv7j8psF5TfYx7otCVmcJsEXgFEhQkbhNW8otDHL1a2KDINW20cfCgnzgMQ==",
|
||||
"cpu": [
|
||||
"loong64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-mips64el": {
|
||||
"version": "0.17.18",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.17.18.tgz",
|
||||
"integrity": "sha512-oVqckATOAGuiUOa6wr8TXaVPSa+6IwVJrGidmNZS1cZVx0HqkTMkqFGD2HIx9H1RvOwFeWYdaYbdY6B89KUMxA==",
|
||||
"cpu": [
|
||||
"mips64el"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-ppc64": {
|
||||
"version": "0.17.18",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.17.18.tgz",
|
||||
"integrity": "sha512-3dLlQO+b/LnQNxgH4l9rqa2/IwRJVN9u/bK63FhOPB4xqiRqlQAU0qDU3JJuf0BmaH0yytTBdoSBHrb2jqc5qQ==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-riscv64": {
|
||||
"version": "0.17.18",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.17.18.tgz",
|
||||
"integrity": "sha512-/x7leOyDPjZV3TcsdfrSI107zItVnsX1q2nho7hbbQoKnmoeUWjs+08rKKt4AUXju7+3aRZSsKrJtaRmsdL1xA==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-s390x": {
|
||||
"version": "0.17.18",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.17.18.tgz",
|
||||
"integrity": "sha512-cX0I8Q9xQkL/6F5zWdYmVf5JSQt+ZfZD2bJudZrWD+4mnUvoZ3TDDXtDX2mUaq6upMFv9FlfIh4Gfun0tbGzuw==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-x64": {
|
||||
"version": "0.17.18",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.17.18.tgz",
|
||||
"integrity": "sha512-66RmRsPlYy4jFl0vG80GcNRdirx4nVWAzJmXkevgphP1qf4dsLQCpSKGM3DUQCojwU1hnepI63gNZdrr02wHUA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/netbsd-x64": {
|
||||
"version": "0.17.18",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.17.18.tgz",
|
||||
"integrity": "sha512-95IRY7mI2yrkLlTLb1gpDxdC5WLC5mZDi+kA9dmM5XAGxCME0F8i4bYH4jZreaJ6lIZ0B8hTrweqG1fUyW7jbg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"netbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/openbsd-x64": {
|
||||
"version": "0.17.18",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.17.18.tgz",
|
||||
"integrity": "sha512-WevVOgcng+8hSZ4Q3BKL3n1xTv5H6Nb53cBrtzzEjDbbnOmucEVcZeGCsCOi9bAOcDYEeBZbD2SJNBxlfP3qiA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/sunos-x64": {
|
||||
"version": "0.17.18",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.17.18.tgz",
|
||||
"integrity": "sha512-Rzf4QfQagnwhQXVBS3BYUlxmEbcV7MY+BH5vfDZekU5eYpcffHSyjU8T0xucKVuOcdCsMo+Ur5wmgQJH2GfNrg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"sunos"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-arm64": {
|
||||
"version": "0.17.18",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.17.18.tgz",
|
||||
"integrity": "sha512-Kb3Ko/KKaWhjeAm2YoT/cNZaHaD1Yk/pa3FTsmqo9uFh1D1Rfco7BBLIPdDOozrObj2sahslFuAQGvWbgWldAg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-ia32": {
|
||||
"version": "0.17.18",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.17.18.tgz",
|
||||
"integrity": "sha512-0/xUMIdkVHwkvxfbd5+lfG7mHOf2FRrxNbPiKWg9C4fFrB8H0guClmaM3BFiRUYrznVoyxTIyC/Ou2B7QQSwmw==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-x64": {
|
||||
"version": "0.17.18",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.17.18.tgz",
|
||||
"integrity": "sha512-qU25Ma1I3NqTSHJUOKi9sAH1/Mzuvlke0ioMJRthLXKm7JiSKVwFghlGbDLOO2sARECGhja4xYfRAZNPAkooYg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/hammerjs": {
|
||||
"version": "2.0.41",
|
||||
"resolved": "https://registry.npmjs.org/@types/hammerjs/-/hammerjs-2.0.41.tgz",
|
||||
"integrity": "sha512-ewXv/ceBaJprikMcxCmWU1FKyMAQ2X7a9Gtmzw8fcg2kIePI1crERDM818W+XYrxqdBBOdlf2rm137bU+BltCA=="
|
||||
},
|
||||
"node_modules/esbuild": {
|
||||
"version": "0.17.18",
|
||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.17.18.tgz",
|
||||
"integrity": "sha512-z1lix43jBs6UKjcZVKOw2xx69ffE2aG0PygLL5qJ9OS/gy0Ewd1gW/PUQIOIQGXBHWNywSc0floSKoMFF8aK2w==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"bin": {
|
||||
"esbuild": "bin/esbuild"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@esbuild/android-arm": "0.17.18",
|
||||
"@esbuild/android-arm64": "0.17.18",
|
||||
"@esbuild/android-x64": "0.17.18",
|
||||
"@esbuild/darwin-arm64": "0.17.18",
|
||||
"@esbuild/darwin-x64": "0.17.18",
|
||||
"@esbuild/freebsd-arm64": "0.17.18",
|
||||
"@esbuild/freebsd-x64": "0.17.18",
|
||||
"@esbuild/linux-arm": "0.17.18",
|
||||
"@esbuild/linux-arm64": "0.17.18",
|
||||
"@esbuild/linux-ia32": "0.17.18",
|
||||
"@esbuild/linux-loong64": "0.17.18",
|
||||
"@esbuild/linux-mips64el": "0.17.18",
|
||||
"@esbuild/linux-ppc64": "0.17.18",
|
||||
"@esbuild/linux-riscv64": "0.17.18",
|
||||
"@esbuild/linux-s390x": "0.17.18",
|
||||
"@esbuild/linux-x64": "0.17.18",
|
||||
"@esbuild/netbsd-x64": "0.17.18",
|
||||
"@esbuild/openbsd-x64": "0.17.18",
|
||||
"@esbuild/sunos-x64": "0.17.18",
|
||||
"@esbuild/win32-arm64": "0.17.18",
|
||||
"@esbuild/win32-ia32": "0.17.18",
|
||||
"@esbuild/win32-x64": "0.17.18"
|
||||
}
|
||||
},
|
||||
"node_modules/fsevents": {
|
||||
"version": "2.3.2",
|
||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
|
||||
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/hammerjs": {
|
||||
"version": "2.0.8",
|
||||
"resolved": "https://registry.npmjs.org/hammerjs/-/hammerjs-2.0.8.tgz",
|
||||
"integrity": "sha512-tSQXBXS/MWQOn/RKckawJ61vvsDpCom87JgxiYdGwHdOa0ht0vzUWDlfioofFCRU0L+6NGDt6XzbgoJvZkMeRQ==",
|
||||
"engines": {
|
||||
"node": ">=0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/nanoid": {
|
||||
"version": "3.3.6",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz",
|
||||
"integrity": "sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ai"
|
||||
}
|
||||
],
|
||||
"bin": {
|
||||
"nanoid": "bin/nanoid.cjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/picocolors": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz",
|
||||
"integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/postcss": {
|
||||
"version": "8.4.23",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.23.tgz",
|
||||
"integrity": "sha512-bQ3qMcpF6A/YjR55xtoTr0jGOlnPOKAIMdOWiv0EIT6HVPEaJiJB4NLljSbiHoC2RX7DN5Uvjtpbg1NPdwv1oA==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/postcss/"
|
||||
},
|
||||
{
|
||||
"type": "tidelift",
|
||||
"url": "https://tidelift.com/funding/github/npm/postcss"
|
||||
},
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ai"
|
||||
}
|
||||
],
|
||||
"dependencies": {
|
||||
"nanoid": "^3.3.6",
|
||||
"picocolors": "^1.0.0",
|
||||
"source-map-js": "^1.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^10 || ^12 || >=14"
|
||||
}
|
||||
},
|
||||
"node_modules/rollup": {
|
||||
"version": "3.21.3",
|
||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-3.21.3.tgz",
|
||||
"integrity": "sha512-VnPfEG51nIv2xPLnZaekkuN06q9ZbnyDcLkaBdJa/W7UddyhOfMP2yOPziYQfeY7k++fZM8FdQIummFN5y14kA==",
|
||||
"dev": true,
|
||||
"bin": {
|
||||
"rollup": "dist/bin/rollup"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.18.0",
|
||||
"npm": ">=8.0.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"fsevents": "~2.3.2"
|
||||
}
|
||||
},
|
||||
"node_modules/source-map-js": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz",
|
||||
"integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/typescript": {
|
||||
"version": "5.0.4",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.0.4.tgz",
|
||||
"integrity": "sha512-cW9T5W9xY37cc+jfEnaUvX91foxtHkza3Nw3wkoF4sSlKn0MONdkdEndig/qPBWXNkmplh3NzayQzCiHM4/hqw==",
|
||||
"dev": true,
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.20"
|
||||
}
|
||||
},
|
||||
"node_modules/vite": {
|
||||
"version": "4.3.4",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-4.3.4.tgz",
|
||||
"integrity": "sha512-f90aqGBoxSFxWph2b39ae2uHAxm5jFBBdnfueNxZAT1FTpM13ccFQExCaKbR2xFW5atowjleRniQ7onjJ22QEg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"esbuild": "^0.17.5",
|
||||
"postcss": "^8.4.23",
|
||||
"rollup": "^3.21.0"
|
||||
},
|
||||
"bin": {
|
||||
"vite": "bin/vite.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^14.18.0 || >=16.0.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"fsevents": "~2.3.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/node": ">= 14",
|
||||
"less": "*",
|
||||
"sass": "*",
|
||||
"stylus": "*",
|
||||
"sugarss": "*",
|
||||
"terser": "^5.4.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/node": {
|
||||
"optional": true
|
||||
},
|
||||
"less": {
|
||||
"optional": true
|
||||
},
|
||||
"sass": {
|
||||
"optional": true
|
||||
},
|
||||
"stylus": {
|
||||
"optional": true
|
||||
},
|
||||
"sugarss": {
|
||||
"optional": true
|
||||
},
|
||||
"terser": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
{
|
||||
"name": "input-test",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite --host",
|
||||
"build": "tsc && vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5.0.2",
|
||||
"vite": "^4.3.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"@types/hammerjs": "^2.0.41",
|
||||
"hammerjs": "^2.0.8"
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
After Width: | Height: | Size: 1.5 KiB |
|
@ -0,0 +1,36 @@
|
|||
export class FrequencyCounter {
|
||||
private wordFrequency: Map<string, number>;
|
||||
private maxFrequency: number;
|
||||
private weightedWords: Map<string, number>;
|
||||
|
||||
constructor(weightedWords: Map<string, number> = new Map()) {
|
||||
this.wordFrequency = new Map();
|
||||
this.maxFrequency = 0;
|
||||
this.weightedWords = weightedWords;
|
||||
}
|
||||
|
||||
public registerWord(word: string): void {
|
||||
const currentFrequency = this.wordFrequency.get(word) || 0;
|
||||
const newFrequency = currentFrequency + 1;
|
||||
this.wordFrequency.set(word, newFrequency);
|
||||
|
||||
if (newFrequency > this.maxFrequency) {
|
||||
this.maxFrequency = newFrequency;
|
||||
}
|
||||
}
|
||||
|
||||
public getWeight(word: string): number {
|
||||
let weight = 0;
|
||||
|
||||
if (this.maxFrequency > 0) {
|
||||
const frequency = this.wordFrequency.get(word) || 0;
|
||||
weight += (frequency / this.maxFrequency) * 2; // You can adjust the multiplier as needed
|
||||
}
|
||||
|
||||
if (this.weightedWords.has(word)) {
|
||||
weight += this.weightedWords.get(word)!;
|
||||
}
|
||||
|
||||
return weight;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,37 @@
|
|||
import { levenshtein } from './levenshtein';
|
||||
import { FrequencyCounter } from './frequencycounter';
|
||||
|
||||
export class AutoCorrect {
|
||||
private dictionary: string[];
|
||||
private frequencyCounter: FrequencyCounter;
|
||||
|
||||
constructor(dictionary: string[]) {
|
||||
this.dictionary = dictionary;
|
||||
this.frequencyCounter = new FrequencyCounter();
|
||||
}
|
||||
|
||||
public registerTypedWord(word: string): void {
|
||||
this.frequencyCounter.registerWord(word);
|
||||
}
|
||||
|
||||
public correct(word: string, tolerance: number = 2): string | null {
|
||||
let minDistance = Infinity;
|
||||
let bestMatch: string | null = null;
|
||||
|
||||
for (const entry of this.dictionary) {
|
||||
let distance = levenshtein(word, entry);
|
||||
|
||||
// Apply weight based on word frequency
|
||||
const weight = this.frequencyCounter.getWeight(entry);
|
||||
distance -= weight;
|
||||
distance = Math.max(distance, 0); // Ensure distance doesn't become negative
|
||||
|
||||
if (distance < minDistance) {
|
||||
minDistance = distance;
|
||||
bestMatch = entry;
|
||||
}
|
||||
}
|
||||
|
||||
return minDistance <= tolerance ? bestMatch : null;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
export function levenshtein(a: string, b: string): number {
|
||||
const matrix: number[][] = [];
|
||||
|
||||
for (let i = 0; i <= a.length; i++) {
|
||||
matrix[i] = [];
|
||||
matrix[i][0] = i;
|
||||
}
|
||||
|
||||
for (let j = 0; j <= b.length; j++) {
|
||||
matrix[0][j] = j;
|
||||
}
|
||||
|
||||
for (let i = 1; i <= a.length; i++) {
|
||||
for (let j = 1; j <= b.length; j++) {
|
||||
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
|
||||
matrix[i][j] = Math.min(
|
||||
matrix[i - 1][j] + 1,
|
||||
matrix[i][j - 1] + 1,
|
||||
matrix[i - 1][j - 1] + cost
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return matrix[a.length][b.length];
|
||||
}
|
|
@ -0,0 +1,272 @@
|
|||
import { Manager, Swipe, Tap } from 'hammerjs';
|
||||
import { MessagingSystem } from '../eventing/messaging-system';
|
||||
import { ItemFocusEvent } from '../eventing/events';
|
||||
|
||||
export class DOMNavigator extends MessagingSystem {
|
||||
private currentIndex: number;
|
||||
private currentElement: HTMLElement;
|
||||
|
||||
constructor(private rootElement: HTMLElement) {
|
||||
super();
|
||||
this.currentIndex = 0;
|
||||
this.currentElement = rootElement;
|
||||
|
||||
this.setupGestures();
|
||||
}
|
||||
|
||||
private setupGestures(): void {
|
||||
const manager = new Manager(this.rootElement);
|
||||
|
||||
const swipe = new Swipe();
|
||||
const tap = new Tap({ event: 'doubletap', taps: 2 });
|
||||
|
||||
manager.add(swipe);
|
||||
manager.add(tap);
|
||||
|
||||
manager.on('swipe', (event) => {
|
||||
switch (event.direction) {
|
||||
case 2: // Left
|
||||
this.navigateSiblings(-1);
|
||||
break;
|
||||
case 4: // Right
|
||||
this.navigateSiblings(1);
|
||||
break;
|
||||
case 8: // Up
|
||||
this.navigateParent();
|
||||
break;
|
||||
case 16: // Down
|
||||
this.navigateChildren();
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
manager.on('doubletap', () => {
|
||||
this.currentElement.click();
|
||||
});
|
||||
|
||||
this.rootElement.addEventListener("keydown", e => {
|
||||
switch (e.key) {
|
||||
case "ArrowRight":
|
||||
this.navigateSiblings(1);
|
||||
break;
|
||||
case "ArrowLeft":
|
||||
this.navigateSiblings(-1);
|
||||
break;
|
||||
case "ArrowUp":
|
||||
this.navigateParent();
|
||||
break;
|
||||
case "ArrowDown":
|
||||
this.navigateChildren();
|
||||
break;
|
||||
case "Enter":
|
||||
this.currentElement.click();
|
||||
break;
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
private navigateSiblings(direction: number): void {
|
||||
const parent = this.currentElement.parentElement;
|
||||
|
||||
if (!parent) return;
|
||||
|
||||
const siblings = Array.from(parent.children);
|
||||
const newIndex = this.currentIndex + direction;
|
||||
|
||||
if (newIndex >= 0 && newIndex < siblings.length) {
|
||||
this.currentIndex = newIndex;
|
||||
this.currentElement = siblings[newIndex] as HTMLElement;
|
||||
}
|
||||
this.handleFocus();
|
||||
}
|
||||
|
||||
private navigateParent2(): void {
|
||||
const parent = this.currentElement.parentElement;
|
||||
|
||||
if (!parent || parent === this.rootElement) return;
|
||||
|
||||
this.currentElement = parent;
|
||||
this.currentIndex = Array.from(parent.parentElement.children).indexOf(parent);
|
||||
this.handleFocus();
|
||||
}
|
||||
|
||||
private navigateChildren2(): void {
|
||||
if (!this.currentElement.children.length) return;
|
||||
|
||||
this.currentElement = this.currentElement.children[0] as HTMLElement;
|
||||
this.currentIndex = 0;
|
||||
this.handleFocus();
|
||||
}
|
||||
|
||||
private handleFocus() {
|
||||
let element = this.currentElement;
|
||||
if (this.isSingleWrapper(element)) {
|
||||
element = this.currentElement.children[0] as HTMLElement;
|
||||
}
|
||||
element.focus();
|
||||
this.sendMessage<ItemFocusEvent>({
|
||||
type: "ItemFocusEvent",
|
||||
data: {
|
||||
node: element,
|
||||
friendlyName: `${this.getFriendlyName(element)} ${this.getFriendlyTypeName(element)}`
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private isContainer(element: HTMLElement): boolean {
|
||||
return ['div', 'label'].includes(element.tagName.toLowerCase());
|
||||
}
|
||||
|
||||
private isSingleDivWrapper(element: HTMLElement): boolean {
|
||||
return this.isContainer(element) && element.children.length === 1 && this.isContainer(element.children[0] as HTMLElement);
|
||||
}
|
||||
|
||||
private isSingleWrapper(element: HTMLElement): boolean {
|
||||
return this.isContainer(element) && element.children.length === 1 && !this.isContainer(element.children[0] as HTMLElement);
|
||||
}
|
||||
|
||||
private navigateParent(): void {
|
||||
let currentParent = this.currentElement.parentElement;
|
||||
|
||||
while (currentParent && currentParent !== this.rootElement) {
|
||||
if (!this.isSingleDivWrapper(currentParent)) {
|
||||
this.currentElement = currentParent;
|
||||
this.currentIndex = Array.from(currentParent.parentElement.children).indexOf(currentParent);
|
||||
break;
|
||||
}
|
||||
currentParent = currentParent.parentElement;
|
||||
}
|
||||
this.handleFocus();
|
||||
}
|
||||
|
||||
private navigateChildren(): void {
|
||||
const children = Array.from(this.currentElement.children);
|
||||
let targetChild = children[0] as HTMLElement;
|
||||
|
||||
while (targetChild && this.isSingleDivWrapper(targetChild)) {
|
||||
targetChild = targetChild.children[0] as HTMLElement;
|
||||
}
|
||||
|
||||
if (targetChild) {
|
||||
this.currentElement = targetChild;
|
||||
this.currentIndex = children.indexOf(targetChild);
|
||||
this.handleFocus();
|
||||
}
|
||||
}
|
||||
|
||||
public hasSiblings(): boolean {
|
||||
const parent = this.currentElement.parentElement;
|
||||
|
||||
if (!parent) return false;
|
||||
|
||||
const siblings = Array.from(parent.children);
|
||||
return siblings.length > 1 && (this.isContainer(this.currentElement));
|
||||
}
|
||||
|
||||
private getFriendlyName(element: HTMLElement) {
|
||||
if (!element) return '';
|
||||
|
||||
// Check if the element is a child of a label
|
||||
let parentLabel: HTMLLabelElement | null = null;
|
||||
let currentParent = element.parentElement;
|
||||
while (currentParent) {
|
||||
if (currentParent.tagName.toLowerCase() === 'label') {
|
||||
parentLabel = currentParent as HTMLLabelElement;
|
||||
break;
|
||||
}
|
||||
currentParent = currentParent.parentElement;
|
||||
}
|
||||
|
||||
// Check if the element has a label with the 'for' or 'aria-labelledby' attribute
|
||||
const labelledById = element.getAttribute('aria-labelledby');
|
||||
const labelledBy = labelledById ? document.getElementById(labelledById) : null;
|
||||
|
||||
if (element.tagName.toLowerCase() === 'div') {
|
||||
const ariaLabel = element.getAttribute('aria-label');
|
||||
const legend = element.querySelector('legend');
|
||||
const groupName = ariaLabel || (legend ? legend.textContent : '');
|
||||
|
||||
if (groupName) {
|
||||
return groupName;
|
||||
}
|
||||
}
|
||||
|
||||
if (parentLabel) {
|
||||
return parentLabel.textContent?.trim() || '';
|
||||
}
|
||||
|
||||
if (labelledBy) {
|
||||
return labelledBy.textContent?.trim() || '';
|
||||
}
|
||||
|
||||
// Check for aria-label attribute
|
||||
const ariaLabel = element.getAttribute("aria-label");
|
||||
if (ariaLabel) {
|
||||
return ariaLabel;
|
||||
}
|
||||
|
||||
// Check for title attribute
|
||||
const title = element.getAttribute("title");
|
||||
if (title) {
|
||||
return title;
|
||||
}
|
||||
|
||||
// Check if the element is a fieldset with a legend child
|
||||
if (element.tagName.toLowerCase() === "fieldset") {
|
||||
const legend = element.querySelector("legend");
|
||||
if (legend) {
|
||||
return legend.textContent || null;
|
||||
}
|
||||
}
|
||||
|
||||
// Return the text content of the element, but only if it's not a container (e.g., div)
|
||||
if (element.tagName.toLowerCase() !== "div") {
|
||||
return element.textContent || null;
|
||||
}
|
||||
|
||||
// If none of the above conditions are met, return null
|
||||
return "Group";
|
||||
}
|
||||
|
||||
private getFriendlyTypeName(element: HTMLElement) {
|
||||
const tagName = element.tagName.toLowerCase();
|
||||
const inputType = (element as HTMLInputElement).type?.toLowerCase();
|
||||
|
||||
switch (tagName) {
|
||||
case 'button':
|
||||
return 'Button';
|
||||
|
||||
case 'input':
|
||||
if (inputType === 'checkbox') {
|
||||
const checked = (element as HTMLInputElement).checked;
|
||||
return `Checkbox (${checked ? 'Checked' : 'Unchecked'})`;
|
||||
}
|
||||
if (inputType === 'radio') {
|
||||
const checked = (element as HTMLInputElement).checked;
|
||||
return `Radio Button (${checked ? 'Selected' : 'Unselected'})`;
|
||||
}
|
||||
return 'Input';
|
||||
|
||||
case 'select':
|
||||
return 'Select';
|
||||
|
||||
case 'textarea':
|
||||
return 'Text Area';
|
||||
|
||||
case 'li':
|
||||
return 'List item';
|
||||
|
||||
case 'ul':
|
||||
return 'List';
|
||||
|
||||
case 'span':
|
||||
case "p":
|
||||
return '';
|
||||
|
||||
default:
|
||||
return tagName.charAt(0).toUpperCase() + tagName.slice(1);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
export type ItemFocusEvent = {
|
||||
node: HTMLElement;
|
||||
friendlyName: string;
|
||||
}
|
|
@ -0,0 +1,63 @@
|
|||
// Define a generic type for messages
|
||||
export type Message<T> = {
|
||||
type: string,
|
||||
data?: T,
|
||||
};
|
||||
|
||||
// Define a generic type for message handlers
|
||||
export type MessageHandler<T> = (message: Message<T>) => void;
|
||||
|
||||
// Define a messaging system class
|
||||
export class MessagingSystem {
|
||||
private handlers: Record<string, MessageHandler<any>[]> = {};
|
||||
|
||||
public registerHandler<T>(type: string, handler: MessageHandler<T>): void {
|
||||
if (!this.handlers[type]) {
|
||||
this.handlers[type] = [];
|
||||
}
|
||||
if (!this.handlers[type].includes(handler)) {
|
||||
this.handlers[type].push(handler);
|
||||
}
|
||||
}
|
||||
|
||||
public unregisterHandler<T>(type: string, handler: MessageHandler<T>): void {
|
||||
if (this.handlers[type]) {
|
||||
this.handlers[type] = this.handlers[type].filter(h => h !== handler);
|
||||
}
|
||||
}
|
||||
|
||||
public registerHandlerOnce<T>(type: string, handler: MessageHandler<T>): void {
|
||||
const wrappedHandler = (message: Message<T>) => {
|
||||
handler(message);
|
||||
this.unregisterHandler(type, wrappedHandler);
|
||||
};
|
||||
this.registerHandler(type, wrappedHandler);
|
||||
}
|
||||
|
||||
public waitForMessage<T>(type: string, timeout?: number): Promise<T> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const handler = (message: Message<T>) => {
|
||||
if (timer) clearTimeout(timer);
|
||||
resolve(message.data);
|
||||
this.unregisterHandler(type, handler);
|
||||
};
|
||||
|
||||
this.registerHandler(type, handler);
|
||||
|
||||
let timer: ReturnType<typeof setTimeout> | undefined;
|
||||
if (timeout) {
|
||||
timer = setTimeout(() => {
|
||||
this.unregisterHandler(type, handler);
|
||||
reject(new Error(`Timeout waiting for message of type '${type}'`));
|
||||
}, timeout);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public sendMessage<T>(message: Message<T>): void {
|
||||
const handlers = this.handlers[message.type];
|
||||
if (handlers) {
|
||||
handlers.forEach(handler => handler(message));
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,224 @@
|
|||
export interface CustomKeyboardOptions {
|
||||
leftKeys: string[][];
|
||||
rightKeys: string[][];
|
||||
}
|
||||
|
||||
export class CustomKeyboard {
|
||||
private container: HTMLElement;
|
||||
private options: CustomKeyboardOptions;
|
||||
private keys: Map<string, HTMLElement> = new Map();
|
||||
private pressedKeys: Map<number, { element: HTMLElement; key: string }> = new Map();
|
||||
|
||||
constructor(container: HTMLElement, options: CustomKeyboardOptions) {
|
||||
this.container = container;
|
||||
this.options = options;
|
||||
this.createKeyboard();
|
||||
this.attachEvents();
|
||||
}
|
||||
|
||||
private createKeyboard(): void {
|
||||
const leftSection = document.createElement('div');
|
||||
leftSection.classList.add('keyboard-section', 'left');
|
||||
this.createKeys(leftSection, this.options.leftKeys);
|
||||
|
||||
const rightSection = document.createElement('div');
|
||||
rightSection.classList.add('keyboard-section', 'right');
|
||||
this.createKeys(rightSection, this.options.rightKeys);
|
||||
|
||||
this.container.appendChild(leftSection);
|
||||
this.container.appendChild(rightSection);
|
||||
}
|
||||
|
||||
private createKeys(section: HTMLElement, keysLayout: string[][]): void {
|
||||
keysLayout.forEach((rowKeys) => {
|
||||
const row = document.createElement('div');
|
||||
row.classList.add('keyboard-row');
|
||||
rowKeys.forEach((key) => {
|
||||
const keyElement = document.createElement('button');
|
||||
keyElement.textContent = key;
|
||||
keyElement.classList.add('keyboard-key');
|
||||
keyElement.dataset.key = key;
|
||||
row.appendChild(keyElement);
|
||||
this.keys.set(key, keyElement);
|
||||
});
|
||||
section.appendChild(row);
|
||||
});
|
||||
}
|
||||
|
||||
private attachEvents(): void {
|
||||
// this.container.addEventListener('touchstart', this.handleTouchStart.bind(this), { passive: false });
|
||||
// this.container.addEventListener('touchend', this.handleTouchEnd.bind(this), { passive: false });
|
||||
// this.container.addEventListener('touchmove', this.handleTouchMove.bind(this), { passive: false });
|
||||
this.container.addEventListener('pointerdown', this.handlePointerDown.bind(this), { passive: false });
|
||||
this.container.addEventListener('pointerup', this.handlePointerUp.bind(this), { passive: false });
|
||||
this.container.addEventListener('pointermove', this.handlePointerMove.bind(this), { passive: false });
|
||||
this.container.addEventListener('contextmenu', this.handleContextMenu.bind(this), { passive: false });
|
||||
// this.container.addEventListener('click', this.handleClick.bind(this), { passive: false });
|
||||
}
|
||||
|
||||
private handleTouchStart(event: TouchEvent): void {
|
||||
event.preventDefault();
|
||||
const target = event.target as HTMLElement;
|
||||
const key = target.dataset.key;
|
||||
if (key) {
|
||||
const touch = event.changedTouches[0];
|
||||
this.pressedKeys.set(touch.identifier, { element: target, key });
|
||||
this.keys.get(key)?.classList.add('pressed');
|
||||
this.emit('keyDown', key);
|
||||
}
|
||||
}
|
||||
|
||||
private handleTouchEnd(event: TouchEvent): void {
|
||||
event.preventDefault();
|
||||
const touch = event.changedTouches[0];
|
||||
const target = document.elementFromPoint(touch.clientX, touch.clientY) as HTMLElement;
|
||||
const key = target.dataset.key;
|
||||
const pressedKeyData = this.pressedKeys.get(touch.identifier);
|
||||
|
||||
if (pressedKeyData) {
|
||||
// Remove the 'pressed' class from the original key pressed
|
||||
pressedKeyData.element.classList.remove('pressed');
|
||||
this.pressedKeys.delete(touch.identifier);
|
||||
|
||||
// Emit the 'keyUp' event for the initially pressed key
|
||||
this.emit('keyUp', pressedKeyData.key);
|
||||
|
||||
// If the finger is released on a different key, emit the 'keyUp' event for that key
|
||||
if (key && pressedKeyData.key !== key) {
|
||||
this.emit('keyUp', key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private handleTouchMove(event: TouchEvent): void {
|
||||
event.preventDefault();
|
||||
const touch = event.changedTouches[0];
|
||||
const target = document.elementFromPoint(touch.clientX, touch.clientY) as HTMLElement;
|
||||
const key = target.dataset.key;
|
||||
|
||||
const pressedKeyData = this.pressedKeys.get(touch.identifier);
|
||||
|
||||
if (pressedKeyData) {
|
||||
// If the finger slides over a different key, emit the 'keyDown' event for that key
|
||||
// and the 'keyUp' event for the previous key
|
||||
if (key && pressedKeyData.key !== key) {
|
||||
// Remove the 'pressed' class from the previous key and update the pressedKeys Map
|
||||
pressedKeyData.element.classList.remove('pressed');
|
||||
this.pressedKeys.set(touch.identifier, { element: target, key });
|
||||
|
||||
// Add the 'pressed' class to the new key
|
||||
this.keys.get(key)?.classList.add('pressed');
|
||||
|
||||
// Emit the 'keyDown' event for the new key
|
||||
this.emit('keyDown', key);
|
||||
|
||||
// Emit the 'keyUp' event for the previous key
|
||||
// this.emit('keyUp', pressedKeyData.key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private handleClick(event: MouseEvent) {
|
||||
|
||||
event.preventDefault();
|
||||
const target = event.target as HTMLElement;
|
||||
const key = target.dataset.key;
|
||||
|
||||
if (key) {
|
||||
this.keys.get(key)?.classList.add('pressed');
|
||||
|
||||
// Emit the 'keyUp' event for the clicked key
|
||||
this.emit('keyUp', key);
|
||||
|
||||
// Remove the 'pressed' class after a short delay
|
||||
setTimeout(() => {
|
||||
this.keys.get(key)?.classList.remove('pressed');
|
||||
}, 100);
|
||||
}
|
||||
}
|
||||
|
||||
private handlePointerDown(event: PointerEvent): void {
|
||||
event.preventDefault();
|
||||
const target = event.target as HTMLElement;
|
||||
const key = target.dataset.key;
|
||||
if (key) {
|
||||
this.pressedKeys.set(event.pointerId, { element: target, key });
|
||||
this.keys.get(key)?.classList.add('pressed');
|
||||
this.emit('keyDown', key);
|
||||
}
|
||||
}
|
||||
|
||||
private handlePointerUp(event: PointerEvent): void {
|
||||
event.preventDefault();
|
||||
const target = document.elementFromPoint(event.clientX, event.clientY) as HTMLElement;
|
||||
const key = target.dataset.key;
|
||||
|
||||
const pressedKeyData = this.pressedKeys.get(event.pointerId);
|
||||
|
||||
if (pressedKeyData) {
|
||||
// Remove the 'pressed' class from the original key pressed
|
||||
pressedKeyData.element.classList.remove('pressed');
|
||||
this.pressedKeys.delete(event.pointerId);
|
||||
|
||||
// Emit the 'keyUp' event for the initially pressed key
|
||||
this.emit('keyUp', pressedKeyData.key);
|
||||
|
||||
// If the finger is released on a different key, emit the 'keyUp' event for that key
|
||||
if (key && pressedKeyData.key !== key) {
|
||||
this.emit('keyUp', key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private handlePointerMove(event: PointerEvent): void {
|
||||
event.preventDefault();
|
||||
const target = document.elementFromPoint(event.clientX, event.clientY) as HTMLElement;
|
||||
const key = target.dataset.key;
|
||||
|
||||
const pressedKeyData = this.pressedKeys.get(event.pointerId);
|
||||
|
||||
if (pressedKeyData) {
|
||||
// If the finger slides over a different key, emit the 'keyDown' event for that key
|
||||
// and the 'keyUp' event for the previous key
|
||||
if (key && pressedKeyData.key !== key) {
|
||||
// Remove the 'pressed' class from the previous key and update the pressedKeys Map
|
||||
pressedKeyData.element.classList.remove('pressed');
|
||||
this.pressedKeys.set(event.pointerId, { element: target, key });
|
||||
|
||||
// Add the 'pressed' class to the new key
|
||||
this.keys.get(key)?.classList.add('pressed');
|
||||
|
||||
// Emit the 'keyDown' event for the new key
|
||||
this.emit('keyDown', key);
|
||||
|
||||
// Emit the 'keyUp' event for the previous key
|
||||
// this.emit('keyUp', pressedKeyData.key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private handleContextMenu(event: MouseEvent): void {
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
private emit(eventName: string, key: string): void {
|
||||
const event = new CustomEvent(eventName, { detail: { key } });
|
||||
this.container.dispatchEvent(event);
|
||||
}
|
||||
|
||||
on(eventName: any, callback: (event: CustomEvent) => void): void {
|
||||
this.container.addEventListener(eventName, callback);
|
||||
}
|
||||
|
||||
off(eventName: any, callback: (event: CustomEvent) => void): void {
|
||||
this.container.removeEventListener(eventName, callback);
|
||||
}
|
||||
|
||||
show(): void {
|
||||
this.container.classList.add('visible');
|
||||
}
|
||||
|
||||
hide(): void {
|
||||
this.container.classList.remove('visible');
|
||||
}
|
||||
}
|
|
@ -0,0 +1,119 @@
|
|||
import './styles.css'
|
||||
import { CustomKeyboard, CustomKeyboardOptions } from "./keyboard";
|
||||
import { AutoCorrect } from './autocorrect';
|
||||
import { WebTTSOutput } from "audiogame-tools/engine/tts/outputs/webtts";
|
||||
import { TTS } from "audiogame-tools/engine/tts";
|
||||
import Resonator from "audiogame-tools/engine/resonator";
|
||||
import { UIWindow, Button, Text, List, ListItem, Checkbox } from "./ui";
|
||||
import { DOMNavigator } from './dom-nav';
|
||||
import { ItemFocusEvent } from './eventing/events';
|
||||
import { RadioGroup } from './ui/radio-group';
|
||||
|
||||
|
||||
const tts = new TTS(new WebTTSOutput());
|
||||
const sound = new Resonator();
|
||||
const keyChar = sound.loadImmediate(`keyChar.wav`);
|
||||
const keyFocus = sound.loadImmediate(`keyDelete.wav`);
|
||||
const sfxCorrect = sound.loadImmediate(`macroSet.wav`);
|
||||
|
||||
|
||||
|
||||
let corrector: AutoCorrect;
|
||||
|
||||
let currentWord = '';
|
||||
|
||||
setupAutoCorrect();
|
||||
|
||||
|
||||
const keyboardContainer = document.getElementById('custom-keyboard') as HTMLElement;
|
||||
const keyboardLayout: CustomKeyboardOptions = {
|
||||
leftKeys: [
|
||||
['q', 'w', 'e', 'r', 't'],
|
||||
['a', 's', 'd', 'f', 'g'],
|
||||
['z', 'x', 'c', 'v'],
|
||||
['⇧', '⌫', ' '],
|
||||
],
|
||||
rightKeys: [
|
||||
['y', 'u', 'i', 'o', 'p'],
|
||||
['h', 'j', 'k', 'l', ';'],
|
||||
['b', 'n', 'm', ', ', '.'],
|
||||
[' ', '↩', '⇩'],
|
||||
],
|
||||
};
|
||||
|
||||
const customKeyboard = new CustomKeyboard(keyboardContainer, keyboardLayout);
|
||||
customKeyboard.on('keyDown', (event: CustomEvent) => {
|
||||
keyFocus.play();
|
||||
speak(event.detail.key);
|
||||
});
|
||||
customKeyboard.on('keyUp', (event: CustomEvent) => {
|
||||
keyChar.play();
|
||||
// speak(event.detail.key);
|
||||
speak(currentWord);
|
||||
if (event.detail.key !== " ") {
|
||||
currentWord += event.detail.key;
|
||||
} else {
|
||||
if (!corrector) return;
|
||||
const corrected = corrector.correct(currentWord, 4);
|
||||
if (corrected) {
|
||||
speak(corrected);
|
||||
sfxCorrect.play();
|
||||
}
|
||||
currentWord = '';
|
||||
}
|
||||
});
|
||||
|
||||
customKeyboard.show();
|
||||
|
||||
|
||||
async function setupAutoCorrect() {
|
||||
const res = await fetch(`dictionary.json`);
|
||||
const dictionary = await res.json();
|
||||
corrector = new AutoCorrect(dictionary);
|
||||
sfxCorrect.play();
|
||||
}
|
||||
|
||||
function speak(text: string) {
|
||||
tts.stop();
|
||||
tts.speak(text);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
const win = new UIWindow("Test window");
|
||||
const btn = new Button("Here is a button");
|
||||
btn.onClick(() => speak(`Button clicked`));
|
||||
win.add(btn);
|
||||
win.add(new Button("Here is another button"));
|
||||
win.add(new Text("Here is some text"));
|
||||
win.add(new Text("And some more text"));
|
||||
const list = new List("A list view");
|
||||
list.add(new ListItem("Entry one"));
|
||||
list.add(new ListItem("Entry two"));
|
||||
win.add(list);
|
||||
const radio = new RadioGroup("Select one", [{
|
||||
key: "meow", value: "Meow"
|
||||
},
|
||||
{ key: "woof", value: "Woof" },
|
||||
{ key: "rawr", value: "Rawr" }]);
|
||||
win.add(radio);
|
||||
const checkbox = new Checkbox("Toggle me");
|
||||
win.add(checkbox);
|
||||
document.getElementById("app")?.appendChild(win.show() as any);
|
||||
|
||||
const nav = new DOMNavigator(document.body);
|
||||
|
||||
nav.registerHandler<ItemFocusEvent>("ItemFocusEvent", (ev) => {
|
||||
keyFocus.play();
|
||||
speak(ev.data?.friendlyName!);
|
||||
if (nav.hasSiblings()) {
|
||||
sfxCorrect.play();
|
||||
}
|
||||
})
|
||||
*/
|
||||
|
||||
const testTTS = document.createElement("button");
|
||||
testTTS.addEventListener("click", e => {
|
||||
speak(`Meow`);
|
||||
});
|
||||
document.getElementById("app")?.appendChild(testTTS);
|
|
@ -0,0 +1,79 @@
|
|||
html,
|
||||
body {
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.app {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
#custom-keyboard {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
background-color: #eee;
|
||||
padding: 10px;
|
||||
box-shadow: 0 -2px 5px rgba(0, 0, 0, 0.1);
|
||||
transition: transform 0.3s ease;
|
||||
transform: translateY(100%);
|
||||
touch-action: none;
|
||||
user-select: none;
|
||||
-webkit-touch-callout: none;
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
}
|
||||
|
||||
#custom-keyboard.visible {
|
||||
transform: translateY(0);
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.keyboard-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.keyboard-row {
|
||||
display: flex;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
/* Apply an offset to every second row */
|
||||
.keyboard-row:nth-child(even) {
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
||||
/* Apply an offset to every third row (if needed) */
|
||||
.keyboard-row:nth-child(3n) {
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
.keyboard-key {
|
||||
border: 1px solid #ccc;
|
||||
padding: 10px;
|
||||
margin: 0px;
|
||||
background-color: #fff;
|
||||
border-radius: 3px;
|
||||
touch-action: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.keyboard-key.pressed {
|
||||
background-color: #ddd;
|
||||
}
|
||||
|
||||
/* Media query for landscape orientation */
|
||||
@media (orientation: landscape) {
|
||||
.keyboard-key {
|
||||
padding: 10px;
|
||||
/* Increase the padding to make the keys larger */
|
||||
}
|
||||
}
|
|
@ -0,0 +1,35 @@
|
|||
import { UINode } from "./node";
|
||||
|
||||
export class Button extends UINode {
|
||||
private buttonElement: HTMLButtonElement;
|
||||
public constructor(title: string, hasPopup: boolean = false) {
|
||||
super(title);
|
||||
this.buttonElement = document.createElement("button");
|
||||
this.buttonElement.innerText = title;
|
||||
if (hasPopup) this.buttonElement.setAttribute("aria-haspopup", "true");
|
||||
this.element.appendChild(this.buttonElement);
|
||||
this.element.setAttribute("aria-label", this.title);
|
||||
}
|
||||
|
||||
public focus() {
|
||||
this.buttonElement.focus();
|
||||
}
|
||||
|
||||
public click() {
|
||||
this.buttonElement.click();
|
||||
}
|
||||
|
||||
public getElement(): HTMLElement {
|
||||
return this.buttonElement;
|
||||
}
|
||||
|
||||
public setText(text: string) {
|
||||
this.title = text;
|
||||
this.buttonElement.innerText = text;
|
||||
this.element.setAttribute("aria-label", this.title);
|
||||
}
|
||||
|
||||
public setDisabled(val: boolean) {
|
||||
this.buttonElement.disabled = val;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
import { UINode } from "./node";
|
||||
|
||||
export class Canvas extends UINode {
|
||||
private canvasElement: HTMLCanvasElement;
|
||||
public constructor(title: string) {
|
||||
super(title);
|
||||
this.canvasElement = document.createElement("canvas");
|
||||
|
||||
this.canvasElement.setAttribute("tabindex", "-1");
|
||||
this.element.appendChild(this.canvasElement);
|
||||
}
|
||||
|
||||
public focus() {
|
||||
this.canvasElement.focus();
|
||||
}
|
||||
|
||||
public click() {
|
||||
this.canvasElement.click();
|
||||
}
|
||||
|
||||
public getElement(): HTMLElement {
|
||||
return this.canvasElement;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,46 @@
|
|||
import { UINode } from "./node";
|
||||
|
||||
export class Checkbox extends UINode {
|
||||
private id: string;
|
||||
private titleElement: HTMLLabelElement;
|
||||
private checkboxElement: HTMLInputElement;
|
||||
public constructor(title: string) {
|
||||
super(title);
|
||||
this.id = Math.random().toString();
|
||||
this.titleElement = document.createElement("label");
|
||||
this.titleElement.id = `chkbx_title_${this.id}`;
|
||||
this.checkboxElement = document.createElement("input");
|
||||
this.checkboxElement.id = `chkbx_${this.id}`;
|
||||
this.checkboxElement.type = "checkbox";
|
||||
this.titleElement.appendChild(this.checkboxElement);
|
||||
this.titleElement.appendChild(document.createTextNode(this.title));
|
||||
this.element.appendChild(this.titleElement);
|
||||
}
|
||||
|
||||
public focus() {
|
||||
this.checkboxElement.focus();
|
||||
}
|
||||
|
||||
public click() {
|
||||
this.checkboxElement.click();
|
||||
}
|
||||
|
||||
public getElement(): HTMLElement {
|
||||
return this.checkboxElement;
|
||||
}
|
||||
|
||||
public setText(text: string) {
|
||||
this.title = text;
|
||||
this.titleElement.innerText = text;
|
||||
this.element.setAttribute("aria-label", this.title);
|
||||
this.element.setAttribute("aria-roledescription", "checkbox");
|
||||
}
|
||||
|
||||
public isChecked(): boolean {
|
||||
return this.checkboxElement.checked;
|
||||
}
|
||||
|
||||
public setChecked(value: boolean) {
|
||||
this.checkboxElement.checked = value;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,40 @@
|
|||
import { Container } from "./container";
|
||||
|
||||
export class CollapsableContainer extends Container {
|
||||
private detailsElement: HTMLDetailsElement;
|
||||
private summaryElement: HTMLElement;
|
||||
private wrapperElement: HTMLDivElement;
|
||||
|
||||
public constructor(title: string) {
|
||||
super(title);
|
||||
this.wrapperElement = document.createElement("div");
|
||||
this.detailsElement = document.createElement("details");
|
||||
this.summaryElement = document.createElement("summary");
|
||||
|
||||
this.summaryElement.innerText = title;
|
||||
this.detailsElement.appendChild(this.summaryElement);
|
||||
this.detailsElement.appendChild(this.containerElement);
|
||||
this.wrapperElement.appendChild(this.detailsElement);
|
||||
}
|
||||
|
||||
public render() {
|
||||
return this.wrapperElement;
|
||||
}
|
||||
|
||||
public setTitle(text: string): void {
|
||||
this.title = text;
|
||||
this.summaryElement.innerText = text;
|
||||
}
|
||||
|
||||
public isCollapsed(): boolean {
|
||||
return this.detailsElement.hasAttribute("open");
|
||||
}
|
||||
|
||||
public expand(val: boolean) {
|
||||
if (val) {
|
||||
this.detailsElement.setAttribute("open", "true");
|
||||
} else {
|
||||
this.detailsElement.removeAttribute("open");
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,55 @@
|
|||
import { UINode } from "./node";
|
||||
|
||||
export class Container extends UINode {
|
||||
public children: UINode[];
|
||||
protected containerElement: HTMLDivElement;
|
||||
private focused: number = 0;
|
||||
|
||||
public constructor(title: string) {
|
||||
super(title);
|
||||
this.children = [];
|
||||
this.containerElement = document.createElement("div");
|
||||
this.containerElement.setAttribute("tabindex", "-1");
|
||||
this.focused = 0;
|
||||
}
|
||||
|
||||
public focus() {
|
||||
this.containerElement.focus();
|
||||
}
|
||||
|
||||
public _onFocus() {
|
||||
this.children[this.focused].focus();
|
||||
}
|
||||
|
||||
public add(node: UINode) {
|
||||
this.children.push(node);
|
||||
node._onConnect();
|
||||
this.containerElement.appendChild(node.render());
|
||||
}
|
||||
|
||||
public remove(node: UINode) {
|
||||
this.children.splice(this.children.indexOf(node), 1);
|
||||
node._onDisconnect();
|
||||
this.containerElement.removeChild(node.render());
|
||||
}
|
||||
|
||||
public render() {
|
||||
return this.containerElement;
|
||||
}
|
||||
|
||||
public getChildren(): UINode[] {
|
||||
return this.children;
|
||||
}
|
||||
|
||||
public _onKeydown(key: string, alt: boolean = false, shift: boolean = false, ctrl: boolean = false): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
public getElement() {
|
||||
return this.containerElement;
|
||||
}
|
||||
|
||||
public setAriaLabel(text: string): void {
|
||||
this.containerElement.setAttribute("aria-label", text);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,41 @@
|
|||
import { UINode } from "./node";
|
||||
|
||||
|
||||
export class DatePicker extends UINode {
|
||||
private id: string;
|
||||
private titleElement: HTMLLabelElement;
|
||||
private inputElement: HTMLInputElement;
|
||||
public constructor(title: string) {
|
||||
super(title);
|
||||
this.id = Math.random().toString();
|
||||
this.titleElement = document.createElement("label");
|
||||
this.titleElement.innerText = title;
|
||||
this.titleElement.id = `datepicker_title_${this.id}`;
|
||||
this.inputElement = document.createElement("input");
|
||||
this.inputElement.id = `datepicker_${this.id}`;
|
||||
this.inputElement.type = "date";
|
||||
this.titleElement.appendChild(this.inputElement);
|
||||
this.element.appendChild(this.titleElement);
|
||||
}
|
||||
|
||||
public focus() {
|
||||
this.inputElement.focus();
|
||||
}
|
||||
|
||||
public getElement(): HTMLElement {
|
||||
return this.inputElement;
|
||||
}
|
||||
|
||||
public setText(text: string) {
|
||||
this.title = text;
|
||||
this.titleElement.innerText = text;
|
||||
}
|
||||
|
||||
public getValue(): string {
|
||||
return this.inputElement.value;
|
||||
}
|
||||
|
||||
public setValue(value: string) {
|
||||
this.inputElement.value = value;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,69 @@
|
|||
import { Container } from "./container";
|
||||
import { UINode } from "./node";
|
||||
import { UIWindow } from "./window";
|
||||
import { Button } from "./button";
|
||||
|
||||
export class Dialog<T> extends UIWindow {
|
||||
private resolvePromise: (value: T | PromiseLike<T>) => void;
|
||||
private rejectPromise: (reason?: any) => void;
|
||||
private promise: Promise<T>;
|
||||
private okButton: Button;
|
||||
private cancelButton: Button;
|
||||
|
||||
private previouslyFocusedElement: HTMLElement;
|
||||
|
||||
public constructor(title: string) {
|
||||
super(title, "dialog", false);
|
||||
this.promise = new Promise<T>((resolve, reject) => {
|
||||
this.resolvePromise = resolve;
|
||||
this.rejectPromise = reject;
|
||||
});
|
||||
|
||||
// Automatically add OK and Cancel buttons
|
||||
this.okButton = new Button("OK");
|
||||
this.okButton.setPosition(70, 90, 10, 5);
|
||||
this.okButton.onClick(() => this.choose(undefined)); // Default OK action
|
||||
|
||||
this.cancelButton = new Button("Cancel");
|
||||
this.cancelButton.setPosition(20, 90, 10, 5);
|
||||
this.cancelButton.onClick(() => this.cancel()); // Default Cancel action
|
||||
}
|
||||
|
||||
public setOkAction(action: () => T): void {
|
||||
this.okButton.onClick(() => {
|
||||
const result = action();
|
||||
this.choose(result);
|
||||
});
|
||||
}
|
||||
|
||||
public setCancelAction(action: () => void): void {
|
||||
this.cancelButton.onClick(() => {
|
||||
action();
|
||||
this.cancel();
|
||||
});
|
||||
}
|
||||
|
||||
public choose(item: T) {
|
||||
this.resolvePromise(item);
|
||||
document.body.removeChild(this.getElement());
|
||||
this.hide();
|
||||
this.previouslyFocusedElement.focus();
|
||||
}
|
||||
|
||||
public cancel(reason?: any) {
|
||||
this.rejectPromise(reason);
|
||||
document.body.removeChild(this.getElement());
|
||||
this.hide();
|
||||
this.previouslyFocusedElement.focus();
|
||||
}
|
||||
|
||||
public open(): Promise<T> {
|
||||
this.previouslyFocusedElement = document.activeElement as HTMLElement;
|
||||
document.body.appendChild( this.show());
|
||||
this.add(this.okButton);
|
||||
this.add(this.cancelButton);
|
||||
this.container.focus();
|
||||
|
||||
return this.promise;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,46 @@
|
|||
import { UINode } from "./node";
|
||||
|
||||
export class Dropdown extends UINode {
|
||||
private id: string;
|
||||
private titleElement: HTMLLabelElement;
|
||||
private selectElement: HTMLSelectElement;
|
||||
public constructor(title: string, options: { key: string; value: string }[]) {
|
||||
super(title);
|
||||
this.id = Math.random().toString();
|
||||
this.titleElement = document.createElement("label");
|
||||
this.titleElement.innerText = title;
|
||||
this.titleElement.id = `dd_title_${this.id}`;
|
||||
this.selectElement = document.createElement("select");
|
||||
this.selectElement.id = `dd_${this.id}`;
|
||||
this.titleElement.appendChild(this.selectElement);
|
||||
this.element.appendChild(this.titleElement);
|
||||
|
||||
options.forEach((option) => {
|
||||
const optionElement = document.createElement("option");
|
||||
optionElement.value = option.key;
|
||||
optionElement.innerText = option.value;
|
||||
this.selectElement.appendChild(optionElement);
|
||||
});
|
||||
}
|
||||
|
||||
public focus() {
|
||||
this.selectElement.focus();
|
||||
}
|
||||
|
||||
public getElement(): HTMLElement {
|
||||
return this.selectElement;
|
||||
}
|
||||
|
||||
public setText(text: string) {
|
||||
this.title = text;
|
||||
this.titleElement.innerText = text;
|
||||
}
|
||||
|
||||
public getSelectedValue(): string {
|
||||
return this.selectElement.value;
|
||||
}
|
||||
|
||||
public setSelectedValue(value: string) {
|
||||
this.selectElement.value = value;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,44 @@
|
|||
import { UINode } from "./node";
|
||||
|
||||
export class FileInput extends UINode {
|
||||
private id: string;
|
||||
private titleElement: HTMLLabelElement;
|
||||
private inputElement: HTMLInputElement;
|
||||
public constructor(title: string, multiple: boolean = false) {
|
||||
super(title);
|
||||
this.id = Math.random().toString();
|
||||
this.titleElement = document.createElement("label");
|
||||
this.titleElement.innerText = title;
|
||||
this.titleElement.id = `fileinpt_title_${this.id}`;
|
||||
this.inputElement = document.createElement("input");
|
||||
this.inputElement.id = `fileinpt_${this.id}`;
|
||||
this.inputElement.type = "file";
|
||||
if (multiple) {
|
||||
this.inputElement.multiple = true;
|
||||
}
|
||||
this.titleElement.appendChild(this.inputElement);
|
||||
this.element.appendChild(this.titleElement);
|
||||
|
||||
}
|
||||
|
||||
public focus() {
|
||||
this.inputElement.focus();
|
||||
}
|
||||
|
||||
public getElement(): HTMLElement {
|
||||
return this.inputElement;
|
||||
}
|
||||
|
||||
public setText(text: string) {
|
||||
this.title = text;
|
||||
this.titleElement.innerText = text;
|
||||
}
|
||||
|
||||
public getFiles(): FileList | null {
|
||||
return this.inputElement.files;
|
||||
}
|
||||
|
||||
public setAccept(accept: string) {
|
||||
this.inputElement.accept = accept;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
import { UINode } from "./node";
|
||||
|
||||
export class Image extends UINode {
|
||||
private imgElement: HTMLImageElement;
|
||||
public constructor(title: string, src: string, altText: string = "") {
|
||||
super(title);
|
||||
this.imgElement = document.createElement("img");
|
||||
this.imgElement.src = src;
|
||||
this.imgElement.alt = altText;
|
||||
this.element.appendChild(this.imgElement);
|
||||
this.element.setAttribute("aria-label", title);
|
||||
}
|
||||
|
||||
public getElement(): HTMLElement {
|
||||
return this.imgElement;
|
||||
}
|
||||
|
||||
public setText(text: string) {
|
||||
this.title = text;
|
||||
this.element.setAttribute("aria-label", text);
|
||||
}
|
||||
|
||||
public setSource(src: string) {
|
||||
this.imgElement.src = src;
|
||||
}
|
||||
|
||||
public setAltText(altText: string) {
|
||||
this.imgElement.alt = altText;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
export { UIWindow } from "./window";
|
||||
export { Button } from "./button";
|
||||
export { Container } from "./container";
|
||||
export { UINode } from "./node";
|
||||
export { List } from "./list";
|
||||
export { Text } from "./text";
|
||||
export { ViewManager } from "./view-manager";
|
||||
export { ListItem } from "./list-item";
|
||||
export { Checkbox } from "./checkbox";
|
||||
export { TextInput } from "./text-input";
|
||||
export { TabBar } from "./tab-bar";
|
||||
export { TabbedView } from "./tabbed-view";
|
||||
export { Canvas } from "./canvas";
|
|
@ -0,0 +1,34 @@
|
|||
import { UINode } from "./node";
|
||||
|
||||
export class ListItem extends UINode {
|
||||
private listElement: HTMLLIElement;
|
||||
|
||||
public constructor(title: string) {
|
||||
super(title);
|
||||
this.listElement = document.createElement("li");
|
||||
this.listElement.innerText = this.title;
|
||||
this.listElement.setAttribute("tabindex", "-1");
|
||||
this.element.appendChild(this.listElement);
|
||||
this.listElement.setAttribute("aria-label", this.title);
|
||||
this.listElement.setAttribute("role", "option");
|
||||
}
|
||||
|
||||
public focus() {
|
||||
this.listElement.focus();
|
||||
}
|
||||
|
||||
public click() {
|
||||
this.listElement.click();
|
||||
}
|
||||
|
||||
public getElement(): HTMLElement {
|
||||
return this.listElement;
|
||||
}
|
||||
|
||||
public setText(text: string) {
|
||||
this.title = text;
|
||||
this.listElement.innerText = text;
|
||||
this.element.setAttribute("aria-label", this.title);
|
||||
this.listElement.setAttribute("aria-label", this.title);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,134 @@
|
|||
import { UINode } from "./node";
|
||||
|
||||
// a list of UINodes navigable with the arrow keys
|
||||
|
||||
export class List extends UINode {
|
||||
public children: UINode[];
|
||||
protected listElement: HTMLUListElement;
|
||||
private focused: number;
|
||||
protected selectCallback: (id: number) => void;
|
||||
public constructor(title: string) {
|
||||
super(title);
|
||||
this.children = [];
|
||||
this.listElement = document.createElement("ul");
|
||||
this.listElement.setAttribute("role", "listbox");
|
||||
this.listElement.style.listStyle = "none";
|
||||
this.element.appendChild(this.listElement);
|
||||
this.element.setAttribute("aria-label", this.title);
|
||||
this.focused = 0;
|
||||
}
|
||||
|
||||
public add(node: UINode) {
|
||||
this.children.push(node);
|
||||
node._onConnect();
|
||||
this.listElement.appendChild(node.render());
|
||||
if (this.children.length === 1) this.calculateTabIndex();
|
||||
node.onFocus(() => this.calculateFocused(node));
|
||||
}
|
||||
|
||||
public remove(node: UINode) {
|
||||
const idx = this.children.indexOf(node);
|
||||
this.children.splice(idx, 1);
|
||||
node._onDisconnect();
|
||||
this.listElement.removeChild(node.render());
|
||||
if (idx === this.focused) {
|
||||
if (this.focused > 0) this.focused--;
|
||||
this.calculateTabIndex();
|
||||
}
|
||||
}
|
||||
|
||||
public _onFocus() {
|
||||
super._onFocus();
|
||||
this.children[this.focused].focus();
|
||||
}
|
||||
|
||||
public _onClick() {
|
||||
this.children[this.focused]._onClick();
|
||||
}
|
||||
|
||||
public _onSelect(id: number) {
|
||||
if (this.selectCallback) this.selectCallback(id);
|
||||
}
|
||||
|
||||
protected calculateStyle(): void {
|
||||
super.calculateStyle();
|
||||
this.element.style.overflowY = "scroll";
|
||||
this.listElement.style.overflowY = "scroll";
|
||||
}
|
||||
|
||||
public _onKeydown(key: string, alt: boolean = false, shift: boolean = false, ctrl: boolean = false): boolean {
|
||||
switch (key) {
|
||||
case "ArrowUp":
|
||||
this.children[this.focused].setTabbable(false);
|
||||
this.focused = Math.max(0, this.focused - 1);
|
||||
this.children[this.focused].setTabbable(true);
|
||||
this.children[this.focused].focus();
|
||||
return true;
|
||||
break;
|
||||
case "ArrowDown":
|
||||
this.children[this.focused].setTabbable(false);
|
||||
this.focused = Math.min(this.children.length - 1, this.focused + 1);
|
||||
this.children[this.focused].setTabbable(true);
|
||||
this.children[this.focused].focus();
|
||||
return true;
|
||||
break;
|
||||
case "Enter":
|
||||
this.children[this.focused].click();
|
||||
return true;
|
||||
break;
|
||||
default:
|
||||
return this.children[this.focused]._onKeydown(key);
|
||||
break;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
protected renderAsListItem(node: UINode) {
|
||||
let li = document.createElement("li");
|
||||
li.appendChild(node.render());
|
||||
return li;
|
||||
}
|
||||
|
||||
public getElement(): HTMLElement {
|
||||
return this.listElement;
|
||||
}
|
||||
|
||||
public isItemFocused(): boolean {
|
||||
const has = this.children.find((child) => child.isFocused);
|
||||
if (has) {
|
||||
console.log("I have child", has);
|
||||
return true;
|
||||
}
|
||||
console.log("No children selected");
|
||||
return false;
|
||||
}
|
||||
|
||||
private calculateTabIndex() {
|
||||
if (this.children.length < 1) return;
|
||||
this.children[this.focused].setTabbable(true);
|
||||
}
|
||||
|
||||
public clear() {
|
||||
this.children.forEach((child) => this.remove(child));
|
||||
this.children = [];
|
||||
this.listElement.innerHTML = '';
|
||||
this.focused = 0;
|
||||
}
|
||||
|
||||
public getFocusedChild() {
|
||||
return this.children[this.focused];
|
||||
}
|
||||
|
||||
public getFocus() {
|
||||
return this.focused;
|
||||
}
|
||||
|
||||
public onSelect(f: (id: number) => void) {
|
||||
this.selectCallback = f;
|
||||
}
|
||||
|
||||
protected calculateFocused(node: UINode) {
|
||||
const idx = this.children.indexOf(node);
|
||||
this._onSelect(idx);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,43 @@
|
|||
import { UINode } from "./node";
|
||||
|
||||
export class MultilineInput extends UINode {
|
||||
private id: string;
|
||||
private titleElement: HTMLLabelElement;
|
||||
private textareaElement: HTMLTextAreaElement;
|
||||
public constructor(title: string) {
|
||||
super(title);
|
||||
this.id = Math.random().toString();
|
||||
this.titleElement = document.createElement("label");
|
||||
this.titleElement.innerText = title;
|
||||
this.titleElement.id = `txtarea_title_${this.id}`;
|
||||
this.textareaElement = document.createElement("textarea");
|
||||
this.textareaElement.id = `txtarea_${this.id}`;
|
||||
this.titleElement.appendChild(this.textareaElement);
|
||||
this.element.appendChild(this.titleElement);
|
||||
}
|
||||
|
||||
public focus() {
|
||||
this.textareaElement.focus();
|
||||
}
|
||||
|
||||
public click() {
|
||||
this.textareaElement.click();
|
||||
}
|
||||
|
||||
public getElement(): HTMLElement {
|
||||
return this.textareaElement;
|
||||
}
|
||||
|
||||
public setText(text: string) {
|
||||
this.title = text;
|
||||
this.titleElement.innerText = text;
|
||||
}
|
||||
|
||||
public getValue(): string {
|
||||
return this.textareaElement.value;
|
||||
}
|
||||
|
||||
public setValue(value: string) {
|
||||
this.textareaElement.value = value;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,154 @@
|
|||
import { UITab } from "./tab";
|
||||
|
||||
export class UINode {
|
||||
protected title: string;
|
||||
protected element: HTMLDivElement;
|
||||
protected position: {x: number, y: number, width: number, height: number};
|
||||
protected positionType: string = "fixed";
|
||||
protected calculateOwnStyle: boolean = true;
|
||||
protected keyDownCallback: (key: string) => void;
|
||||
protected focusCallback: () => void;
|
||||
protected blurCallback: () => void;
|
||||
protected clickCallback: () => void;
|
||||
protected globalKeydown: boolean = false;
|
||||
protected visible: boolean;
|
||||
public isFocused: boolean;
|
||||
private userdata: any;
|
||||
|
||||
public constructor(title: string) {
|
||||
this.title = title;
|
||||
this.element = document.createElement("div");
|
||||
this.element.setAttribute("tabindex", "-1");
|
||||
this.visible = false;
|
||||
this.isFocused = false;
|
||||
}
|
||||
|
||||
public focus() {
|
||||
this.element.focus();
|
||||
}
|
||||
|
||||
public click() {
|
||||
this.element.click();
|
||||
}
|
||||
|
||||
public _onConnect() {
|
||||
this.calculateStyle();
|
||||
this.addListeners();
|
||||
return;
|
||||
}
|
||||
|
||||
public _onDisconnect() {
|
||||
return;
|
||||
}
|
||||
|
||||
public _onFocus() {
|
||||
if (this.focusCallback) this.focusCallback();
|
||||
this.isFocused = true;
|
||||
return;
|
||||
}
|
||||
|
||||
public _onBlur() {
|
||||
if (this.blurCallback) this.blurCallback();
|
||||
this.isFocused = false;
|
||||
return;
|
||||
}
|
||||
|
||||
public _onClick() {
|
||||
if (this.clickCallback) this.clickCallback();
|
||||
return;
|
||||
}
|
||||
|
||||
public _onKeydown(key: string, alt: boolean = false, shift: boolean = false, ctrl: boolean = false): boolean {
|
||||
if (this.keyDownCallback) {
|
||||
if (this.globalKeydown || (!this.globalKeydown && document.activeElement === this.getElement())) {
|
||||
this.keyDownCallback(key);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public render(): HTMLElement {
|
||||
this.visible = true;
|
||||
return this.element;
|
||||
}
|
||||
|
||||
protected addListeners() {
|
||||
const elem = this.element;
|
||||
this.getElement().addEventListener("focus", (e) => this._onFocus());
|
||||
elem.addEventListener("blur", (e) => this._onBlur());
|
||||
elem.addEventListener("click", (e) => this._onClick());
|
||||
elem.addEventListener("keydown", e => this._onKeydown(e.key));
|
||||
}
|
||||
|
||||
protected calculateStyle() {
|
||||
if (!this.calculateOwnStyle || !this.position) return;
|
||||
this.element.style.position = this.positionType;
|
||||
this.element.style.left = `${this.position.x}%`;
|
||||
this.element.style.top = `${this.position.y}%`;
|
||||
this.element.style.width = `${this.position.width}%`;
|
||||
this.element.style.height = `${this.position.height}%`;
|
||||
}
|
||||
|
||||
public setPosition(x: number, y: number, width: number, height: number, type: string = "fixed") {
|
||||
this.position = {
|
||||
x: x,
|
||||
y: y,
|
||||
width: width,
|
||||
height: height,
|
||||
};
|
||||
this.positionType = type;
|
||||
this.calculateOwnStyle = true;
|
||||
this.calculateStyle();
|
||||
}
|
||||
|
||||
public onClick(f: () => void) {
|
||||
this.clickCallback = f;
|
||||
return this;
|
||||
}
|
||||
|
||||
public onFocus(f: () => void) {
|
||||
this.focusCallback = f;
|
||||
return this;
|
||||
}
|
||||
|
||||
public onKeyDown(f: (key: string) => void, global: boolean = false) {
|
||||
this.keyDownCallback = f;
|
||||
return this;
|
||||
}
|
||||
|
||||
public onBlur(f: () => void) {
|
||||
this.blurCallback = f;
|
||||
return this;
|
||||
}
|
||||
|
||||
public getElement(): HTMLElement {
|
||||
return this.element;
|
||||
}
|
||||
|
||||
public setTabbable(val: boolean) {
|
||||
this.getElement().setAttribute("tabindex",
|
||||
(val === true) ? "0" :
|
||||
"-1");
|
||||
}
|
||||
|
||||
public setAriaLabel(text: string) {
|
||||
this.element.setAttribute("aria-label", text);
|
||||
}
|
||||
|
||||
public setRole(role: string) {
|
||||
this.getElement().setAttribute("role", role);
|
||||
}
|
||||
|
||||
public getUserData(): any {
|
||||
return this.userdata;
|
||||
}
|
||||
|
||||
public setUserData(obj: any) {
|
||||
this.userdata = obj;
|
||||
}
|
||||
|
||||
public setAccessKey(key: string) {
|
||||
this.getElement().accessKey = key;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,37 @@
|
|||
import { UINode } from "./node";
|
||||
|
||||
export class ProgressBar extends UINode {
|
||||
private progressElement: HTMLProgressElement;
|
||||
public constructor(title: string, max: number) {
|
||||
super(title);
|
||||
this.progressElement = document.createElement("progress");
|
||||
this.progressElement.max = max;
|
||||
this.element.appendChild(this.progressElement);
|
||||
this.element.setAttribute("aria-label", title);
|
||||
}
|
||||
|
||||
public getElement(): HTMLElement {
|
||||
return this.progressElement;
|
||||
}
|
||||
|
||||
public setText(text: string) {
|
||||
this.title = text;
|
||||
this.element.setAttribute("aria-label", text);
|
||||
}
|
||||
|
||||
public getValue(): number {
|
||||
return this.progressElement.value;
|
||||
}
|
||||
|
||||
public setValue(value: number) {
|
||||
this.progressElement.value = value;
|
||||
}
|
||||
|
||||
public getMax(): number {
|
||||
return this.progressElement.max;
|
||||
}
|
||||
|
||||
public setMax(max: number) {
|
||||
this.progressElement.max = max;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,77 @@
|
|||
import { UINode } from "./node";
|
||||
|
||||
export class RadioGroup extends UINode {
|
||||
private id: string;
|
||||
private titleElement: HTMLLegendElement;
|
||||
private containerElement: HTMLFieldSetElement;
|
||||
private radioElements: Map<string, HTMLInputElement>;
|
||||
private radioLabels: Map<string, HTMLLabelElement>;
|
||||
|
||||
public constructor(title: string, options: { key: string; value: string }[]) {
|
||||
super(title);
|
||||
this.id = Math.random().toString();
|
||||
this.titleElement = document.createElement("legend");
|
||||
this.titleElement.innerText = title;
|
||||
this.titleElement.id = `rdgrp_title_${this.id}`;
|
||||
this.containerElement = document.createElement("fieldset");
|
||||
this.containerElement.appendChild(this.titleElement);
|
||||
this.element.appendChild(this.containerElement);
|
||||
|
||||
this.radioElements = new Map();
|
||||
this.radioLabels = new Map();
|
||||
|
||||
options.forEach((option) => {
|
||||
const radioId = `rd_${this.id}_${option.key}`;
|
||||
const radioElement = document.createElement("input");
|
||||
radioElement.id = radioId;
|
||||
radioElement.type = "radio";
|
||||
radioElement.name = `rdgrp_${this.id}`;
|
||||
radioElement.value = option.key;
|
||||
radioElement.setAttribute("aria-labeledby", `${radioId}_label`);
|
||||
radioElement.title = option.value;
|
||||
|
||||
const radioLabel = document.createElement("label");
|
||||
radioLabel.innerText = option.value;
|
||||
radioLabel.id = `${radioId}_label`;
|
||||
radioLabel.setAttribute("for", radioId);
|
||||
|
||||
this.radioElements.set(option.key, radioElement);
|
||||
this.radioLabels.set(option.key, radioLabel);
|
||||
|
||||
this.containerElement.appendChild(radioElement);
|
||||
this.containerElement.appendChild(radioLabel);
|
||||
});
|
||||
}
|
||||
|
||||
public focus() {
|
||||
const firstRadioElement = this.radioElements.values().next().value;
|
||||
if (firstRadioElement) {
|
||||
firstRadioElement.focus();
|
||||
}
|
||||
}
|
||||
|
||||
public getElement(): HTMLElement {
|
||||
return this.containerElement;
|
||||
}
|
||||
|
||||
public setText(text: string) {
|
||||
this.title = text;
|
||||
this.titleElement.innerText = text;
|
||||
}
|
||||
|
||||
public getSelectedValue(): string | null {
|
||||
for (const [key, radioElement] of this.radioElements.entries()) {
|
||||
if (radioElement.checked) {
|
||||
return key;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public setSelectedValue(value: string) {
|
||||
const radioElement = this.radioElements.get(value);
|
||||
if (radioElement) {
|
||||
radioElement.checked = true;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,47 @@
|
|||
import { UINode } from "./node";
|
||||
|
||||
export class Slider extends UINode {
|
||||
private id: string;
|
||||
private titleElement: HTMLLabelElement;
|
||||
private sliderElement: HTMLInputElement;
|
||||
public constructor(title: string, min: number, max: number, step: number = 1) {
|
||||
super(title);
|
||||
this.id = Math.random().toString();
|
||||
this.titleElement = document.createElement("label");
|
||||
this.titleElement.innerText = title;
|
||||
this.titleElement.id = `sldr_title_${this.id}`;
|
||||
this.sliderElement = document.createElement("input");
|
||||
this.sliderElement.id = `sldr_${this.id}`;
|
||||
this.sliderElement.type = "range";
|
||||
this.sliderElement.min = min.toString();
|
||||
this.sliderElement.max = max.toString();
|
||||
this.sliderElement.step = step.toString();
|
||||
this.titleElement.appendChild(this.sliderElement);
|
||||
this.element.appendChild(this.titleElement);
|
||||
}
|
||||
|
||||
public focus() {
|
||||
this.sliderElement.focus();
|
||||
}
|
||||
|
||||
public click() {
|
||||
this.sliderElement.click();
|
||||
}
|
||||
|
||||
public getElement(): HTMLElement {
|
||||
return this.sliderElement;
|
||||
}
|
||||
|
||||
public setText(text: string) {
|
||||
this.title = text;
|
||||
this.titleElement.innerText = text;
|
||||
}
|
||||
|
||||
public getValue(): number {
|
||||
return parseInt(this.sliderElement.value);
|
||||
}
|
||||
|
||||
public setValue(value: number) {
|
||||
this.sliderElement.value = value.toString();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,97 @@
|
|||
import { UINode } from "./node";
|
||||
import { UITab } from "./tab";
|
||||
|
||||
export class TabBar extends UINode {
|
||||
private tabs: UITab[];
|
||||
private tabBarContainer: HTMLDivElement;
|
||||
private onTabChangeCallback: (index: number) => void;
|
||||
private focused: number;
|
||||
|
||||
public constructor(title: string = "tab bar") {
|
||||
super(title);
|
||||
this.tabs = [];
|
||||
this.tabBarContainer = document.createElement("div");
|
||||
this.tabBarContainer.setAttribute("role", "tablist");
|
||||
this.tabBarContainer.style.display = "flex";
|
||||
this.tabBarContainer.style.alignItems = "center";
|
||||
// this.tabBarContainer.style.justifyContent = "space-between";
|
||||
this.tabBarContainer.style.overflow = "hidden";
|
||||
|
||||
this.element.appendChild(this.tabBarContainer);
|
||||
this.focused = 0;
|
||||
}
|
||||
|
||||
public _onFocus() {
|
||||
this.tabs[this.focused].focus();
|
||||
}
|
||||
|
||||
public focus() {
|
||||
this.tabs[this.focused].focus();
|
||||
}
|
||||
|
||||
public add(title: string) {
|
||||
const idx = this.tabs.length;
|
||||
const elem = new UITab(title);
|
||||
elem.onClick(() => {
|
||||
this.selectTab(idx);
|
||||
});
|
||||
this.tabs.push(elem);
|
||||
this.tabBarContainer.appendChild(elem.render());
|
||||
elem._onConnect();
|
||||
if (this.tabs.length === 1) this.calculateTabIndex();
|
||||
}
|
||||
|
||||
public onTabChange(f: (index: number) => void) {
|
||||
this.onTabChangeCallback = f;
|
||||
}
|
||||
|
||||
private selectTab(idx: number) {
|
||||
if (idx !== this.focused) {
|
||||
this.tabs[this.focused].setTabbable(false);
|
||||
this.focused = idx;
|
||||
}
|
||||
if (!this.onTabChangeCallback) return;
|
||||
this.onTabChangeCallback(idx);
|
||||
this.tabs[idx].setTabbable(true);
|
||||
this.tabs[idx].focus();
|
||||
this.updateView();
|
||||
}
|
||||
|
||||
public _onKeydown(key: string): boolean {
|
||||
switch (key) {
|
||||
case "ArrowLeft":
|
||||
this.tabs[this.focused].setTabbable(false);
|
||||
this.focused = Math.max(0, this.focused - 1);
|
||||
this.tabs[this.focused].setTabbable(true);
|
||||
this.selectTab(this.focused);
|
||||
return true;
|
||||
break;
|
||||
case "ArrowRight":
|
||||
this.tabs[this.focused].setTabbable(false);
|
||||
this.focused = Math.min(this.tabs.length - 1, this.focused + 1);
|
||||
this.tabs[this.focused].setTabbable(true);
|
||||
this.selectTab(this.focused);
|
||||
return true;
|
||||
break;
|
||||
default:
|
||||
return false;
|
||||
break;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private updateView() {
|
||||
for (let i = 0; i < this.tabs.length; i++) {
|
||||
this.tabs[i].setSelected(i === this.focused);
|
||||
}
|
||||
}
|
||||
|
||||
public getElement(): HTMLElement {
|
||||
return this.element;
|
||||
}
|
||||
|
||||
|
||||
public calculateTabIndex() {
|
||||
this.tabs[this.focused].setTabbable(true);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,40 @@
|
|||
import { UINode } from "./node";
|
||||
|
||||
export class UITab extends UINode {
|
||||
private textElement: HTMLButtonElement;
|
||||
private selected: boolean;
|
||||
|
||||
public constructor(title: string) {
|
||||
super(title);
|
||||
this.title = title;
|
||||
this.textElement = document.createElement("button");
|
||||
this.textElement.innerText = title;
|
||||
this.textElement.setAttribute("tabindex", "-1");
|
||||
this.textElement.setAttribute("role", "tab");
|
||||
this.textElement.setAttribute("aria-selected", "false");
|
||||
this.element.appendChild(this.textElement);
|
||||
this.selected = false;
|
||||
}
|
||||
|
||||
public focus() {
|
||||
this.textElement.focus();
|
||||
}
|
||||
|
||||
public click() {
|
||||
this.textElement.click();
|
||||
}
|
||||
|
||||
public getElement(): HTMLElement {
|
||||
return this.textElement;
|
||||
}
|
||||
|
||||
public setText(text: string) {
|
||||
this.title = text;
|
||||
this.textElement.innerText = text;
|
||||
}
|
||||
|
||||
public setSelected(val: boolean) {
|
||||
this.selected = val;
|
||||
this.textElement.setAttribute("aria-selected", this.selected.toString());
|
||||
}
|
||||
}
|
|
@ -0,0 +1,50 @@
|
|||
import { UINode } from "./node";
|
||||
import { TabBar } from "./tab-bar";
|
||||
import { Container } from "./container";
|
||||
|
||||
|
||||
export class TabbedView extends UINode {
|
||||
private bar: TabBar;
|
||||
private containers: Container[];
|
||||
private containerElement: HTMLDivElement;
|
||||
private barAtTop: boolean;
|
||||
private currentView: Container;
|
||||
public constructor(title: string, barAtTop: boolean = true) {
|
||||
super(title);
|
||||
this.bar = new TabBar();
|
||||
this.bar._onConnect();
|
||||
this.bar.onTabChange((index: number) => this.onTabChanged(index));
|
||||
this.containers = [];
|
||||
this.containerElement = document.createElement("div");
|
||||
this.element.appendChild(this.bar.render());
|
||||
this.element.appendChild(this.containerElement);
|
||||
this.element.setAttribute("tabindex", "-1");
|
||||
this.barAtTop = barAtTop;
|
||||
}
|
||||
|
||||
public add(name: string, container: Container) {
|
||||
this.bar.add(name);
|
||||
container.setRole("tabpanel");
|
||||
this.containers.push(container);
|
||||
}
|
||||
|
||||
private onTabChanged(idx: number) {
|
||||
if (this.currentView) {
|
||||
this.containerElement.removeChild(this.currentView.render());
|
||||
}
|
||||
this.currentView = this.containers[idx];
|
||||
this.containerElement.appendChild(this.currentView.render());
|
||||
}
|
||||
|
||||
public getElement(): HTMLElement {
|
||||
return this.containerElement;
|
||||
}
|
||||
|
||||
protected calculateStyle(): void {
|
||||
if (this.barAtTop) {
|
||||
this.bar.setPosition(0, 0, 100, 5);
|
||||
} else {
|
||||
this.bar.setPosition(0, 90, 100, 5);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,44 @@
|
|||
import { UINode } from "./node";
|
||||
|
||||
export class TextInput extends UINode {
|
||||
private id: string;
|
||||
private titleElement: HTMLLabelElement;
|
||||
private inputElement: HTMLInputElement;
|
||||
public constructor(title: string) {
|
||||
super(title);
|
||||
this.id = Math.random().toString();
|
||||
this.titleElement = document.createElement("label");
|
||||
this.titleElement.innerText = title;
|
||||
this.titleElement.id = `inpt_title_${this.id}`;
|
||||
this.inputElement = document.createElement("input");
|
||||
this.inputElement.id = `inpt_${this.id}`;
|
||||
this.inputElement.type = "text";
|
||||
this.titleElement.appendChild(this.inputElement);
|
||||
this.element.appendChild(this.titleElement);
|
||||
}
|
||||
|
||||
public focus() {
|
||||
this.inputElement.focus();
|
||||
}
|
||||
|
||||
public click() {
|
||||
this.inputElement.click();
|
||||
}
|
||||
|
||||
public getElement(): HTMLElement {
|
||||
return this.inputElement;
|
||||
}
|
||||
|
||||
public setText(text: string) {
|
||||
this.title = text;
|
||||
this.titleElement.innerText = text;
|
||||
}
|
||||
|
||||
public getValue(): string {
|
||||
return this.inputElement.value;
|
||||
}
|
||||
|
||||
public setValue(value: string) {
|
||||
this.inputElement.value = value;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
import { UINode } from "./node";
|
||||
|
||||
export class Text extends UINode {
|
||||
private textElement: HTMLSpanElement;
|
||||
public constructor(title: string) {
|
||||
super(title);
|
||||
this.textElement = document.createElement("span");
|
||||
this.textElement.innerText = title;
|
||||
this.textElement.setAttribute("tabindex", "-1");
|
||||
this.element.appendChild(this.textElement);
|
||||
}
|
||||
|
||||
public focus() {
|
||||
this.textElement.focus();
|
||||
}
|
||||
|
||||
public click() {
|
||||
this.textElement.click();
|
||||
}
|
||||
|
||||
public getElement(): HTMLElement {
|
||||
return this.textElement;
|
||||
}
|
||||
|
||||
public setText(text: string) {
|
||||
this.title = text;
|
||||
this.textElement.innerText = text;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,40 @@
|
|||
import { UINode } from "./node";
|
||||
|
||||
export class TimePicker extends UINode {
|
||||
private id: string;
|
||||
private titleElement: HTMLLabelElement;
|
||||
private inputElement: HTMLInputElement;
|
||||
public constructor(title: string) {
|
||||
super(title);
|
||||
this.id = Math.random().toString();
|
||||
this.titleElement = document.createElement("label");
|
||||
this.titleElement.innerText = title;
|
||||
this.titleElement.id = `timepicker_title_${this.id}`;
|
||||
this.inputElement = document.createElement("input");
|
||||
this.inputElement.id = `timepicker_${this.id}`;
|
||||
this.inputElement.type = "time";
|
||||
this.titleElement.appendChild(this.inputElement);
|
||||
this.element.appendChild(this.titleElement);
|
||||
}
|
||||
|
||||
public focus() {
|
||||
this.inputElement.focus();
|
||||
}
|
||||
|
||||
public getElement(): HTMLElement {
|
||||
return this.inputElement;
|
||||
}
|
||||
|
||||
public setText(text: string) {
|
||||
this.title = text;
|
||||
this.titleElement.innerText = text;
|
||||
}
|
||||
|
||||
public getValue(): string {
|
||||
return this.inputElement.value;
|
||||
}
|
||||
|
||||
public setValue(value: string) {
|
||||
this.inputElement.value = value;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,197 @@
|
|||
import { UINode } from "./node";
|
||||
import { Treeview } from "./treeview";
|
||||
|
||||
export class TreeviewItem extends UINode {
|
||||
private listElement: HTMLLIElement;
|
||||
private childContainer: HTMLUListElement;
|
||||
|
||||
public children: TreeviewItem[];
|
||||
|
||||
private expanded: boolean;
|
||||
|
||||
private focused: number;
|
||||
|
||||
private parent: TreeviewItem;
|
||||
|
||||
private root: Treeview;
|
||||
|
||||
private previousItem: TreeviewItem;
|
||||
private nextItem: TreeviewItem;
|
||||
|
||||
public constructor(title: string) {
|
||||
super(title);
|
||||
this.listElement = document.createElement("li");
|
||||
this.listElement.innerText = this.title;
|
||||
this.listElement.setAttribute("tabindex", "-1");
|
||||
this.listElement.setAttribute("role", "treeitem");
|
||||
this.element.appendChild(this.listElement);
|
||||
this.listElement.setAttribute("aria-label", this.title);
|
||||
this.children = [];
|
||||
this.focused = 0;
|
||||
}
|
||||
|
||||
public focus() {
|
||||
this.listElement.focus();
|
||||
}
|
||||
|
||||
public click() {
|
||||
this.listElement.click();
|
||||
}
|
||||
|
||||
public getElement(): HTMLElement {
|
||||
return this.listElement;
|
||||
}
|
||||
|
||||
public setText(text: string) {
|
||||
this.title = text;
|
||||
this.listElement.innerText = text;
|
||||
this.element.setAttribute("aria-label", this.title);
|
||||
this.listElement.setAttribute("aria-label", this.title);
|
||||
}
|
||||
|
||||
public add(node: TreeviewItem) {
|
||||
this.children.push(node);
|
||||
node.setParent(this);
|
||||
this.setExpanded(false);
|
||||
if (this.children.length > 0) {
|
||||
this.previousItem = this.children[this.children.length - 1];
|
||||
this.previousItem.setNextItem(node);
|
||||
}
|
||||
}
|
||||
|
||||
public remove(node: TreeviewItem) {
|
||||
const idx = this.children.indexOf(node);
|
||||
if (idx > -1) {
|
||||
this.children.splice(idx, 1);
|
||||
this.updateShownItems();
|
||||
}
|
||||
}
|
||||
|
||||
public expand() {
|
||||
if (!this.isExpandable()) return;
|
||||
if (this.isExpandable() && this.isExpanded() && this.children[this.focused].isExpandable()) {
|
||||
this.children[this.focused].expand();
|
||||
return;
|
||||
}
|
||||
this.setExpanded(true);
|
||||
this.updateShownItems();
|
||||
this.children[this.focused].focus();
|
||||
}
|
||||
|
||||
public collapse(ignoreFocus: boolean = false) {
|
||||
if (!this.isExpandable()) {
|
||||
if (this.getElement() !== document.activeElement && ignoreFocus) return;
|
||||
this.parent.collapse(true);
|
||||
return;
|
||||
}
|
||||
this.setExpanded(false);
|
||||
this.updateShownItems();
|
||||
setTimeout(() => this.focus(), 0);
|
||||
}
|
||||
|
||||
public isExpandable(): boolean {
|
||||
return this.children.length > 0;
|
||||
}
|
||||
|
||||
public isExpanded() {
|
||||
return this.expanded;
|
||||
}
|
||||
|
||||
private setExpanded(val: boolean) {
|
||||
this.expanded = val;
|
||||
if (this.expanded) {
|
||||
this.listElement.setAttribute("aria-expanded", "true");
|
||||
return;
|
||||
}
|
||||
this.listElement.setAttribute("aria-expanded", "false");
|
||||
}
|
||||
|
||||
private updateShownItems() {
|
||||
if (this.expanded) {
|
||||
if (!this.childContainer) {
|
||||
this.childContainer = document.createElement("ul");
|
||||
this.childContainer.setAttribute("role", "group");
|
||||
this.children.forEach((child) => this.childContainer.appendChild(child.render()));
|
||||
this.listElement.appendChild(this.childContainer);
|
||||
} else {
|
||||
this.childContainer.hidden = false;
|
||||
}
|
||||
} else {
|
||||
this.childContainer.hidden = true;
|
||||
// this.listElement.removeChild(this.childContainer);
|
||||
}
|
||||
}
|
||||
|
||||
public focusNext(): boolean {
|
||||
if (this.isExpandable() && this.isExpanded() && this.children[this.focused].isExpandable()) {
|
||||
return this.children[this.focused].focusNext();
|
||||
}
|
||||
this.children[this.focused].setTabbable(false);
|
||||
this.focused = Math.min(this.children.length - 1, this.focused + 1);
|
||||
this.children[this.focused].setTabbable(true);
|
||||
this.children[this.focused].focus();
|
||||
return true;
|
||||
}
|
||||
|
||||
public focusPrevious(): boolean {
|
||||
if (this.isExpandable() && this.isExpanded() && this.children[this.focused].isExpandable()) {
|
||||
return this.children[this.focused].focusPrevious();
|
||||
}
|
||||
this.children[this.focused].setTabbable(false);
|
||||
this.focused = Math.max(0, this.focused - 1);
|
||||
this.children[this.focused].setTabbable(true);
|
||||
this.children[this.focused].focus();
|
||||
return true;
|
||||
}
|
||||
|
||||
public setParent(item: TreeviewItem) {
|
||||
this.parent = this.parent;
|
||||
}
|
||||
|
||||
public getParent(): TreeviewItem {
|
||||
return this.parent;
|
||||
}
|
||||
|
||||
public setPrevious(node: TreeviewItem) {
|
||||
this.previousItem = node;
|
||||
}
|
||||
|
||||
public setNextItem(node: TreeviewItem) {
|
||||
this.nextItem = node;
|
||||
}
|
||||
|
||||
public getPrevious(): TreeviewItem {
|
||||
return this.previousItem;
|
||||
}
|
||||
|
||||
public getNext(): TreeviewItem {
|
||||
return this.nextItem;
|
||||
}
|
||||
|
||||
public _onKeydown(key: string, alt?: boolean, shift?: boolean, ctrl?: boolean): boolean {
|
||||
switch (key) {
|
||||
case "ArrowUp":
|
||||
this.focusPrevious();
|
||||
return true;
|
||||
break;
|
||||
case "ArrowDown":
|
||||
this.focusNext();
|
||||
return true;
|
||||
case "ArrowLeft":
|
||||
this.collapse();
|
||||
return true;
|
||||
case "ArrowRight":
|
||||
if (this.children[this.focused].isExpandable() && !this.children[this.focused].isExpanded()) {
|
||||
this.children[this.focused].expand();
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public focusOnItem() {
|
||||
this.children[this.focused].focus();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,153 @@
|
|||
import { UINode } from "./node";
|
||||
import { TreeviewItem } from "./treeview-item";
|
||||
|
||||
export class Treeview extends UINode {
|
||||
public children: TreeviewItem[];
|
||||
protected listElement: HTMLUListElement;
|
||||
private focused: number;
|
||||
protected selectCallback: (id: number) => void;
|
||||
public constructor(title: string) {
|
||||
super(title);
|
||||
this.children = [];
|
||||
this.listElement = document.createElement("ul");
|
||||
this.listElement.setAttribute("role", "tree");
|
||||
this.listElement.style.listStyle = "none";
|
||||
this.element.appendChild(this.listElement);
|
||||
this.element.setAttribute("aria-label", this.title);
|
||||
this.focused = 0;
|
||||
}
|
||||
|
||||
public add(node: TreeviewItem) {
|
||||
this.children.push(node);
|
||||
node._onConnect();
|
||||
this.listElement.appendChild(node.render());
|
||||
if (this.children.length === 1) this.calculateTabIndex();
|
||||
node.onFocus(() => this.calculateFocused(node));
|
||||
}
|
||||
|
||||
public remove(node: TreeviewItem) {
|
||||
const idx = this.children.indexOf(node);
|
||||
this.children.splice(idx, 1);
|
||||
node._onDisconnect();
|
||||
this.listElement.removeChild(node.render());
|
||||
if (idx === this.focused) {
|
||||
if (this.focused > 0) this.focused--;
|
||||
this.calculateTabIndex();
|
||||
}
|
||||
}
|
||||
|
||||
public _onFocus() {
|
||||
super._onFocus();
|
||||
this.children[this.focused].focus();
|
||||
}
|
||||
|
||||
public _onClick() {
|
||||
this.children[this.focused]._onClick();
|
||||
}
|
||||
|
||||
public _onSelect(id: number) {
|
||||
if (this.selectCallback) this.selectCallback(id);
|
||||
}
|
||||
|
||||
protected calculateStyle(): void {
|
||||
super.calculateStyle();
|
||||
this.element.style.overflowY = "scroll";
|
||||
this.listElement.style.overflowY = "scroll";
|
||||
}
|
||||
|
||||
public _onKeydown(key: string, alt: boolean = false, shift: boolean = false, ctrl: boolean = false): boolean {
|
||||
switch (key) {
|
||||
case "ArrowUp":
|
||||
return this.focusPrevious();
|
||||
break;
|
||||
case "ArrowDown":
|
||||
return this.focusNext();
|
||||
break;
|
||||
case "Enter":
|
||||
this.children[this.focused].click();
|
||||
return true;
|
||||
break;
|
||||
case "ArrowLeft":
|
||||
// this.children[this.focused].collapse();
|
||||
return true;
|
||||
break;
|
||||
case "ArrowRight":
|
||||
this.children[this.focused].expand();
|
||||
return true;
|
||||
break;
|
||||
default:
|
||||
return this.children[this.focused]._onKeydown(key);
|
||||
break;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
protected renderAsListItem(node: UINode) {
|
||||
let li = document.createElement("li");
|
||||
li.appendChild(node.render());
|
||||
return li;
|
||||
}
|
||||
|
||||
public getElement(): HTMLElement {
|
||||
return this.listElement;
|
||||
}
|
||||
|
||||
public isItemFocused(): boolean {
|
||||
const has = this.children.find((child) => child.isFocused);
|
||||
if (has) {
|
||||
console.log("I have child", has);
|
||||
return true;
|
||||
}
|
||||
console.log("No children selected");
|
||||
return false;
|
||||
}
|
||||
|
||||
private calculateTabIndex() {
|
||||
this.children[this.focused].setTabbable(true);
|
||||
}
|
||||
|
||||
public clear() {
|
||||
this.children.forEach((child) => this.remove(child));
|
||||
this.children = [];
|
||||
this.listElement.innerHTML = '';
|
||||
this.focused = 0;
|
||||
}
|
||||
|
||||
public getFocusedChild() {
|
||||
return this.children[this.focused];
|
||||
}
|
||||
|
||||
public getFocus() {
|
||||
return this.focused;
|
||||
}
|
||||
|
||||
public onSelect(f: (id: number) => void) {
|
||||
this.selectCallback = f;
|
||||
}
|
||||
|
||||
protected calculateFocused(node: TreeviewItem) {
|
||||
const idx = this.children.indexOf(node);
|
||||
this._onSelect(idx);
|
||||
}
|
||||
|
||||
public focusPrevious() {
|
||||
if (this.children[this.focused].isExpanded()) {
|
||||
// return this.children[this.focused].focusPrevious();
|
||||
} else {
|
||||
this.focused = Math.max(0, this.focused - 1);
|
||||
this.children[this.focused].focus();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public focusNext() {
|
||||
if (this.children[this.focused].isExpanded()) {
|
||||
// return this.children[this.focused].focusNext();
|
||||
} else {
|
||||
this.focused = Math.min(this.children.length - 1, this.focused + 1);
|
||||
this.children[this.focused].focus();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,35 @@
|
|||
import { UIWindow } from "./window";
|
||||
|
||||
export class ViewManager {
|
||||
private currentWindow: UIWindow;
|
||||
private windows: UIWindow[];
|
||||
private viewElement: HTMLDivElement;
|
||||
public constructor() {
|
||||
this.windows = [];
|
||||
this.viewElement = document.createElement("div");
|
||||
}
|
||||
|
||||
public add(window: UIWindow) {
|
||||
this.windows.push(window);
|
||||
window.onConnect();
|
||||
this.viewElement.appendChild(window.show());
|
||||
}
|
||||
|
||||
public remove(window: UIWindow) {
|
||||
this.windows.splice(this.windows.indexOf(window), 1);
|
||||
window.onDisconnect();
|
||||
this.viewElement.removeChild(window.show());
|
||||
}
|
||||
|
||||
public switchTo(window: UIWindow) {
|
||||
if (this.currentWindow !== undefined) {
|
||||
this.currentWindow.onDisconnect();
|
||||
}
|
||||
this.currentWindow = window;
|
||||
this.currentWindow.onConnect();
|
||||
}
|
||||
|
||||
public render() {
|
||||
return this.viewElement;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,76 @@
|
|||
import { Container } from "./container";
|
||||
import { UINode } from "./node";
|
||||
|
||||
export class UIWindow {
|
||||
public title: string;
|
||||
public width: number;
|
||||
public height: number;
|
||||
public position: { x: number; y: number; };
|
||||
public container: Container;
|
||||
public visible: boolean;
|
||||
private element: HTMLDivElement;
|
||||
private rendered: boolean;
|
||||
private keyDown: (e: KeyboardEvent) => void;
|
||||
|
||||
public constructor(
|
||||
title: string,
|
||||
classname?: string,
|
||||
private setTitle: boolean = true
|
||||
) {
|
||||
this.title = title;
|
||||
this.container = new Container(this.title);
|
||||
this.container._onConnect();
|
||||
this.element = document.createElement("div");
|
||||
if (classname) {
|
||||
this.element.className = classname;
|
||||
}
|
||||
this.keyDown = this.onKeyDown.bind(this);
|
||||
this.visible = false;
|
||||
}
|
||||
|
||||
public add(node: UINode) {
|
||||
this.container.add(node);
|
||||
}
|
||||
|
||||
public remove(node: UINode) {
|
||||
this.container.remove(node);
|
||||
}
|
||||
|
||||
public show() {
|
||||
if (this.visible) return;
|
||||
if (this.setTitle) document.title = this.title;
|
||||
if (this.rendered) return this.element;
|
||||
this.element.appendChild(this.container.render());
|
||||
this.element.addEventListener("keydown", this.keyDown);
|
||||
this.element.focus();
|
||||
this.visible = true;
|
||||
this.rendered = true;
|
||||
return this.element;
|
||||
}
|
||||
|
||||
public hide() {
|
||||
if (!this.visible) return;
|
||||
this.visible = false;
|
||||
this.rendered = false;
|
||||
this.element.replaceChildren();
|
||||
this.element.removeEventListener("keydown", this.keyDown);
|
||||
}
|
||||
|
||||
public onKeyDown(e: KeyboardEvent) {
|
||||
if (this.container._onKeydown(e.key, e.altKey, e.shiftKey, e.ctrlKey)) {
|
||||
e.preventDefault();
|
||||
}
|
||||
}
|
||||
|
||||
public onConnect() {
|
||||
return;
|
||||
}
|
||||
|
||||
public onDisconnect() {
|
||||
return;
|
||||
}
|
||||
|
||||
public getElement(): HTMLElement {
|
||||
return this.element;
|
||||
}
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
/// <reference types="vite/client" />
|
|
@ -0,0 +1,23 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"useDefineForClassFields": true,
|
||||
"module": "ESNext",
|
||||
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||
"skipLibCheck": true,
|
||||
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
|
||||
/* Linting */
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
Loading…
Reference in New Issue