Convert to typescript (#795)

* Typescript config setup

* Add type annotations to components

* Further type additions

* And more...

* Add types to actions

* Add types to templates

* Further type checks

* Further type additons

* Install fuse latest

* Housekeeping

* Remove old type definitions

* Fix remaning type issues

* Fix some failing tests

* Remove types workflow

* Fix failing unit tests

* Resolve back space event regression

* Convert cypress files to .ts

* Fix eslint issues

* Remove cachebusting urls

* Resolve delete button bug

* Resolve regression bugs

* Fix lint script

* Fix lint workflow

* Pass args instead of object to keyboard handlers

* Flatten misc reducer

* Resolve keyboad action test failures

* Use Pick instead of Partial

* Use interfaces in action tests

* Update firefox image

* Incorporate #791

* Incorporate #788
This commit is contained in:
Josh Johnson 2019-12-23 18:22:54 +00:00 committed by GitHub
parent 3ed027229d
commit 68313da412
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
73 changed files with 5703 additions and 5720 deletions

View File

@ -1,16 +1,21 @@
{
"parserOptions": {
"ecmaVersion": 2020
},
"parser": "@typescript-eslint/parser",
"plugins": ["@typescript-eslint", "prettier", "sort-class-members"],
"extends": [
"airbnb-base",
"plugin:prettier/recommended",
"plugin:compat/recommended"
"plugin:compat/recommended",
"plugin:@typescript-eslint/recommended"
],
"plugins": ["prettier", "sort-class-members"],
"env": {
"es6": true,
"browser": true
"browser": true,
"mocha": true,
"cypress/globals": true
},
"parserOptions": {
"project": "./tsconfig.json",
"ecmaVersion": 2020
},
"rules": {
"import/prefer-default-export": "off",
@ -54,18 +59,23 @@
],
"accessorPairPositioning": "getThenSet"
}
]
],
"lines-between-class-members": "off",
"@typescript-eslint/no-namespace": "off"
},
"overrides": [
{
"files": ["*.test.js"],
"files": ["*.test.ts"],
"env": {
"mocha": true
},
"rules": {
"no-restricted-syntax": "off",
"compat/compat": "off",
"no-new": "off"
"no-new": "off",
"@typescript-eslint/no-empty-function": "off",
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/no-non-null-assertion": "off"
}
},
{
@ -92,6 +102,11 @@
"Element.prototype.classList",
"Element.prototype.closest",
"Element.prototype.dataset"
]
],
"import/resolver": {
"node": {
"extensions": [".js", ".ts"]
}
}
}
}

BIN
.github/actions-scripts/__snapshots__/firefox-darwin.png vendored Normal file → Executable file

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

After

Width:  |  Height:  |  Size: 44 KiB

View File

@ -27,10 +27,10 @@ jobs:
- name: run eslint
run: |
CHANGED_JS=$(git --no-pager diff --name-only ..origin/master | grep '^src\/scripts\/.*\.js$' | xargs ls -d 2>/dev/null | paste -sd " " -)
if [[ -z $(sed -e 's/[[:space:]]*$//' <<<${CHANGED_JS}) ]]; then CHANGED_JS="src/scripts"; fi
echo $CHANGED_JS
node node_modules/eslint/bin/eslint.js $CHANGED_JS
CHANGED_TS=$(git --no-pager diff --name-only ..origin/master | grep '^src\/scripts\/.*\.ts$' | xargs ls -d 2>/dev/null | paste -sd " " -)
if [[ -z $(sed -e 's/[[:space:]]*$//' <<<${CHANGED_TS}) ]]; then CHANGED_TS="src/scripts"; fi
echo $CHANGED_TS
node node_modules/eslint/bin/eslint.js $CHANGED_TS
- name: Lint JS bundle
run: |

View File

@ -1,27 +0,0 @@
name: TypeScript Check
on:
pull_request:
paths:
- 'types/index.d.ts'
jobs:
tsc:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
with:
fetch-depth: 1
- uses: actions/setup-node@v1
with:
node-version: 12
- name: Install TypeScript
run: npm install -g typescript
- name: Install required dependencies
run: npm i --only=production --no-optional --no-audit --ignore-scripts
- name: Check typings file
run: tsc types/index.d.ts

View File

@ -1,7 +1,8 @@
require:
- '@babel/register'
- 'ts-node/register'
- './config/jsdom.js'
exit: true
spec: src/**/*.test.js
spec: src/**/**/*.test.ts
extension:
- ts
- js

View File

@ -59,5 +59,8 @@
"fileMatch": [".prettierrc.json"],
"url": "http://json.schemastore.org/prettierrc"
}
]
],
"editor.codeActionsOnSave": {
"source.fixAll.eslint": true
}
}

326
package-lock.json generated
View File

@ -950,6 +950,18 @@
"integrity": "sha512-+iTbntw2IZPb/anVDbypzfQa+ay64MW0Zo8aJ8gZPWMMK6/OubMVb6lUPMagqjOPnmtauXnFCACVl3O7ogjeqQ==",
"dev": true
},
"@types/chai": {
"version": "4.2.7",
"resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.2.7.tgz",
"integrity": "sha512-luq8meHGYwvky0O7u0eQZdA7B4Wd9owUCqvbw2m3XCrCU8mplYOujMBbvyS547AxJkC+pGnd0Cm15eNxEUNU8g==",
"dev": true
},
"@types/eslint-visitor-keys": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@types/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz",
"integrity": "sha512-OCutwjDZ4aFS6PB1UZ988C4YgwlBHJd6wCeQqaLdmadZ/7e+w79+hbMUFC1QXDNCmdyoRfAFdm0RypzwR+Qpag==",
"dev": true
},
"@types/events": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/@types/events/-/events-3.0.0.tgz",
@ -967,12 +979,24 @@
"@types/node": "*"
}
},
"@types/json-schema": {
"version": "7.0.3",
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.3.tgz",
"integrity": "sha512-Il2DtDVRGDcqjDtE+rF8iqg1CArehSK84HZJCT7AMITlyXRBpuPhqGLDQMowraqqu1coEaimg4ZOqggt6L6L+A==",
"dev": true
},
"@types/minimatch": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.3.tgz",
"integrity": "sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA==",
"dev": true
},
"@types/mocha": {
"version": "5.2.7",
"resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-5.2.7.tgz",
"integrity": "sha512-NYrtPht0wGzhwe9+/idPaBB+TqkY9AhTvOLMkThm0IoEfLaiVQZwBwyJ5puCkO3AUCWrmcoePjp2mbFocKy4SQ==",
"dev": true
},
"@types/node": {
"version": "12.11.2",
"resolved": "https://registry.npmjs.org/@types/node/-/node-12.11.2.tgz",
@ -985,12 +1009,109 @@
"integrity": "sha512-f5j5b/Gf71L+dbqxIpQ4Z2WlmI/mPJ0fOkGGmFgtb6sAu97EPczzbS3/tJKxmcYDj55OX6ssqwDAWOHIYDRDGA==",
"dev": true
},
"@types/sinon": {
"version": "7.5.1",
"resolved": "https://registry.npmjs.org/@types/sinon/-/sinon-7.5.1.tgz",
"integrity": "sha512-EZQUP3hSZQyTQRfiLqelC9NMWd1kqLcmQE0dMiklxBkgi84T+cHOhnKpgk4NnOWpGX863yE6+IaGnOXUNFqDnQ==",
"dev": true
},
"@types/sinon-chai": {
"version": "3.2.3",
"resolved": "https://registry.npmjs.org/@types/sinon-chai/-/sinon-chai-3.2.3.tgz",
"integrity": "sha512-TOUFS6vqS0PVL1I8NGVSNcFaNJtFoyZPXZ5zur+qlhDfOmQECZZM4H4kKgca6O8L+QceX/ymODZASfUfn+y4yQ==",
"dev": true,
"requires": {
"@types/chai": "*",
"@types/sinon": "*"
}
},
"@types/sizzle": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/@types/sizzle/-/sizzle-2.3.2.tgz",
"integrity": "sha512-7EJYyKTL7tFR8+gDbB6Wwz/arpGa0Mywk1TJbNzKzHtzbwVmY4HR9WqS5VV7dsBUKQmPNr192jHr/VpBluj/hg==",
"dev": true
},
"@typescript-eslint/eslint-plugin": {
"version": "2.11.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-2.11.0.tgz",
"integrity": "sha512-G2HHA1vpMN0EEbUuWubiCCfd0R3a30BB+UdvnFkxwZIxYEGOrWEXDv8tBFO9f44CWc47Xv9lLM3VSn4ORLI2bA==",
"dev": true,
"requires": {
"@typescript-eslint/experimental-utils": "2.11.0",
"eslint-utils": "^1.4.3",
"functional-red-black-tree": "^1.0.1",
"regexpp": "^3.0.0",
"tsutils": "^3.17.1"
},
"dependencies": {
"regexpp": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.0.0.tgz",
"integrity": "sha512-Z+hNr7RAVWxznLPuA7DIh8UNX1j9CDrUQxskw9IrBE1Dxue2lyXT+shqEIeLUjrokxIP8CMy1WkjgG3rTsd5/g==",
"dev": true
}
}
},
"@typescript-eslint/experimental-utils": {
"version": "2.11.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-2.11.0.tgz",
"integrity": "sha512-YxcA/y0ZJaCc/fB/MClhcDxHI0nOBB7v2/WxBju2cOTanX7jO9ttQq6Fy4yW9UaY5bPd9xL3cun3lDVqk67sPQ==",
"dev": true,
"requires": {
"@types/json-schema": "^7.0.3",
"@typescript-eslint/typescript-estree": "2.11.0",
"eslint-scope": "^5.0.0"
}
},
"@typescript-eslint/parser": {
"version": "2.11.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-2.11.0.tgz",
"integrity": "sha512-DyGXeqhb3moMioEFZIHIp7oXBBh7dEfPTzGrlyP0Mi9ScCra4SWEGs3kPd18mG7Sy9Wy8z88zmrw5tSGL6r/6A==",
"dev": true,
"requires": {
"@types/eslint-visitor-keys": "^1.0.0",
"@typescript-eslint/experimental-utils": "2.11.0",
"@typescript-eslint/typescript-estree": "2.11.0",
"eslint-visitor-keys": "^1.1.0"
}
},
"@typescript-eslint/typescript-estree": {
"version": "2.11.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-2.11.0.tgz",
"integrity": "sha512-HGY4+d4MagO6cKMcKfIKaTMxcAv7dEVnji2Zi+vi5VV8uWAM631KjAB5GxFcexMYrwKT0EekRiiGK1/Sd7VFGA==",
"dev": true,
"requires": {
"debug": "^4.1.1",
"eslint-visitor-keys": "^1.1.0",
"glob": "^7.1.6",
"is-glob": "^4.0.1",
"lodash.unescape": "4.0.1",
"semver": "^6.3.0",
"tsutils": "^3.17.1"
},
"dependencies": {
"glob": {
"version": "7.1.6",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz",
"integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==",
"dev": true,
"requires": {
"fs.realpath": "^1.0.0",
"inflight": "^1.0.4",
"inherits": "2",
"minimatch": "^3.0.4",
"once": "^1.3.0",
"path-is-absolute": "^1.0.0"
}
},
"semver": {
"version": "6.3.0",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
"integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==",
"dev": true
}
}
},
"@webassemblyjs/ast": {
"version": "1.8.5",
"resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.8.5.tgz",
@ -1413,6 +1534,12 @@
"readable-stream": "^2.0.6"
}
},
"arg": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/arg/-/arg-4.1.2.tgz",
"integrity": "sha512-+ytCkGcBtHZ3V2r2Z06AncYO8jz46UEamcspGoU8lHcEbpn6J77QK0vdWvChsclg/tM5XIJC5tnjmPp7Eq6Obg==",
"dev": true
},
"argparse": {
"version": "1.0.10",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
@ -3681,9 +3808,9 @@
}
},
"eslint": {
"version": "6.6.0",
"resolved": "https://registry.npmjs.org/eslint/-/eslint-6.6.0.tgz",
"integrity": "sha512-PpEBq7b6qY/qrOmpYQ/jTMDYfuQMELR4g4WI1M/NaSDDD/bdcMb+dj4Hgks7p41kW2caXsPsEZAEAyAgjVVC0g==",
"version": "6.8.0",
"resolved": "https://registry.npmjs.org/eslint/-/eslint-6.8.0.tgz",
"integrity": "sha512-K+Iayyo2LtyYhDSYwz5D5QdWw0hCacNzyq1Y821Xna2xSJj7cijoLLYmLxTQgcgZ9mC61nryMy9S7GRbYpI5Ig==",
"dev": true,
"requires": {
"@babel/code-frame": "^7.0.0",
@ -3701,7 +3828,7 @@
"file-entry-cache": "^5.0.1",
"functional-red-black-tree": "^1.0.1",
"glob-parent": "^5.0.0",
"globals": "^11.7.0",
"globals": "^12.1.0",
"ignore": "^4.0.6",
"import-fresh": "^3.0.0",
"imurmurhash": "^0.1.4",
@ -3714,7 +3841,7 @@
"minimatch": "^3.0.4",
"mkdirp": "^0.5.1",
"natural-compare": "^1.4.0",
"optionator": "^0.8.2",
"optionator": "^0.8.3",
"progress": "^2.0.0",
"regexpp": "^2.0.1",
"semver": "^6.1.2",
@ -3731,16 +3858,39 @@
"integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==",
"dev": true
},
"globals": {
"version": "12.3.0",
"resolved": "https://registry.npmjs.org/globals/-/globals-12.3.0.tgz",
"integrity": "sha512-wAfjdLgFsPZsklLJvOBUBmzYE8/CwhEqSBEMRXA3qxIiNtyqvjYurAtIfDh6chlEPUfmTY3MnZh5Hfh4q0UlIw==",
"dev": true,
"requires": {
"type-fest": "^0.8.1"
}
},
"import-fresh": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.1.0.tgz",
"integrity": "sha512-PpuksHKGt8rXfWEr9m9EHIpgyyaltBy8+eF6GJM0QCAxMgxCfucMF3mjecK2QsJr0amJW7gTqh5/wht0z2UhEQ==",
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.2.1.tgz",
"integrity": "sha512-6e1q1cnWP2RXD9/keSkxHScg508CdXqXWgWBaETNhyuBFz+kUZlKboh+ISK+bU++DmbHimVBrOz/zzPe0sZ3sQ==",
"dev": true,
"requires": {
"parent-module": "^1.0.0",
"resolve-from": "^4.0.0"
}
},
"optionator": {
"version": "0.8.3",
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz",
"integrity": "sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==",
"dev": true,
"requires": {
"deep-is": "~0.1.3",
"fast-levenshtein": "~2.0.6",
"levn": "~0.3.0",
"prelude-ls": "~1.1.2",
"type-check": "~0.3.2",
"word-wrap": "~1.2.3"
}
},
"resolve-from": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
@ -3767,6 +3917,12 @@
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.0.1.tgz",
"integrity": "sha512-VTyMAUfdm047mwKl+u79WIdrZxtFtn+nBxHeb844XBQ9uMNTuTHdx2hc5RiAJYqwTj3wc/xe5HLSdJSkJ+WfZw==",
"dev": true
},
"type-fest": {
"version": "0.8.1",
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz",
"integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==",
"dev": true
}
}
},
@ -3955,9 +4111,9 @@
}
},
"eslint-plugin-cypress": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/eslint-plugin-cypress/-/eslint-plugin-cypress-2.7.0.tgz",
"integrity": "sha512-52Lq5ePCD/8jc536e1RqtLfj33BAy1s7BlYgCjbG39J5kqUitcTlRY5i3NRoeAyPHueDwETsq0eASF44ugLosQ==",
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/eslint-plugin-cypress/-/eslint-plugin-cypress-2.8.1.tgz",
"integrity": "sha512-jDpcP+MmjmqQO/x3bwIXgp4cl7Q66RYS5/IsuOQP4Qo2sEqE3DI8tTxBQ1EhnV5qEDd2Z2TYHR+5vYI6oCN4uw==",
"dev": true,
"requires": {
"globals": "^11.12.0"
@ -5870,9 +6026,9 @@
"dev": true
},
"fuse.js": {
"version": "3.4.5",
"resolved": "https://registry.npmjs.org/fuse.js/-/fuse.js-3.4.5.tgz",
"integrity": "sha512-s9PGTaQIkT69HaeoTVjwGsLfb8V8ScJLx5XGFcKHg0MqLUH/UZ4EKOtqtXX9k7AFqCGxD1aJmYb8Q5VYDibVRQ=="
"version": "3.4.6",
"resolved": "https://registry.npmjs.org/fuse.js/-/fuse.js-3.4.6.tgz",
"integrity": "sha512-H6aJY4UpLFwxj1+5nAvufom5b2BT2v45P1MkPvdGIK8fWjQx/7o6tTT1+ALV0yawQvbmvCF0ufl2et8eJ7v7Cg=="
},
"gauge": {
"version": "2.7.4",
@ -6653,9 +6809,9 @@
"dev": true
},
"inquirer": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/inquirer/-/inquirer-7.0.0.tgz",
"integrity": "sha512-rSdC7zelHdRQFkWnhsMu2+2SO41mpv2oF2zy4tMhmiLWkcKbOAs87fWAJhVXttKVwhdZvymvnuM95EyEXg2/tQ==",
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/inquirer/-/inquirer-7.0.1.tgz",
"integrity": "sha512-V1FFQ3TIO15det8PijPLFR9M9baSlnRs9nL7zWu1MNVA2T9YVl9ZbrHJhYs7e9X8jeMZ3lr2JH/rdHFgNCBdYw==",
"dev": true,
"requires": {
"ansi-escapes": "^4.2.1",
@ -6667,25 +6823,25 @@
"lodash": "^4.17.15",
"mute-stream": "0.0.8",
"run-async": "^2.2.0",
"rxjs": "^6.4.0",
"rxjs": "^6.5.3",
"string-width": "^4.1.0",
"strip-ansi": "^5.1.0",
"through": "^2.3.6"
},
"dependencies": {
"ansi-escapes": {
"version": "4.2.1",
"resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.2.1.tgz",
"integrity": "sha512-Cg3ymMAdN10wOk/VYfLV7KCQyv7EDirJ64500sU7n9UlmioEtDuU5Gd+hj73hXSU/ex7tHJSssmyftDdkMLO8Q==",
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.0.tgz",
"integrity": "sha512-EiYhwo0v255HUL6eDyuLrXEkTi7WwVCLAw+SeOQ7M7qdun1z1pum4DEm/nuqIVbPvi9RPPc9k9LbyBv6H0DwVg==",
"dev": true,
"requires": {
"type-fest": "^0.5.2"
"type-fest": "^0.8.1"
}
},
"ansi-regex": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz",
"integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==",
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz",
"integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==",
"dev": true
},
"cli-cursor": {
@ -6747,14 +6903,25 @@
}
},
"string-width": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.1.0.tgz",
"integrity": "sha512-NrX+1dVVh+6Y9dnQ19pR0pP4FiEIlUvdTGn8pw6CKTNq5sgib2nIhmUNT5TAmhWmvKr3WcxBcP3E8nWezuipuQ==",
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz",
"integrity": "sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==",
"dev": true,
"requires": {
"emoji-regex": "^8.0.0",
"is-fullwidth-code-point": "^3.0.0",
"strip-ansi": "^5.2.0"
"strip-ansi": "^6.0.0"
},
"dependencies": {
"strip-ansi": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz",
"integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==",
"dev": true,
"requires": {
"ansi-regex": "^5.0.0"
}
}
}
},
"strip-ansi": {
@ -6764,12 +6931,20 @@
"dev": true,
"requires": {
"ansi-regex": "^4.1.0"
},
"dependencies": {
"ansi-regex": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz",
"integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==",
"dev": true
}
}
},
"type-fest": {
"version": "0.5.2",
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.5.2.tgz",
"integrity": "sha512-DWkS49EQKVX//Tbupb9TFa19c7+MK1XmzkrZUR8TAktmE/DizXoaoJV6TZ/tSIPXipqNiRI6CyAe7x69Jb6RSw==",
"version": "0.8.1",
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz",
"integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==",
"dev": true
}
}
@ -8064,6 +8239,12 @@
"integrity": "sha1-7dFMgk4sycHgsKG0K7UhBRakJDg=",
"dev": true
},
"lodash.unescape": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/lodash.unescape/-/lodash.unescape-4.0.1.tgz",
"integrity": "sha1-vyJJiGzlFM2hEvrpIYzcBlIR/Jw=",
"dev": true
},
"log-symbols": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-2.2.0.tgz",
@ -8133,6 +8314,12 @@
"semver": "^5.6.0"
}
},
"make-error": {
"version": "1.3.5",
"resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.5.tgz",
"integrity": "sha512-c3sIjNUow0+8swNwVpqoH4YCShKNFkMaw6oH1mNS2haDZQqkeZFlHS3dhoeEbKKmJB4vXpJucU6oH75aDYeE9g==",
"dev": true
},
"mamacro": {
"version": "0.0.3",
"resolved": "https://registry.npmjs.org/mamacro/-/mamacro-0.0.3.tgz",
@ -10794,9 +10981,9 @@
"dev": true
},
"prettier": {
"version": "1.18.2",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-1.18.2.tgz",
"integrity": "sha512-OeHeMc0JhFE9idD4ZdtNibzY0+TPHSpSSb9h8FqtP+YnoZZ1sl8Vc9b1sasjfymH3SonAF4QcA2+mzHPhMvIiw==",
"version": "1.19.1",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-1.19.1.tgz",
"integrity": "sha512-s7PoyDv/II1ObgQunCbB9PdLmUcBZcnWOcxDh7O0N/UwDEsHyqkW+Qh28jW+mVuCdx7gLB0BotYI1Y6uI9iyew==",
"dev": true
},
"prettier-linter-helpers": {
@ -13195,12 +13382,63 @@
"glob": "^7.1.2"
}
},
"ts-loader": {
"version": "6.2.1",
"resolved": "https://registry.npmjs.org/ts-loader/-/ts-loader-6.2.1.tgz",
"integrity": "sha512-Dd9FekWuABGgjE1g0TlQJ+4dFUfYGbYcs52/HQObE0ZmUNjQlmLAS7xXsSzy23AMaMwipsx5sNHvoEpT2CZq1g==",
"dev": true,
"requires": {
"chalk": "^2.3.0",
"enhanced-resolve": "^4.0.0",
"loader-utils": "^1.0.2",
"micromatch": "^4.0.0",
"semver": "^6.0.0"
},
"dependencies": {
"semver": {
"version": "6.3.0",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
"integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==",
"dev": true
}
}
},
"ts-node": {
"version": "8.5.4",
"resolved": "https://registry.npmjs.org/ts-node/-/ts-node-8.5.4.tgz",
"integrity": "sha512-izbVCRV68EasEPQ8MSIGBNK9dc/4sYJJKYA+IarMQct1RtEot6Xp0bXuClsbUSnKpg50ho+aOAx8en5c+y4OFw==",
"dev": true,
"requires": {
"arg": "^4.1.0",
"diff": "^4.0.1",
"make-error": "^1.1.1",
"source-map-support": "^0.5.6",
"yn": "^3.0.0"
},
"dependencies": {
"diff": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/diff/-/diff-4.0.1.tgz",
"integrity": "sha512-s2+XdvhPCOF01LRQBC8hf4vhbVmI2CGS5aZnxLJlT5FtdhPCDFq80q++zK2KlrVorVDdL5BOGZ/VfLrVtYNF+Q==",
"dev": true
}
}
},
"tslib": {
"version": "1.10.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.10.0.tgz",
"integrity": "sha512-qOebF53frne81cf0S9B41ByenJ3/IuH8yJKngAX35CmiZySA0khhkovshKK+jGCaMnVomla7gVlIcc3EvKPbTQ==",
"dev": true
},
"tsutils": {
"version": "3.17.1",
"resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.17.1.tgz",
"integrity": "sha512-kzeQ5B8H3w60nFY2g8cJIuH7JDpsALXySGtwGJ0p2LSjLgay3NdIpqq5SoOBe46bKDW2iq25irHCr8wjomUS2g==",
"dev": true,
"requires": {
"tslib": "^1.8.1"
}
},
"tty-browserify": {
"version": "0.0.0",
"resolved": "https://registry.npmjs.org/tty-browserify/-/tty-browserify-0.0.0.tgz",
@ -13259,6 +13497,12 @@
"integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=",
"dev": true
},
"typescript": {
"version": "3.7.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-3.7.3.tgz",
"integrity": "sha512-Mcr/Qk7hXqFBXMN7p7Lusj1ktCBydylfQM/FZCk5glCNQJrCUKPkMHdo9R0MTFWsC/4kPFvDS0fDPvukfCkFsw==",
"dev": true
},
"uglify-js": {
"version": "3.6.3",
"resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.6.3.tgz",
@ -14667,6 +14911,12 @@
}
}
},
"word-wrap": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz",
"integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==",
"dev": true
},
"wordwrap": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz",
@ -14891,6 +15141,12 @@
}
}
}
},
"yn": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz",
"integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==",
"dev": true
}
}
}

View File

@ -7,14 +7,14 @@
"scripts": {
"start": "run-p js:watch css:watch",
"build": "run-p js:build css:build",
"lint": "eslint src/scripts",
"lint": "eslint src/scripts/**/*.ts",
"bundlesize": "bundlesize",
"cypress:run": "cypress run",
"cypress:open": "cypress open",
"cypress:ci": "cypress run --record --group $GITHUB_REF --ci-build-id $GITHUB_SHA",
"test": "run-s test:unit test:e2e",
"test:unit": "NODE_ENV=test mocha",
"test:unit:watch": "NODE_ENV=test mocha --watch --inspect=5556",
"test:unit": "TS_NODE_TRANSPILE_ONLY=true NODE_ENV=test mocha",
"test:unit:watch": "npm run test:unit -- --watch --inspect=5556",
"test:unit:coverage": "NODE_ENV=test nyc --reporter=lcov --reporter=text --reporter=text-summary mocha",
"test:e2e": "run-p --race start cypress:run",
"js:watch": "cross-env NODE_ENV=development node server.js",
@ -56,6 +56,12 @@
"@babel/core": "^7.6.4",
"@babel/preset-env": "^7.6.3",
"@babel/register": "^7.6.2",
"@types/chai": "^4.2.7",
"@types/mocha": "^5.2.7",
"@types/sinon": "^7.5.1",
"@types/sinon-chai": "^3.2.3",
"@typescript-eslint/eslint-plugin": "^2.11.0",
"@typescript-eslint/parser": "^2.11.0",
"autoprefixer": "^9.6.5",
"babel-loader": "^8.0.6",
"bundlesize": "^0.18.0",
@ -63,12 +69,12 @@
"cross-env": "^6.0.3",
"csso-cli": "^3.0.0",
"cypress": "3.6.0",
"eslint": "^6.6.0",
"eslint": "^6.8.0",
"eslint-config-airbnb-base": "^14.0.0",
"eslint-config-prettier": "^6.5.0",
"eslint-loader": "^3.0.2",
"eslint-plugin-compat": "3.3.0",
"eslint-plugin-cypress": "^2.7.0",
"eslint-plugin-cypress": "^2.8.1",
"eslint-plugin-import": "^2.18.2",
"eslint-plugin-prettier": "^3.1.1",
"eslint-plugin-sort-class-members": "^1.6.0",
@ -82,9 +88,12 @@
"npm-run-all": "^4.1.5",
"nyc": "^14.1.1",
"postcss-cli": "^6.1.3",
"prettier": "^1.18.2",
"prettier": "^1.19.1",
"sinon": "^7.5.0",
"sinon-chai": "^3.3.0",
"ts-loader": "^6.2.1",
"ts-node": "^8.5.4",
"typescript": "^3.7.3",
"webpack": "^4.41.2",
"webpack-cli": "^3.3.9",
"webpack-dev-middleware": "^3.7.2",
@ -92,7 +101,7 @@
},
"dependencies": {
"deepmerge": "^4.2.0",
"fuse.js": "^3.4.5",
"fuse.js": "^3.4.6",
"redux": "^4.0.4"
},
"npmName": "choices.js",

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -43,18 +43,12 @@
<meta name="theme-color" content="#ffffff" />
<!-- Ignore these -->
<link
rel="stylesheet"
href="../../assets/styles/base.min.css?version=6.0.3"
/>
<link rel="stylesheet" href="../../assets/styles/base.min.css" />
<!-- End ignore these -->
<!-- Choices includes -->
<link
rel="stylesheet"
href="../../assets/styles/choices.min.css?version=6.0.3"
/>
<script src="../../assets/scripts/choices.min.js?version=6.0.3"></script>
<link rel="stylesheet" href="../../assets/styles/choices.min.css" />
<script src="../../assets/scripts/choices.min.js"></script>
<!-- End Choices includes -->
</head>

View File

@ -43,18 +43,12 @@
<meta name="theme-color" content="#ffffff" />
<!-- Ignore these -->
<link
rel="stylesheet"
href="../../assets/styles/base.min.css?version=6.0.3"
/>
<link rel="stylesheet" href="../../assets/styles/base.min.css" />
<!-- End ignore these -->
<!-- Choices includes -->
<link
rel="stylesheet"
href="../../assets/styles/choices.min.css?version=6.0.3"
/>
<script src="../../assets/scripts/choices.min.js?version=6.0.3"></script>
<link rel="stylesheet" href="../../assets/styles/choices.min.css" />
<script src="../../assets/scripts/choices.min.js"></script>
<!-- End Choices includes -->
</head>

View File

@ -43,18 +43,12 @@
<meta name="theme-color" content="#ffffff" />
<!-- Ignore these -->
<link
rel="stylesheet"
href="../../assets/styles/base.min.css?version=6.0.3"
/>
<link rel="stylesheet" href="../../assets/styles/base.min.css" />
<!-- End ignore these -->
<!-- Choices includes -->
<link
rel="stylesheet"
href="../../assets/styles/choices.min.css?version=6.0.3"
/>
<script src="../../assets/scripts/choices.min.js?version=6.0.3"></script>
<link rel="stylesheet" href="../../assets/styles/choices.min.css" />
<script src="../../assets/scripts/choices.min.js"></script>
<!-- End Choices includes -->
</head>

View File

@ -1,58 +0,0 @@
/**
* @typedef {import('redux').Action} Action
* @typedef {import('../../../types/index').Choices.Choice} Choice
*/
import { ACTION_TYPES } from '../constants';
/**
* @argument {Choice} choice
* @returns {Action & Choice}
*/
export const addChoice = ({
value,
label,
id,
groupId,
disabled,
elementId,
customProperties,
placeholder,
keyCode,
}) => ({
type: ACTION_TYPES.ADD_CHOICE,
value,
label,
id,
groupId,
disabled,
elementId,
customProperties,
placeholder,
keyCode,
});
/**
* @argument {Choice[]} results
* @returns {Action & { results: Choice[] }}
*/
export const filterChoices = results => ({
type: ACTION_TYPES.FILTER_CHOICES,
results,
});
/**
* @argument {boolean} active
* @returns {Action & { active: boolean }}
*/
export const activateChoices = (active = true) => ({
type: ACTION_TYPES.ACTIVATE_CHOICES,
active,
});
/**
* @returns {Action}
*/
export const clearChoices = () => ({
type: ACTION_TYPES.CLEAR_CHOICES,
});

View File

@ -6,15 +6,15 @@ describe('actions/choices', () => {
it('returns ADD_CHOICE action', () => {
const value = 'test';
const label = 'test';
const id = 'test';
const groupId = 'test';
const id = 1;
const groupId = 1;
const disabled = false;
const elementId = 'test';
const customProperties = 'test';
const placeholder = 'test';
const elementId = 1;
const customProperties = { test: true };
const placeholder = true;
const keyCode = 10;
const expectedAction = {
const expectedAction: actions.AddChoiceAction = {
type: 'ADD_CHOICE',
value,
label,
@ -46,7 +46,7 @@ describe('actions/choices', () => {
describe('filterChoices action', () => {
it('returns FILTER_CHOICES action', () => {
const results = Array(10);
const expectedAction = {
const expectedAction: actions.FilterChoicesAction = {
type: 'FILTER_CHOICES',
results,
};
@ -58,7 +58,7 @@ describe('actions/choices', () => {
describe('activateChoices action', () => {
describe('not passing active parameter', () => {
it('returns ACTIVATE_CHOICES action', () => {
const expectedAction = {
const expectedAction: actions.ActivateChoicesAction = {
type: 'ACTIVATE_CHOICES',
active: true,
};
@ -70,7 +70,7 @@ describe('actions/choices', () => {
describe('passing active parameter', () => {
it('returns ACTIVATE_CHOICES action', () => {
const active = true;
const expectedAction = {
const expectedAction: actions.ActivateChoicesAction = {
type: 'ACTIVATE_CHOICES',
active,
};
@ -82,7 +82,7 @@ describe('actions/choices', () => {
describe('clearChoices action', () => {
it('returns CLEAR_CHOICES action', () => {
const expectedAction = {
const expectedAction: actions.ClearChoicesAction = {
type: 'CLEAR_CHOICES',
};

View File

@ -0,0 +1,73 @@
import { ACTION_TYPES } from '../constants';
import { Choice } from '../interfaces';
export interface AddChoiceAction {
type: typeof ACTION_TYPES.ADD_CHOICE;
id: number;
value: string;
label: string;
groupId: number;
disabled: boolean;
elementId: number;
customProperties: object;
placeholder: boolean;
keyCode: number;
}
export interface Result<T> {
item: T;
score: number;
}
export interface FilterChoicesAction {
type: typeof ACTION_TYPES.FILTER_CHOICES;
results: Result<Choice>[];
}
export interface ActivateChoicesAction {
type: typeof ACTION_TYPES.ACTIVATE_CHOICES;
active: boolean;
}
export interface ClearChoicesAction {
type: typeof ACTION_TYPES.CLEAR_CHOICES;
}
export const addChoice = ({
value,
label,
id,
groupId,
disabled,
elementId,
customProperties,
placeholder,
keyCode,
}): AddChoiceAction => ({
type: ACTION_TYPES.ADD_CHOICE,
value,
label,
id,
groupId,
disabled,
elementId,
customProperties,
placeholder,
keyCode,
});
export const filterChoices = (
results: Result<Choice>[],
): FilterChoicesAction => ({
type: ACTION_TYPES.FILTER_CHOICES,
results,
});
export const activateChoices = (active = true): ActivateChoicesAction => ({
type: ACTION_TYPES.ACTIVATE_CHOICES,
active,
});
export const clearChoices = (): ClearChoicesAction => ({
type: ACTION_TYPES.CLEAR_CHOICES,
});

View File

@ -1,18 +0,0 @@
import { ACTION_TYPES } from '../constants';
/**
* @typedef {import('redux').Action} Action
* @typedef {import('../../../types/index').Choices.Group} Group
*/
/**
* @param {Group} group
* @returns {Action & Group}
*/
export const addGroup = ({ value, id, active, disabled }) => ({
type: ACTION_TYPES.ADD_GROUP,
value,
id,
active,
disabled,
});

View File

@ -5,10 +5,11 @@ describe('actions/groups', () => {
describe('addGroup action', () => {
it('returns ADD_GROUP action', () => {
const value = 'test';
const id = 'test';
const id = 1;
const active = true;
const disabled = false;
const expectedAction = {
const expectedAction: actions.AddGroupAction = {
type: 'ADD_GROUP',
value,
id,

View File

@ -0,0 +1,27 @@
import { ACTION_TYPES } from '../constants';
export interface AddGroupAction {
type: typeof ACTION_TYPES.ADD_GROUP;
id: number;
value: string;
active: boolean;
disabled: boolean;
}
export const addGroup = ({
value,
id,
active,
disabled,
}: {
id: number;
value: string;
active: boolean;
disabled: boolean;
}): AddGroupAction => ({
type: ACTION_TYPES.ADD_GROUP,
value,
id,
active,
disabled,
});

View File

@ -1,53 +0,0 @@
import { ACTION_TYPES } from '../constants';
/**
* @typedef {import('redux').Action} Action
* @typedef {import('../../../types/index').Choices.Item} Item
*/
/**
* @param {Item} item
* @returns {Action & Item}
*/
export const addItem = ({
value,
label,
id,
choiceId,
groupId,
customProperties,
placeholder,
keyCode,
}) => ({
type: ACTION_TYPES.ADD_ITEM,
value,
label,
id,
choiceId,
groupId,
customProperties,
placeholder,
keyCode,
});
/**
* @param {number} id
* @param {number} choiceId
* @returns {Action & { id: number, choiceId: number }}
*/
export const removeItem = (id, choiceId) => ({
type: ACTION_TYPES.REMOVE_ITEM,
id,
choiceId,
});
/**
* @param {number} id
* @param {boolean} highlighted
* @returns {Action & { id: number, highlighted: boolean }}
*/
export const highlightItem = (id, highlighted) => ({
type: ACTION_TYPES.HIGHLIGHT_ITEM,
id,
highlighted,
});

View File

@ -6,14 +6,14 @@ describe('actions/items', () => {
it('returns ADD_ITEM action', () => {
const value = 'test';
const label = 'test';
const id = '1234';
const choiceId = '1234';
const groupId = 'test';
const id = 1;
const choiceId = 1;
const groupId = 1;
const customProperties = { test: true };
const placeholder = true;
const keyCode = 10;
const expectedAction = {
const expectedAction: actions.AddItemAction = {
type: 'ADD_ITEM',
value,
label,
@ -42,9 +42,10 @@ describe('actions/items', () => {
describe('removeItem action', () => {
it('returns REMOVE_ITEM action', () => {
const id = '1234';
const choiceId = '1';
const expectedAction = {
const id = 1;
const choiceId = 1;
const expectedAction: actions.RemoveItemAction = {
type: 'REMOVE_ITEM',
id,
choiceId,
@ -56,10 +57,10 @@ describe('actions/items', () => {
describe('highlightItem action', () => {
it('returns HIGHLIGHT_ITEM action', () => {
const id = '1234';
const id = 1;
const highlighted = true;
const expectedAction = {
const expectedAction: actions.HighlightItemAction = {
type: 'HIGHLIGHT_ITEM',
id,
highlighted,

View File

@ -0,0 +1,70 @@
import { ACTION_TYPES } from '../constants';
export interface AddItemAction {
type: typeof ACTION_TYPES.ADD_ITEM;
id: number;
value: string;
label: string;
choiceId: number;
groupId: number;
customProperties: object;
placeholder: boolean;
keyCode: number;
}
export interface RemoveItemAction {
type: typeof ACTION_TYPES.REMOVE_ITEM;
id: number;
choiceId: number;
}
export interface HighlightItemAction {
type: typeof ACTION_TYPES.HIGHLIGHT_ITEM;
id: number;
highlighted: boolean;
}
export const addItem = ({
value,
label,
id,
choiceId,
groupId,
customProperties,
placeholder,
keyCode,
}: {
id: number;
value: string;
label: string;
choiceId: number;
groupId: number;
customProperties: object;
placeholder: boolean;
keyCode: number;
}): AddItemAction => ({
type: ACTION_TYPES.ADD_ITEM,
value,
label,
id,
choiceId,
groupId,
customProperties,
placeholder,
keyCode,
});
export const removeItem = (id: number, choiceId: number): RemoveItemAction => ({
type: ACTION_TYPES.REMOVE_ITEM,
id,
choiceId,
});
export const highlightItem = (
id: number,
highlighted: boolean,
): HighlightItemAction => ({
type: ACTION_TYPES.HIGHLIGHT_ITEM,
id,
highlighted,
});

View File

@ -1,28 +0,0 @@
/**
* @typedef {import('redux').Action} Action
*/
/**
* @returns {Action}
*/
export const clearAll = () => ({
type: 'CLEAR_ALL',
});
/**
* @param {any} state
* @returns {Action & { state: object }}
*/
export const resetTo = state => ({
type: 'RESET_TO',
state,
});
/**
* @param {boolean} isLoading
* @returns {Action & { isLoading: boolean }}
*/
export const setIsLoading = isLoading => ({
type: 'SET_IS_LOADING',
isLoading,
});

View File

@ -1,10 +1,11 @@
import { expect } from 'chai';
import * as actions from './misc';
import { State } from '../interfaces';
describe('actions/misc', () => {
describe('clearAll action', () => {
it('returns CLEAR_ALL action', () => {
const expectedAction = {
const expectedAction: actions.ClearAllAction = {
type: 'CLEAR_ALL',
};
@ -14,8 +15,13 @@ describe('actions/misc', () => {
describe('resetTo action', () => {
it('returns RESET_TO action', () => {
const state = { test: true };
const expectedAction = {
const state: State = {
choices: [],
items: [],
groups: [],
loading: false,
};
const expectedAction: actions.ResetToAction = {
type: 'RESET_TO',
state,
};
@ -27,7 +33,7 @@ describe('actions/misc', () => {
describe('setIsLoading action', () => {
describe('setting loading state to true', () => {
it('returns expected action', () => {
const expectedAction = {
const expectedAction: actions.SetIsLoadingAction = {
type: 'SET_IS_LOADING',
isLoading: true,
};
@ -38,7 +44,7 @@ describe('actions/misc', () => {
describe('setting loading state to false', () => {
it('returns expected action', () => {
const expectedAction = {
const expectedAction: actions.SetIsLoadingAction = {
type: 'SET_IS_LOADING',
isLoading: false,
};

View File

@ -0,0 +1,30 @@
import { State } from '../interfaces';
import { ACTION_TYPES } from '../constants';
export interface ClearAllAction {
type: typeof ACTION_TYPES.CLEAR_ALL;
}
export interface ResetToAction {
type: typeof ACTION_TYPES.RESET_TO;
state: State;
}
export interface SetIsLoadingAction {
type: typeof ACTION_TYPES.SET_IS_LOADING;
isLoading: boolean;
}
export const clearAll = (): ClearAllAction => ({
type: ACTION_TYPES.CLEAR_ALL,
});
export const resetTo = (state: State): ResetToAction => ({
type: ACTION_TYPES.RESET_TO,
state,
});
export const setIsLoading = (isLoading: boolean): SetIsLoadingAction => ({
type: ACTION_TYPES.SET_IS_LOADING,
isLoading,
});

View File

@ -3,9 +3,12 @@ import { spy, stub } from 'sinon';
import sinonChai from 'sinon-chai';
import Choices from './choices';
import { EVENTS, ACTION_TYPES, DEFAULT_CONFIG, KEY_CODES } from './constants';
import { WrappedSelect, WrappedInput } from './components/index';
import { removeItem } from './actions/items';
import { Item, Choice, Group } from './interfaces';
import templates from './templates';
chai.use(sinonChai);
@ -28,12 +31,6 @@ describe('choices', () => {
instance = null;
});
const returnsInstance = () => {
it('returns this', () => {
expect(output).to.eql(instance);
});
};
describe('constructor', () => {
describe('config', () => {
describe('not passing config options', () => {
@ -88,7 +85,7 @@ describe('choices', () => {
`;
instance = new Choices('[data-choice]', {
renderSelectedChoices: 'test',
renderSelectedChoices: 'test' as any,
});
expect(instance.config.renderSelectedChoices).to.equal('auto');
@ -211,7 +208,7 @@ describe('choices', () => {
<input data-choice type="text" id="input-1" />
`;
instance = new Choices(document.querySelector('[data-choice]'));
instance = new Choices('[data-choice]');
expect(instance.passedElement).to.be.an.instanceOf(WrappedInput);
});
@ -223,7 +220,7 @@ describe('choices', () => {
<select data-choice id="select-1"></select>
`;
instance = new Choices(document.querySelector('[data-choice]'));
instance = new Choices('[data-choice]');
expect(instance.passedElement).to.be.an.instanceOf(WrappedSelect);
});
@ -386,8 +383,8 @@ describe('choices', () => {
expect(clearStoreSpy.called).to.equal(true);
});
it('nullifys templates config', () => {
expect(instance._templates).to.equal(null);
it('restes templates config', () => {
expect(instance._templates).to.deep.equal(templates);
});
it('resets initialise flag', () => {
@ -423,7 +420,9 @@ describe('choices', () => {
output = instance.enable();
});
returnsInstance(output);
it('returns this', () => {
expect(output).to.eql(instance);
});
it('returns early', () => {
expect(passedElementEnableSpy.called).to.equal(false);
@ -481,7 +480,9 @@ describe('choices', () => {
output = instance.disable();
});
returnsInstance(output);
it('returns this', () => {
expect(output).to.eql(instance);
});
it('returns early', () => {
expect(removeEventListenersSpy.called).to.equal(false);
@ -638,7 +639,9 @@ describe('choices', () => {
output = instance.hideDropdown();
});
returnsInstance(output);
it('returns this', () => {
expect(output).to.eql(instance);
});
it('returns early', () => {
expect(containerOuterCloseSpy.called).to.equal(false);
@ -735,7 +738,9 @@ describe('choices', () => {
output = instance.highlightItem();
});
returnsInstance(output);
it('returns this', () => {
expect(output).to.eql(instance);
});
it('returns early', () => {
expect(passedElementTriggerEventStub.called).to.equal(false);
@ -745,7 +750,7 @@ describe('choices', () => {
});
describe('item passed', () => {
const item = {
const item: Item = {
id: 1234,
value: 'Test',
label: 'Test',
@ -756,7 +761,9 @@ describe('choices', () => {
output = instance.highlightItem(item, true);
});
returnsInstance(output);
it('returns this', () => {
expect(output).to.eql(instance);
});
it('dispatches highlightItem action with correct arguments', () => {
expect(storeDispatchSpy.called).to.equal(true);
@ -817,7 +824,9 @@ describe('choices', () => {
expect(passedElementTriggerEventStub.called).to.equal(false);
});
returnsInstance(output);
it('returns this', () => {
expect(output).to.eql(instance);
});
});
});
});
@ -850,7 +859,9 @@ describe('choices', () => {
output = instance.unhighlightItem();
});
returnsInstance(output);
it('returns this', () => {
expect(output).to.eql(instance);
});
it('returns early', () => {
expect(passedElementTriggerEventStub.called).to.equal(false);
@ -860,7 +871,7 @@ describe('choices', () => {
});
describe('item passed', () => {
const item = {
const item: Item = {
id: 1234,
value: 'Test',
label: 'Test',
@ -871,7 +882,9 @@ describe('choices', () => {
output = instance.unhighlightItem(item, true);
});
returnsInstance(output);
it('returns this', () => {
expect(output).to.eql(instance);
});
it('dispatches highlightItem action with correct arguments', () => {
expect(storeDispatchSpy.called).to.equal(true);
@ -932,7 +945,9 @@ describe('choices', () => {
expect(passedElementTriggerEventStub.called).to.equal(false);
});
returnsInstance(output);
it('returns this', () => {
expect(output).to.eql(instance);
});
});
});
});
@ -966,7 +981,9 @@ describe('choices', () => {
storeGetItemsStub.reset();
});
returnsInstance(output);
it('returns this', () => {
expect(output).to.eql(instance);
});
it('highlights each item in store', () => {
expect(highlightItemStub.callCount).to.equal(items.length);
@ -1004,7 +1021,9 @@ describe('choices', () => {
storeGetItemsStub.reset();
});
returnsInstance(output);
it('returns this', () => {
expect(output).to.eql(instance);
});
it('unhighlights each item in store', () => {
expect(unhighlightItemStub.callCount).to.equal(items.length);
@ -1027,7 +1046,9 @@ describe('choices', () => {
instance._store.dispatch.reset();
});
returnsInstance(output);
it('returns this', () => {
expect(output).to.eql(instance);
});
it('dispatches clearChoices action', () => {
expect(storeDispatchStub.lastCall.args[0]).to.eql({
@ -1050,7 +1071,9 @@ describe('choices', () => {
instance._store.dispatch.reset();
});
returnsInstance(output);
it('returns this', () => {
expect(output).to.eql(instance);
});
it('dispatches clearAll action', () => {
expect(storeDispatchStub.lastCall.args[0]).to.eql({
@ -1075,7 +1098,9 @@ describe('choices', () => {
instance._store.dispatch.reset();
});
returnsInstance(output);
it('returns this', () => {
expect(output).to.eql(instance);
});
describe('text element', () => {
beforeEach(() => {
@ -1164,14 +1189,14 @@ describe('choices', () => {
const handleLoadingStateSpy = spy(choice, '_handleLoadingState');
let fetcherCalled = false;
const fetcher = async inst => {
const fetcher = async (inst): Promise<Choice[]> => {
expect(inst).to.eq(choice);
fetcherCalled = true;
await new Promise(resolve => setTimeout(resolve, 800));
return [
{ label: 'l1', value: 'v1', customProperties: 'prop1' },
{ label: 'l2', value: 'v2', customProperties: 'prop2' },
{ label: 'l1', value: 'v1', customProperties: { prop1: true } },
{ label: 'l2', value: 'v2', customProperties: { prop2: false } },
];
};
expect(choice._store.choices.length).to.equal(0);
@ -1182,7 +1207,9 @@ describe('choices', () => {
expect(handleLoadingStateSpy.callCount).to.equal(2);
expect(choice._store.choices[1].value).to.equal('v2');
expect(choice._store.choices[1].label).to.equal('l2');
expect(choice._store.choices[1].customProperties).to.equal('prop2');
expect(choice._store.choices[1].customProperties).to.deep.equal({
prop2: false,
});
});
});
});
@ -1211,7 +1238,9 @@ describe('choices', () => {
output = instance.setValue(values);
});
returnsInstance(output);
it('returns this', () => {
expect(output).to.eql(instance);
});
it('returns early', () => {
expect(setChoiceOrItemStub.called).to.equal(false);
@ -1224,7 +1253,9 @@ describe('choices', () => {
output = instance.setValue(values);
});
returnsInstance(output);
it('returns this', () => {
expect(output).to.eql(instance);
});
it('sets each value', () => {
expect(setChoiceOrItemStub.callCount).to.equal(2);
@ -1252,7 +1283,9 @@ describe('choices', () => {
output = instance.setChoiceByValue([]);
});
returnsInstance(output);
it('returns this', () => {
expect(output).to.eql(instance);
});
it('returns early', () => {
expect(findAndSelectChoiceByValueStub.called).to.equal(false);
@ -1272,7 +1305,9 @@ describe('choices', () => {
output = instance.setChoiceByValue(value);
});
returnsInstance(output);
it('returns this', () => {
expect(output).to.eql(instance);
});
it('sets each choice with same value', () => {
expect(findAndSelectChoiceByValueStub.called).to.equal(true);
@ -1289,7 +1324,9 @@ describe('choices', () => {
output = instance.setChoiceByValue(values);
});
returnsInstance(output);
it('returns this', () => {
expect(output).to.eql(instance);
});
it('sets each choice with same value', () => {
expect(findAndSelectChoiceByValueStub.callCount).to.equal(2);
@ -1509,7 +1546,9 @@ describe('choices', () => {
output = instance.removeHighlightedItems();
});
returnsInstance(output);
it('returns this', () => {
expect(output).to.eql(instance);
});
it('removes each highlighted item in store', () => {
expect(removeItemStub.callCount).to.equal(2);
@ -1521,7 +1560,9 @@ describe('choices', () => {
output = instance.removeHighlightedItems(true);
});
returnsInstance(output);
it('returns this', () => {
expect(output).to.eql(instance);
});
it('triggers event with item value', () => {
expect(triggerChangeStub.callCount).to.equal(2);
@ -1538,19 +1579,23 @@ describe('choices', () => {
let containerOuterRemoveLoadingStateStub;
const value = 'value';
const label = 'label';
const choices = [
const choices: Choice[] = [
{
id: 1,
value: '1',
label: 'Test 1',
selected: false,
disabled: false,
},
{
id: 2,
value: '2',
label: 'Test 2',
selected: false,
disabled: true,
},
];
const groups = [
const groups: Group[] = [
{
...choices[0],
choices,
@ -1634,10 +1679,10 @@ describe('choices', () => {
expect(call.args[0]).to.eql({
value: choices[index][value],
label: choices[index][label],
isSelected: choices[index].selected,
isDisabled: choices[index].disabled,
isSelected: !!choices[index].selected,
isDisabled: !!choices[index].disabled,
customProperties: choices[index].customProperties,
placeholder: choices[index].placeholder,
placeholder: !!choices[index].placeholder,
});
});
});
@ -1679,7 +1724,7 @@ describe('choices', () => {
describe('private methods', () => {
describe('_createGroupsFragment', () => {
let _createChoicesFragmentStub;
const choices = [
const choices: Choice[] = [
{
id: 1,
selected: true,
@ -1703,7 +1748,7 @@ describe('choices', () => {
},
];
const groups = [
const groups: Group[] = [
{
id: 2,
value: 'Group 2',
@ -1925,12 +1970,12 @@ describe('choices', () => {
});
describe('when a placeholder option is not defined', () => {
it('returns false', () => {
it('returns null', () => {
instance._isSelectElement = true;
instance.passedElement.placeholderOption = undefined;
const value = instance._generatePlaceholderValue();
expect(value).to.equal(false);
expect(value).to.equal(null);
});
});
});
@ -1970,7 +2015,7 @@ describe('choices', () => {
});
describe('when the placeholder attribute is not defined on the passed element', () => {
it('returns false', () => {
it('returns null', () => {
instance._isSelectElement = false;
instance.config.placeholder = true;
instance.config.placeholderValue = undefined;
@ -1981,32 +2026,25 @@ describe('choices', () => {
};
const value = instance._generatePlaceholderValue();
expect(value).to.equal(false);
expect(value).to.equal(null);
});
});
});
});
describe('when the placeholder config option is set to false', () => {
it('returns false', () => {
it('returns null', () => {
instance._isSelectElement = false;
instance.config.placeholder = false;
const value = instance._generatePlaceholderValue();
expect(value).to.equal(false);
expect(value).to.equal(null);
});
});
});
});
describe('_getTemplate', () => {
describe('when not passing a template key', () => {
it('returns null', () => {
output = instance._getTemplate();
expect(output).to.equal(null);
});
});
describe('when passing a template key', () => {
it('returns the generated template for the given template key', () => {
const templateKey = 'test';
@ -2028,130 +2066,96 @@ describe('choices', () => {
});
describe('_onKeyDown', () => {
let activeItems;
let hasItems;
let hasActiveDropdown;
let hasFocussedInput;
beforeEach(() => {
instance.showDropdown = stub();
instance._onAKey = stub();
instance._onSelectKey = stub();
instance._onEnterKey = stub();
instance._onEscapeKey = stub();
instance._onDirectionKey = stub();
instance._onDeleteKey = stub();
({ activeItems } = instance._store);
hasItems = instance.itemList.hasChildren();
hasActiveDropdown = instance.dropdown.isActive;
hasFocussedInput = instance.input.isFocussed;
});
const scenarios = [
{
keyCode: KEY_CODES.BACK_KEY,
expectedFunctionCall: '_onDeleteKey',
},
{
keyCode: KEY_CODES.DELETE_KEY,
expectedFunctionCall: '_onDeleteKey',
},
{
keyCode: KEY_CODES.A_KEY,
expectedFunctionCall: '_onAKey',
},
{
keyCode: KEY_CODES.ENTER_KEY,
expectedFunctionCall: '_onEnterKey',
},
{
keyCode: KEY_CODES.UP_KEY,
expectedFunctionCall: '_onDirectionKey',
},
{
keyCode: KEY_CODES.DOWN_KEY,
expectedFunctionCall: '_onDirectionKey',
},
{
keyCode: KEY_CODES.DOWN_KEY,
expectedFunctionCall: '_onDirectionKey',
},
{
keyCode: KEY_CODES.ESC_KEY,
expectedFunctionCall: '_onEscapeKey',
},
describe('direction key', () => {
const keyCodes = [
KEY_CODES.UP_KEY,
KEY_CODES.DOWN_KEY,
KEY_CODES.PAGE_UP_KEY,
KEY_CODES.PAGE_DOWN_KEY,
];
describe('when called with a keydown event', () => {
scenarios.forEach(({ keyCode, expectedFunctionCall }) => {
describe(`when the keyCode is ${keyCode}`, () => {
it(`calls ${expectedFunctionCall} with the expected arguments`, () => {
const mockEvent = {
keyCodes.forEach(keyCode => {
it(`calls _onDirectionKey with the expected arguments`, () => {
const event = {
keyCode,
};
instance._onKeyDown(mockEvent);
instance._onKeyDown(event);
expect(instance[expectedFunctionCall]).to.have.been.calledWith({
event: mockEvent,
activeItems: instance._store.activeItems,
hasActiveDropdown: instance.dropdown.isActive,
hasFocusedInput: instance.input.isFocussed,
hasItems: instance.itemList.hasChildren(),
});
expect(instance._onDirectionKey).to.have.been.calledWith(
event,
hasActiveDropdown,
);
});
});
});
describe('select input', () => {
describe('when the dropdown is not active', () => {
describe('when the key was alpha-numeric', () => {
beforeEach(() => {
instance._isTextElement = false;
instance.dropdown.isActive = false;
});
it('shows the dropdown', () => {
instance._onKeyDown({
describe('select key', () => {
it(`calls _onSelectKey with the expected arguments`, () => {
const event = {
keyCode: KEY_CODES.A_KEY,
});
};
expect(instance.showDropdown).to.have.been.calledWith();
});
instance._onKeyDown(event);
describe('when the input is not focussed', () => {
beforeEach(() => {
instance.input.isFocussed = false;
});
it('updates the input value with the character corresponding to the key code', () => {
instance._onKeyDown({
keyCode: KEY_CODES.A_KEY,
});
expect(instance.input.value).to.contain('a');
expect(instance._onSelectKey).to.have.been.calledWith(
event,
hasItems,
);
});
});
describe('when the input is focussed', () => {
beforeEach(() => {
instance.input.isFocussed = true;
});
describe('enter key', () => {
it(`calls _onEnterKey with the expected arguments`, () => {
const event = {
keyCode: KEY_CODES.ENTER_KEY,
};
it('does not update the input value', () => {
instance._onKeyDown({
keyCode: KEY_CODES.A_KEY,
});
instance._onKeyDown(event);
expect(instance.input.value).to.not.contain('a');
});
expect(instance._onEnterKey).to.have.been.calledWith(
event,
activeItems,
hasActiveDropdown,
);
});
});
describe('when the input was not alpha-numeric', () => {
beforeEach(() => {
instance._isTextElement = false;
instance.dropdown.isActive = false;
});
describe('delete key', () => {
const keyCodes = [KEY_CODES.DELETE_KEY, KEY_CODES.BACK_KEY];
it('does not show the dropdown', () => {
instance._onKeyDown({
keyCode: KEY_CODES.DELETE_KEY,
});
keyCodes.forEach(keyCode => {
it(`calls _onDeleteKey with the expected arguments`, () => {
const event = {
keyCode,
};
expect(instance.showDropdown).to.not.have.been.called;
});
});
instance._onKeyDown(event);
expect(instance._onDeleteKey).to.have.been.calledWith(
event,
activeItems,
hasFocussedInput,
);
});
});
});

File diff suppressed because it is too large Load Diff

View File

@ -13,7 +13,7 @@ describe('components/container', () => {
document.body.appendChild(element);
instance = new Container({
element: document.getElementById('container'),
element: document.getElementById('container') as HTMLElement,
classNames: DEFAULT_CLASSNAMES,
position: 'auto',
type: 'text',
@ -383,7 +383,7 @@ describe('components/container', () => {
});
afterEach(() => {
document.getElementById('wrap-test').remove();
document.getElementById('wrap-test')!.remove();
});
it('wraps passed element inside element', () => {
@ -406,7 +406,7 @@ describe('components/container', () => {
});
afterEach(() => {
document.body.removeChild(document.getElementById('unwrap-test'));
document.body.removeChild(document.getElementById('unwrap-test') as Node);
});
it('moves wrapped element outside of element', () => {

View File

@ -1,20 +1,29 @@
import { wrap } from '../lib/utils';
import { SELECT_ONE_TYPE } from '../constants';
import { PassedElement, ClassNames, Options } from '../interfaces';
/**
* @typedef {import('../../../types/index').Choices.passedElement} passedElement
* @typedef {import('../../../types/index').Choices.ClassNames} ClassNames
*/
export default class Container {
/**
* @param {{
* element: HTMLElement,
* type: passedElement['type'],
* classNames: ClassNames,
* position
* }} args
*/
constructor({ element, type, classNames, position }) {
element: HTMLElement;
type: PassedElement['type'];
classNames: ClassNames;
position: Options['position'];
isOpen: boolean;
isFlipped: boolean;
isFocussed: boolean;
isDisabled: boolean;
isLoading: boolean;
constructor({
element,
type,
classNames,
position,
}: {
element: HTMLElement;
type: PassedElement['type'];
classNames: ClassNames;
position: Options['position'];
}) {
this.element = element;
this.classNames = classNames;
this.type = type;
@ -28,12 +37,12 @@ export default class Container {
this._onBlur = this._onBlur.bind(this);
}
addEventListeners() {
addEventListeners(): void {
this.element.addEventListener('focus', this._onFocus);
this.element.addEventListener('blur', this._onBlur);
}
removeEventListeners() {
removeEventListeners(): void {
this.element.removeEventListener('focus', this._onFocus);
this.element.removeEventListener('blur', this._onBlur);
}
@ -41,10 +50,8 @@ export default class Container {
/**
* Determine whether container should be flipped based on passed
* dropdown position
* @param {number} dropdownPos
* @returns {boolean}
*/
shouldFlip(dropdownPos) {
shouldFlip(dropdownPos: number): boolean {
if (typeof dropdownPos !== 'number') {
return false;
}
@ -62,21 +69,15 @@ export default class Container {
return shouldFlip;
}
/**
* @param {string} activeDescendantID
*/
setActiveDescendant(activeDescendantID) {
setActiveDescendant(activeDescendantID: string): void {
this.element.setAttribute('aria-activedescendant', activeDescendantID);
}
removeActiveDescendant() {
removeActiveDescendant(): void {
this.element.removeAttribute('aria-activedescendant');
}
/**
* @param {number} dropdownPos
*/
open(dropdownPos) {
open(dropdownPos: number): void {
this.element.classList.add(this.classNames.openState);
this.element.setAttribute('aria-expanded', 'true');
this.isOpen = true;
@ -87,7 +88,7 @@ export default class Container {
}
}
close() {
close(): void {
this.element.classList.remove(this.classNames.openState);
this.element.setAttribute('aria-expanded', 'false');
this.removeActiveDescendant();
@ -100,21 +101,21 @@ export default class Container {
}
}
focus() {
focus(): void {
if (!this.isFocussed) {
this.element.focus();
}
}
addFocusState() {
addFocusState(): void {
this.element.classList.add(this.classNames.focusState);
}
removeFocusState() {
removeFocusState(): void {
this.element.classList.remove(this.classNames.focusState);
}
enable() {
enable(): void {
this.element.classList.remove(this.classNames.disabledState);
this.element.removeAttribute('aria-disabled');
if (this.type === SELECT_ONE_TYPE) {
@ -123,7 +124,7 @@ export default class Container {
this.isDisabled = false;
}
disable() {
disable(): void {
this.element.classList.add(this.classNames.disabledState);
this.element.setAttribute('aria-disabled', 'true');
if (this.type === SELECT_ONE_TYPE) {
@ -132,40 +133,36 @@ export default class Container {
this.isDisabled = true;
}
/**
* @param {HTMLElement} element
*/
wrap(element) {
wrap(element: HTMLSelectElement | HTMLInputElement | HTMLElement): void {
wrap(element, this.element);
}
/**
* @param {Element} element
*/
unwrap(element) {
unwrap(element: HTMLElement): void {
if (this.element.parentNode) {
// Move passed element outside this element
this.element.parentNode.insertBefore(element, this.element);
// Remove this element
this.element.parentNode.removeChild(this.element);
}
}
addLoadingState() {
addLoadingState(): void {
this.element.classList.add(this.classNames.loadingState);
this.element.setAttribute('aria-busy', 'true');
this.isLoading = true;
}
removeLoadingState() {
removeLoadingState(): void {
this.element.classList.remove(this.classNames.loadingState);
this.element.removeAttribute('aria-busy');
this.isLoading = false;
}
_onFocus() {
_onFocus(): void {
this.isFocussed = true;
}
_onBlur() {
_onBlur(): void {
this.isFocussed = false;
}
}

View File

@ -1,17 +1,20 @@
/**
* @typedef {import('../../../types/index').Choices.passedElement} passedElement
* @typedef {import('../../../types/index').Choices.ClassNames} ClassNames
*/
import { PassedElement, ClassNames } from '../interfaces';
export default class Dropdown {
/**
* @param {{
* element: HTMLElement,
* type: passedElement['type'],
* classNames: ClassNames,
* }} args
*/
constructor({ element, type, classNames }) {
element: HTMLElement;
type: PassedElement['type'];
classNames: ClassNames;
isActive: boolean;
constructor({
element,
type,
classNames,
}: {
element: HTMLElement;
type: PassedElement['type'];
classNames: ClassNames;
}) {
this.element = element;
this.classNames = classNames;
this.type = type;
@ -20,26 +23,19 @@ export default class Dropdown {
/**
* Bottom position of dropdown in viewport coordinates
* @returns {number} Vertical position
*/
get distanceFromTopWindow() {
get distanceFromTopWindow(): number {
return this.element.getBoundingClientRect().bottom;
}
/**
* Find element that matches passed selector
* @param {string} selector
* @returns {HTMLElement | null}
*/
getChild(selector) {
getChild(selector: string): HTMLElement | null {
return this.element.querySelector(selector);
}
/**
* Show dropdown to user by adding active state class
* @returns {this}
*/
show() {
show(): this {
this.element.classList.add(this.classNames.activeState);
this.element.setAttribute('aria-expanded', 'true');
this.isActive = true;
@ -49,9 +45,8 @@ export default class Dropdown {
/**
* Hide dropdown from user
* @returns {this}
*/
hide() {
hide(): this {
this.element.classList.remove(this.classNames.activeState);
this.element.setAttribute('aria-expanded', 'false');
this.isActive = false;

View File

@ -13,7 +13,6 @@ describe('components/input', () => {
element: choicesElement,
type: 'text',
classNames: DEFAULT_CLASSNAMES,
placeholderValue: null,
preventPaste: false,
});
});
@ -49,7 +48,7 @@ describe('components/input', () => {
expect(['input', 'paste', 'focus', 'blur']).to.have.members(
Array.from(
{ length: addEventListenerStub.callCount },
(v, i) => addEventListenerStub.getCall(i).args[0],
(_, i) => addEventListenerStub.getCall(i).args[0],
),
);
});

View File

@ -1,27 +1,32 @@
import { sanitise } from '../lib/utils';
import { SELECT_ONE_TYPE } from '../constants';
/**
* @typedef {import('../../../types/index').Choices.passedElement} passedElement
* @typedef {import('../../../types/index').Choices.ClassNames} ClassNames
*/
import { PassedElement, ClassNames } from '../interfaces';
export default class Input {
/**
* @param {{
* element: HTMLInputElement,
* type: passedElement['type'],
* classNames: ClassNames,
* preventPaste: boolean
* }} args
*/
constructor({ element, type, classNames, preventPaste }) {
element: HTMLInputElement;
type: PassedElement['type'];
classNames: ClassNames;
preventPaste: boolean;
isFocussed: boolean;
isDisabled: boolean;
constructor({
element,
type,
classNames,
preventPaste,
}: {
element: HTMLInputElement;
type: PassedElement['type'];
classNames: ClassNames;
preventPaste: boolean;
}) {
this.element = element;
this.type = type;
this.classNames = classNames;
this.preventPaste = preventPaste;
this.isFocussed = this.element === document.activeElement;
this.isFocussed = this.element.isEqualNode(document.activeElement);
this.isDisabled = element.disabled;
this._onPaste = this._onPaste.bind(this);
this._onInput = this._onInput.bind(this);
@ -29,28 +34,19 @@ export default class Input {
this._onBlur = this._onBlur.bind(this);
}
/**
* @param {string} placeholder
*/
set placeholder(placeholder) {
set placeholder(placeholder: string) {
this.element.placeholder = placeholder;
}
/**
* @returns {string}
*/
get value() {
get value(): string {
return sanitise(this.element.value);
}
/**
* @param {string} value
*/
set value(value) {
set value(value: string) {
this.element.value = value;
}
addEventListeners() {
addEventListeners(): void {
this.element.addEventListener('paste', this._onPaste);
this.element.addEventListener('input', this._onInput, {
passive: true,
@ -63,47 +59,36 @@ export default class Input {
});
}
removeEventListeners() {
this.element.removeEventListener('input', this._onInput, {
passive: true,
});
removeEventListeners(): void {
this.element.removeEventListener('input', this._onInput);
this.element.removeEventListener('paste', this._onPaste);
this.element.removeEventListener('focus', this._onFocus, {
passive: true,
});
this.element.removeEventListener('blur', this._onBlur, {
passive: true,
});
this.element.removeEventListener('focus', this._onFocus);
this.element.removeEventListener('blur', this._onBlur);
}
enable() {
enable(): void {
this.element.removeAttribute('disabled');
this.isDisabled = false;
}
disable() {
disable(): void {
this.element.setAttribute('disabled', '');
this.isDisabled = true;
}
focus() {
focus(): void {
if (!this.isFocussed) {
this.element.focus();
}
}
blur() {
blur(): void {
if (this.isFocussed) {
this.element.blur();
}
}
/**
* Set value of input to blank
* @param {boolean} setWidth
* @returns {this}
*/
clear(setWidth = true) {
clear(setWidth = true): this {
if (this.element.value) {
this.element.value = '';
}
@ -119,44 +104,38 @@ export default class Input {
* Set the correct input width based on placeholder
* value or input value
*/
setWidth() {
setWidth(): void {
// Resize input to contents or placeholder
const { style, value, placeholder } = this.element;
style.minWidth = `${placeholder.length + 1}ch`;
style.width = `${value.length + 1}ch`;
}
/**
* @param {string} activeDescendantID
*/
setActiveDescendant(activeDescendantID) {
setActiveDescendant(activeDescendantID: string): void {
this.element.setAttribute('aria-activedescendant', activeDescendantID);
}
removeActiveDescendant() {
removeActiveDescendant(): void {
this.element.removeAttribute('aria-activedescendant');
}
_onInput() {
_onInput(): void {
if (this.type !== SELECT_ONE_TYPE) {
this.setWidth();
}
}
/**
* @param {Event} event
*/
_onPaste(event) {
_onPaste(event: ClipboardEvent): void {
if (this.preventPaste) {
event.preventDefault();
}
}
_onFocus() {
_onFocus(): void {
this.isFocussed = true;
}
_onBlur() {
_onBlur(): void {
this.isFocussed = false;
}
}

View File

@ -1,53 +1,37 @@
import { SCROLLING_SPEED } from '../constants';
/**
* @typedef {import('../../../types/index').Choices.Choice} Choice
*/
export default class List {
/**
* @param {{ element: HTMLElement }} args
*/
constructor({ element }) {
element: HTMLElement;
scrollPos: number;
height: number;
constructor({ element }: { element: HTMLElement }) {
this.element = element;
this.scrollPos = this.element.scrollTop;
this.height = this.element.offsetHeight;
}
clear() {
clear(): void {
this.element.innerHTML = '';
}
/**
* @param {Element | DocumentFragment} node
*/
append(node) {
append(node: Element | DocumentFragment): void {
this.element.appendChild(node);
}
/**
* @param {string} selector
* @returns {Element | null}
*/
getChild(selector) {
getChild(selector: string): HTMLElement | null {
return this.element.querySelector(selector);
}
/**
* @returns {boolean}
*/
hasChildren() {
hasChildren(): boolean {
return this.element.hasChildNodes();
}
scrollToTop() {
scrollToTop(): void {
this.element.scrollTop = 0;
}
/**
* @param {Element} element
* @param {1 | -1} direction
*/
scrollToChildElement(element, direction) {
scrollToChildElement(element: HTMLElement, direction: 1 | -1): void {
if (!element) {
return;
}
@ -71,35 +55,21 @@ export default class List {
});
}
/**
* @param {number} scrollPos
* @param {number} strength
* @param {number} destination
*/
_scrollDown(scrollPos, strength, destination) {
_scrollDown(scrollPos: number, strength: number, destination: number): void {
const easing = (destination - scrollPos) / strength;
const distance = easing > 1 ? easing : 1;
this.element.scrollTop = scrollPos + distance;
}
/**
* @param {number} scrollPos
* @param {number} strength
* @param {number} destination
*/
_scrollUp(scrollPos, strength, destination) {
_scrollUp(scrollPos: number, strength: number, destination: number): void {
const easing = (scrollPos - destination) / strength;
const distance = easing > 1 ? easing : 1;
this.element.scrollTop = scrollPos - distance;
}
/**
* @param {*} destination
* @param {*} direction
*/
_animateScroll(destination, direction) {
_animateScroll(destination: number, direction: number): void {
const strength = SCROLLING_SPEED;
const choiceListScrollTop = this.element.scrollTop;
let continueAnimation = false;

View File

@ -1,17 +1,11 @@
import { dispatchEvent } from '../lib/utils';
/**
* @typedef {import('../../../types/index').Choices.passedElement} passedElement
* @typedef {import('../../../types/index').Choices.ClassNames} ClassNames
*/
import { ClassNames, EventMap } from '../interfaces';
export default class WrappedElement {
/**
* @param {{
* element: HTMLInputElement | HTMLSelectElement,
* classNames: ClassNames,
* }} args
*/
element: HTMLInputElement | HTMLSelectElement;
classNames: ClassNames;
isDisabled: boolean;
constructor({ element, classNames }) {
this.element = element;
this.classNames = classNames;
@ -26,24 +20,24 @@ export default class WrappedElement {
this.isDisabled = false;
}
get isActive() {
get isActive(): boolean {
return this.element.dataset.choice === 'active';
}
get dir() {
get dir(): string {
return this.element.dir;
}
get value() {
get value(): string {
return this.element.value;
}
set value(value) {
set value(value: string) {
// you must define setter here otherwise it will be readonly property
this.element.value = value;
}
conceal() {
conceal(): void {
// Hide passed input
this.element.classList.add(this.classNames.input);
this.element.hidden = true;
@ -61,7 +55,7 @@ export default class WrappedElement {
this.element.setAttribute('data-choice', 'active');
}
reveal() {
reveal(): void {
// Reinstate passed element
this.element.classList.remove(this.classNames.input);
this.element.hidden = false;
@ -83,19 +77,19 @@ export default class WrappedElement {
this.element.value = this.element.value; // eslint-disable-line no-self-assign
}
enable() {
enable(): void {
this.element.removeAttribute('disabled');
this.element.disabled = false;
this.isDisabled = false;
}
disable() {
disable(): void {
this.element.setAttribute('disabled', '');
this.element.disabled = true;
this.isDisabled = true;
}
triggerEvent(eventType, data) {
triggerEvent<K extends keyof EventMap>(eventType: K, data?: object): void {
dispatchEvent(this.element, eventType, data);
}
}

View File

@ -1,38 +0,0 @@
import WrappedElement from './wrapped-element';
/**
* @typedef {import('../../../types/index').Choices.ClassNames} ClassNames
* @typedef {import('../../../types/index').Choices.Item} Item
*/
export default class WrappedInput extends WrappedElement {
/**
* @param {{
* element: HTMLInputElement,
* classNames: ClassNames,
* delimiter: string
* }} args
*/
constructor({ element, classNames, delimiter }) {
super({ element, classNames });
this.delimiter = delimiter;
}
/**
* @returns {string}
*/
get value() {
return this.element.value;
}
/**
* @param {Item[]} items
*/
set value(items) {
const itemValues = items.map(({ value }) => value);
const joinedValues = itemValues.join(this.delimiter);
this.element.setAttribute('value', joinedValues);
this.element.value = joinedValues;
}
}

View File

@ -34,10 +34,12 @@ describe('components/wrappedInput', () => {
});
describe('inherited methods', () => {
['conceal', 'reveal', 'enable', 'disable'].forEach(method => {
const methods: string[] = ['conceal', 'reveal', 'enable', 'disable'];
methods.forEach(method => {
describe(method, () => {
beforeEach(() => {
stub(WrappedElement.prototype, method);
stub(WrappedElement.prototype, method as keyof WrappedElement);
});
afterEach(() => {
@ -54,27 +56,11 @@ describe('components/wrappedInput', () => {
});
describe('value setter', () => {
const data = [
{
id: 'ID 1',
value: 'Value 1',
},
{
id: 'ID 2',
value: 'Value 2',
},
{
id: 'ID 3',
value: 'Value 3',
},
];
it('sets delimited value of element based on passed data', () => {
it('sets the value of the input to the given value', () => {
const newValue = 'Value 1, Value 2, Value 3';
expect(instance.element.value).to.equal('');
instance.value = data;
expect(instance.value).to.equal(
`Value 1${delimiter}Value 2${delimiter}Value 3`,
);
instance.value = newValue;
expect(instance.value).to.equal(newValue);
});
});
});

View File

@ -0,0 +1,29 @@
import WrappedElement from './wrapped-element';
import { ClassNames } from '../interfaces';
export default class WrappedInput extends WrappedElement {
element: HTMLInputElement;
delimiter: string;
constructor({
element,
classNames,
delimiter,
}: {
element: HTMLInputElement;
classNames: ClassNames;
delimiter: string;
}) {
super({ element, classNames });
this.delimiter = delimiter;
}
get value(): string {
return this.element.value;
}
set value(value: string) {
this.element.setAttribute('value', value);
this.element.value = value;
}
}

View File

@ -32,7 +32,7 @@ describe('components/wrappedSelect', () => {
document.body.appendChild(element);
instance = new WrappedSelect({
element: document.getElementById('target'),
element: document.getElementById('target') as HTMLSelectElement,
classNames: DEFAULT_CLASSNAMES,
template: spy(Templates.option),
});
@ -54,9 +54,11 @@ describe('components/wrappedSelect', () => {
});
describe('inherited methods', () => {
['conceal', 'reveal', 'enable', 'disable'].forEach(method => {
const methods: string[] = ['conceal', 'reveal', 'enable', 'disable'];
methods.forEach(method => {
beforeEach(() => {
stub(WrappedElement.prototype, method);
stub(WrappedElement.prototype, method as keyof WrappedElement);
});
afterEach(() => {

View File

@ -1,26 +1,25 @@
import WrappedElement from './wrapped-element';
/**
* @typedef {import('../../../types/index').Choices.ClassNames} ClassNames
* @typedef {import('../../../types/index').Choices.Item} Item
* @typedef {import('../../../types/index').Choices.Choice} Choice
*/
import { ClassNames, Item } from '../interfaces';
export default class WrappedSelect extends WrappedElement {
/**
* @param {{
* element: HTMLSelectElement,
* classNames: ClassNames,
* delimiter: string
* template: function
* }} args
*/
constructor({ element, classNames, template }) {
element: HTMLSelectElement;
classNames: ClassNames;
template: (data: object) => HTMLOptionElement;
constructor({
element,
classNames,
template,
}: {
element: HTMLSelectElement;
classNames: ClassNames;
template: (data: object) => HTMLOptionElement;
}) {
super({ element, classNames });
this.template = template;
}
get placeholderOption() {
get placeholderOption(): HTMLOptionElement | null {
return (
this.element.querySelector('option[value=""]') ||
// Backward compatibility layer for the non-standard placeholder attribute supported in older versions.
@ -28,26 +27,17 @@ export default class WrappedSelect extends WrappedElement {
);
}
/**
* @returns {Element[]}
*/
get optionGroups() {
get optionGroups(): Element[] {
return Array.from(this.element.getElementsByTagName('OPTGROUP'));
}
/**
* @returns {Item[] | Choice[]}
*/
get options() {
get options(): Item[] | HTMLOptionElement[] {
return Array.from(this.element.options);
}
/**
* @param {Item[] | Choice[]} options
*/
set options(options) {
set options(options: Item[] | HTMLOptionElement[]) {
const fragment = document.createDocumentFragment();
const addOptionToFragment = data => {
const addOptionToFragment = (data): void => {
// Create a standard select option
const option = this.template(data);
// Append it to fragment
@ -60,10 +50,7 @@ export default class WrappedSelect extends WrappedElement {
this.appendDocFragment(fragment);
}
/**
* @param {DocumentFragment} fragment
*/
appendDocFragment(fragment) {
appendDocFragment(fragment: DocumentFragment): void {
this.element.innerHTML = '';
this.element.appendChild(fragment);
}

View File

@ -104,6 +104,7 @@ describe('constants', () => {
'removeItem',
'highlightItem',
'highlightChoice',
'unhighlightItem',
]);
});
});
@ -121,6 +122,8 @@ describe('constants', () => {
'REMOVE_ITEM',
'HIGHLIGHT_ITEM',
'CLEAR_ALL',
'RESET_TO',
'SET_IS_LOADING',
]);
});
});

View File

@ -1,12 +1,13 @@
import { sanitise, sortByAlpha } from './lib/utils';
import {
Options,
ClassNames,
EventMap,
ActionType,
KeyCodeMap,
} from './interfaces';
/**
* @typedef {import('../../types/index').Choices.ClassNames} ClassNames
* @typedef {import('../../types/index').Choices.Options} Options
*/
/** @type {ClassNames} */
export const DEFAULT_CLASSNAMES = {
export const DEFAULT_CLASSNAMES: ClassNames = {
containerOuter: 'choices',
containerInner: 'choices__inner',
input: 'choices__input',
@ -35,8 +36,7 @@ export const DEFAULT_CLASSNAMES = {
noChoices: 'has-no-choices',
};
/** @type {Options} */
export const DEFAULT_CONFIG = {
export const DEFAULT_CONFIG: Options = {
items: [],
choices: [],
silent: false,
@ -83,7 +83,7 @@ export const DEFAULT_CONFIG = {
classNames: DEFAULT_CLASSNAMES,
};
export const EVENTS = {
export const EVENTS: Record<keyof EventMap, keyof EventMap> = {
showDropdown: 'showDropdown',
hideDropdown: 'hideDropdown',
change: 'change',
@ -93,9 +93,10 @@ export const EVENTS = {
removeItem: 'removeItem',
highlightItem: 'highlightItem',
highlightChoice: 'highlightChoice',
unhighlightItem: 'unhighlightItem',
};
export const ACTION_TYPES = {
export const ACTION_TYPES: Record<ActionType, ActionType> = {
ADD_CHOICE: 'ADD_CHOICE',
FILTER_CHOICES: 'FILTER_CHOICES',
ACTIVATE_CHOICES: 'ACTIVATE_CHOICES',
@ -105,9 +106,11 @@ export const ACTION_TYPES = {
REMOVE_ITEM: 'REMOVE_ITEM',
HIGHLIGHT_ITEM: 'HIGHLIGHT_ITEM',
CLEAR_ALL: 'CLEAR_ALL',
RESET_TO: 'RESET_TO',
SET_IS_LOADING: 'SET_IS_LOADING',
};
export const KEY_CODES = {
export const KEY_CODES: KeyCodeMap = {
BACK_KEY: 46,
DELETE_KEY: 8,
ENTER_KEY: 13,
@ -119,8 +122,9 @@ export const KEY_CODES = {
PAGE_DOWN_KEY: 34,
};
export const TEXT_TYPE = 'text';
export const SELECT_ONE_TYPE = 'select-one';
export const SELECT_MULTIPLE_TYPE = 'select-multiple';
export const TEXT_TYPE: HTMLInputElement['type'] = 'text';
export const SELECT_ONE_TYPE: HTMLSelectElement['type'] = 'select-one';
export const SELECT_MULTIPLE_TYPE: HTMLSelectElement['type'] =
'select-multiple';
export const SCROLLING_SPEED = 4;

754
src/scripts/interfaces.ts Normal file
View File

@ -0,0 +1,754 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { FuseOptions } from 'fuse.js';
import Choices from './choices';
export namespace Types {
export type strToEl = (
str: string,
) => HTMLElement | HTMLInputElement | HTMLOptionElement;
export type stringFunction = () => string;
export type noticeStringFunction = (value: string) => string;
export type noticeLimitFunction = (maxItemCount: number) => string;
export type filterFunction = (value: string) => boolean;
export type valueCompareFunction = (
value1: string,
value2: string,
) => boolean;
}
export interface Choice {
id?: number;
customProperties?: Record<string, any>;
disabled?: boolean;
active?: boolean;
elementId?: number;
groupId?: number;
keyCode?: number;
label: string;
placeholder?: boolean;
selected?: boolean;
value: string;
score?: number;
choices?: Choice[];
}
export interface Group {
id?: number;
active?: boolean;
disabled?: boolean;
value: any;
}
export interface Item extends Choice {
choiceId?: number;
highlighted?: boolean;
}
/**
* Events fired by Choices behave the same as standard events. Each event is triggered on the element passed to Choices (accessible via `this.passedElement`. Arguments are accessible within the `event.detail` object.
*/
export interface EventMap {
/**
* Triggered each time an item is added (programmatically or by the user).
*
* **Input types affected:** text, select-one, select-multiple
*
* Arguments: id, value, label, groupValue, keyCode
*/
addItem: CustomEvent<{
id: number;
value: string;
label: string;
groupValue: string;
keyCode: number;
}>;
/**
* Triggered each time an item is removed (programmatically or by the user).
*
* **Input types affected:** text, select-one, select-multiple
*
* Arguments: id, value, label, groupValue
*/
removeItem: CustomEvent<{
id: number;
value: string;
label: string;
groupValue: string;
}>;
/**
* Triggered each time an item is highlighted.
*
* **Input types affected:** text, select-multiple
*
* Arguments: id, value, label, groupValue
*/
highlightItem: CustomEvent<{
id: number;
value: string;
label: string;
groupValue: string;
}>;
/**
* Triggered each time an item is unhighlighted.
*
* **Input types affected:** text, select-multiple
*
* Arguments: id, value, label, groupValue
*/
unhighlightItem: CustomEvent<{
id: number;
value: string;
label: string;
groupValue: string;
}>;
/**
* Triggered each time a choice is selected **by a user**, regardless if it changes the value of the input.
*
* **Input types affected:** select-one, select-multiple
*
* Arguments: choice: Choice
*/
choice: CustomEvent<{ choice: Choice }>;
/**
* Triggered each time an item is added/removed **by a user**.
*
* **Input types affected:** text, select-one, select-multiple
*
* Arguments: value
*/
change: CustomEvent<{ value: string }>;
/**
* Triggered when a user types into an input to search choices.
*
* **Input types affected:** select-one, select-multiple
*
* Arguments: value, resultCount
*/
search: CustomEvent<{ value: string; resultCount: number }>;
/**
* Triggered when the dropdown is shown.
*
* **Input types affected:** select-one, select-multiple
*
* Arguments: -
*/
showDropdown: CustomEvent<undefined>;
/**
* Triggered when the dropdown is hidden.
*
* **Input types affected:** select-one, select-multiple
*
* Arguments: -
*/
hideDropdown: CustomEvent<undefined>;
/**
* Triggered when a choice from the dropdown is highlighted.
*
* Input types affected: select-one, select-multiple
* Arguments: el is the choice.passedElement that was affected.
*/
highlightChoice: CustomEvent<{ el: PassedElement }>;
}
export interface KeyCodeMap {
BACK_KEY: 46;
DELETE_KEY: 8;
ENTER_KEY: 13;
A_KEY: 65;
ESC_KEY: 27;
UP_KEY: 38;
DOWN_KEY: 40;
PAGE_UP_KEY: 33;
PAGE_DOWN_KEY: 34;
}
export type ActionType =
| 'ADD_CHOICE'
| 'FILTER_CHOICES'
| 'ACTIVATE_CHOICES'
| 'CLEAR_CHOICES'
| 'ADD_GROUP'
| 'ADD_ITEM'
| 'REMOVE_ITEM'
| 'HIGHLIGHT_ITEM'
| 'CLEAR_ALL'
| 'RESET_TO'
| 'SET_IS_LOADING';
/** Classes added to HTML generated by By default classnames follow the BEM notation. */
export interface ClassNames {
/** @default 'choices' */
containerOuter: string;
/** @default 'choices__inner' */
containerInner: string;
/** @default 'choices__input' */
input: string;
/** @default 'choices__input--cloned' */
inputCloned: string;
/** @default 'choices__list' */
list: string;
/** @default 'choices__list--multiple' */
listItems: string;
/** @default 'choices__list--single' */
listSingle: string;
/** @default 'choices__list--dropdown' */
listDropdown: string;
/** @default 'choices__item' */
item: string;
/** @default 'choices__item--selectable' */
itemSelectable: string;
/** @default 'choices__item--disabled' */
itemDisabled: string;
/** @default 'choices__item--choice' */
itemChoice: string;
/** @default 'choices__placeholder' */
placeholder: string;
/** @default 'choices__group' */
group: string;
/** @default 'choices__heading' */
groupHeading: string;
/** @default 'choices__button' */
button: string;
/** @default 'is-active' */
activeState: string;
/** @default 'is-focused' */
focusState: string;
/** @default 'is-open' */
openState: string;
/** @default 'is-disabled' */
disabledState: string;
/** @default 'is-highlighted' */
highlightedState: string;
/** @default 'is-selected' */
selectedState: string;
/** @default 'is-flipped' */
flippedState: string;
/** @default 'is-loading' */
loadingState: string;
/** @default 'has-no-results' */
noResults: string;
/** @default 'has-no-choices' */
noChoices: string;
}
export interface PassedElement extends HTMLElement {
classNames: ClassNames;
element: (HTMLInputElement | HTMLSelectElement) & {
// Extends HTMLElement addEventListener with Choices events
addEventListener<K extends keyof EventMap>(
type: K,
listener: (
this: HTMLInputElement | HTMLSelectElement,
ev: EventMap[K],
) => void,
options?: boolean | AddEventListenerOptions,
): void;
};
type: 'text' | 'select-one' | 'select-multiple';
isDisabled: boolean;
parentInstance: Choices;
}
/**
* Choices options interface
*
* **Terminology**
*
* - **Choice:** A choice is a value a user can select. A choice would be equivalent to the `<option></option>` element within a select input.
* - **Group:** A group is a collection of choices. A group should be seen as equivalent to a `<optgroup></optgroup>` element within a select input.
* - **Item:** An item is an inputted value **_(text input)_** or a selected choice **_(select element)_**. In the context of a select element, an item is equivelent to a selected option element: `<option value="Hello" selected></option>` whereas in the context of a text input an item is equivelant to `<input type="text" value="Hello">`
*/
export interface Options {
/**
* Optionally suppress console errors and warnings.
*
* **Input types affected:** text, select-single, select-multiple
*
* @default false
*/
silent: boolean;
/**
* Add pre-selected items (see terminology) to text input.
*
* **Input types affected:** text
*
* @example
* ```
* ['value 1', 'value 2', 'value 3']
* ```
*
* @example
* ```
* [{
* value: 'Value 1',
* label: 'Label 1',
* id: 1
* },
* {
* value: 'Value 2',
* label: 'Label 2',
* id: 2,
* customProperties: {
* random: 'I am a custom property'
* }
* }]
* ```
*
* @default []
*/
items: string[] | Choice[];
/**
* Add choices (see terminology) to select input.
*
* **Input types affected:** select-one, select-multiple
*
* @example
* ```
* [{
* value: 'Option 1',
* label: 'Option 1',
* selected: true,
* disabled: false,
* },
* {
* value: 'Option 2',
* label: 'Option 2',
* selected: false,
* disabled: true,
* customProperties: {
* description: 'Custom description about Option 2',
* random: 'Another random custom property'
* },
* }]
* ```
*
* @default []
*/
choices: Choice[];
/**
* The amount of choices to be rendered within the dropdown list `("-1" indicates no limit)`. This is useful if you have a lot of choices where it is easier for a user to use the search area to find a choice.
*
* **Input types affected:** select-one, select-multiple
*
* @default -1
*/
renderChoiceLimit: number;
/**
* The amount of items a user can input/select `("-1" indicates no limit)`.
*
* **Input types affected:** text, select-multiple
*
* @default -1
*/
maxItemCount: number;
/**
* Whether a user can add items.
*
* **Input types affected:** text
*
* @default true
*/
addItems: boolean;
/**
* A filter that will need to pass for a user to successfully add an item.
*
* **Input types affected:** text
*
* @default null
*/
addItemFilter: string | RegExp | Types.filterFunction | null;
/**
* The text that is shown when a user has inputted a new item but has not pressed the enter key. To access the current input value, pass a function with a `value` argument (see the **default config** [https://github.com/jshjohnson/Choices#setup] for an example), otherwise pass a string.
*
* **Input types affected:** text
*
* @default
* ```
* (value) => `Press Enter to add <b>"${value}"</b>`;
* ```
*/
addItemText: string | Types.noticeStringFunction;
/**
* Whether a user can remove items.
*
* **Input types affected:** text, select-multiple
*
* @default true
*/
removeItems: boolean;
/**
* Whether each item should have a remove button.
*
* **Input types affected:** text, select-one, select-multiple
*
* @default false
*/
removeItemButton: boolean;
/**
* Whether a user can edit items. An item's value can be edited by pressing the backspace.
*
* **Input types affected:** text
*
* @default false
*/
editItems: boolean;
/**
* Whether each inputted/chosen item should be unique.
*
* **Input types affected:** text, select-multiple
*
* @default true
*/
duplicateItemsAllowed: boolean;
/**
* What divides each value. The default delimiter separates each value with a comma: `"Value 1, Value 2, Value 3"`.
*
* **Input types affected:** text
*
* @default ','
*/
delimiter: string;
/**
* Whether a user can paste into the input.
*
* **Input types affected:** text, select-multiple
*
* @default true
*/
paste: boolean;
/**
* Whether a search area should be shown.
*
* @note Multiple select boxes will always show search areas.
*
* **Input types affected:** select-one
*
* @default true
*/
searchEnabled: boolean;
/**
* Whether choices should be filtered by input or not. If `false`, the search event will still emit, but choices will not be filtered.
*
* **Input types affected:** select-one
*
* @default true
*/
searchChoices: boolean;
/**
* The minimum length a search value should be before choices are searched.
*
* **Input types affected:** select-one, select-multiple
*
* @default 1
*/
searchFloor: number;
/**
* The maximum amount of search results to show.
*
* **Input types affected:** select-one, select-multiple
*
* @default 4
*/
searchResultLimit: number;
/**
* Specify which fields should be used when a user is searching. If you have added custom properties to your choices, you can add these values thus: `['label', 'value', 'customProperties.example']`.
*
* Input types affected:select-one, select-multiple
*
* @default ['label', 'value']
*/
searchFields: string[];
/**
* Whether the dropdown should appear above `(top)` or below `(bottom)` the input. By default, if there is not enough space within the window the dropdown will appear above the input, otherwise below it.
*
* **Input types affected:** select-one, select-multiple
*
* @default 'auto'
*/
position: 'auto' | 'top' | 'bottom';
/**
* Whether the scroll position should reset after adding an item.
*
* **Input types affected:** select-multiple
*
* @default true
*/
resetScrollPosition: boolean;
/**
* Whether choices and groups should be sorted. If false, choices/groups will appear in the order they were given.
*
* **Input types affected:** select-one, select-multiple
*
* @default true
*/
shouldSort: boolean;
/**
* Whether items should be sorted. If false, items will appear in the order they were selected.
*
* **Input types affected:** text, select-multiple
*
* @default false
*/
shouldSortItems: boolean;
/**
* The function that will sort choices and items before they are displayed (unless a user is searching). By default choices and items are sorted by alphabetical order.
*
* **Input types affected:** select-one, select-multiple
*
* @example
* ```
* // Sorting via length of label from largest to smallest
* const example = new Choices(element, {
* sorter: function(a, b) {
* return b.label.length - a.label.length;
* },
* };
* ```
*
* @default sortByAlpha
*/
sorter: (current: Choice, next: Choice) => number;
/**
* Whether the input should show a placeholder. Used in conjunction with `placeholderValue`. If `placeholder` is set to true and no value is passed to `placeholderValue`, the passed input's placeholder attribute will be used as the placeholder value.
*
* **Input types affected:** text, select-multiple
*
* @note For single select boxes, the recommended way of adding a placeholder is as follows:
* ```
* <select>
* <option placeholder>This is a placeholder</option>
* <option>...</option>
* <option>...</option>
* <option>...</option>
* </select>
* ```
*
* @default true
*/
placeholder: boolean;
/**
* The value of the inputs placeholder.
*
* **Input types affected:** text, select-multiple
*
* @default null
*/
placeholderValue: string | null;
/**
* The value of the search inputs placeholder.
*
* **Input types affected:** select-one
*
* @default null
*/
searchPlaceholderValue: string | null;
/**
* Prepend a value to each item added/selected.
*
* **Input types affected:** text, select-one, select-multiple
*
* @default null
*/
prependValue: string | null;
/**
* Append a value to each item added/selected.
*
* **Input types affected:** text, select-one, select-multiple
*
* @default null
*/
appendValue: string | null;
/**
* Whether selected choices should be removed from the list. By default choices are removed when they are selected in multiple select box. To always render choices pass `always`.
*
* **Input types affected:** select-one, select-multiple
*
* @default 'auto';
*/
renderSelectedChoices: 'auto' | 'always';
/**
* The text that is shown whilst choices are being populated via AJAX.
*
* **Input types affected:** select-one, select-multiple
*
* @default 'Loading...'
*/
loadingText: string;
/**
* The text that is shown when a user's search has returned no results. Optionally pass a function returning a string.
*
* **Input types affected:** select-one, select-multiple
*
* @default 'No results found'
*/
noResultsText: string | Types.stringFunction;
/**
* The text that is shown when a user has selected all possible choices. Optionally pass a function returning a string.
*
* **Input types affected:** select-multiple
*
* @default 'No choices to choose from'
*/
noChoicesText: string | Types.stringFunction;
/**
* The text that is shown when a user hovers over a selectable choice.
*
* **Input types affected:** select-multiple, select-one
*
* @default 'Press to select'
*/
itemSelectText: string;
/**
* The text that is shown when a user has focus on the input but has already reached the **max item count** [https://github.com/jshjohnson/Choices#maxitemcount]. To access the max item count, pass a function with a `maxItemCount` argument (see the **default config** [https://github.com/jshjohnson/Choices#setup] for an example), otherwise pass a string.
*
* **Input types affected:** text
*
* @default
* ```
* (maxItemCount) => `Only ${maxItemCount} values can be added.`;
* ```
*/
maxItemText: string | Types.noticeLimitFunction;
/**
* If no duplicates are allowed, and the value already exists in the array.
*
* @default 'Only unique values can be added'
*/
uniqueItemText: string | Types.noticeStringFunction;
/**
* The text that is shown when addItemFilter is passed and it returns false
*
* **Input types affected:** text
*
* @default 'Only values matching specific conditions can be added'
*/
customAddItemText: string | Types.noticeStringFunction;
/**
* Compare choice and value in appropriate way (e.g. deep equality for objects). To compare choice and value, pass a function with a `valueComparer` argument (see the [default config](https://github.com/jshjohnson/Choices#setup) for an example).
*
* **Input types affected:** select-one, select-multiple
*
* @default
* ```
* (choice, item) => choice === item;
* ```
*/
valueComparer: Types.valueCompareFunction;
/**
* Classes added to HTML generated by By default classnames follow the BEM notation.
*
* **Input types affected:** text, select-one, select-multiple
*/
classNames: ClassNames;
/**
* Choices uses the great Fuse library for searching. You can find more options here: https://github.com/krisk/Fuse#options
*/
fuseOptions: FuseOptions<Choice>;
/**
* Function to run once Choices initialises.
*
* **Input types affected:** text, select-one, select-multiple
*
* @note For each callback, this refers to the current instance of This can be useful if you need access to methods `(this.disable())` or the config object `(this.config)`.
*
* @default null
*/
callbackOnInit: ((this: Choices) => void) | null;
/**
* Function to run on template creation. Through this callback it is possible to provide custom templates for the various components of Choices (see terminology). For Choices to work with custom templates, it is important you maintain the various data attributes defined here [https://github.com/jshjohnson/Choices/blob/67f29c286aa21d88847adfcd6304dc7d068dc01f/assets/scripts/src/choices.js#L1993-L2067].
*
* **Input types affected:** text, select-one, select-multiple
*
* @note For each callback, this refers to the current instance of This can be useful if you need access to methods `(this.disable())` or the config object `(this.config)`.
*
* @example
* ```
* const example = new Choices(element, {
* callbackOnCreateTemplates: function (template) {
* var classNames = this.config.classNames;
* return {
* item: (data) => {
* return template(`
* <div class="${classNames.item} ${data.highlighted ? classNames.highlightedState : classNames.itemSelectable}" data-item data-id="${data.id}" data-value="${data.value}" ${data.active ? 'aria-selected="true"' : ''} ${data.disabled ? 'aria-disabled="true"' : ''}>
* <span>&bigstar;</span> ${data.label}
* </div>
* `);
* },
* choice: (data) => {
* return template(`
* <div class="${classNames.item} ${classNames.itemChoice} ${data.disabled ? classNames.itemDisabled : classNames.itemSelectable}" data-select-text="${this.config.itemSelectText}" data-choice ${data.disabled ? 'data-choice-disabled aria-disabled="true"' : 'data-choice-selectable'} data-id="${data.id}" data-value="${data.value}" ${data.groupId > 0 ? 'role="treeitem"' : 'role="option"'}>
* <span>&bigstar;</span> ${data.label}
* </div>
* `);
* },
* };
* }
* });
* ```
*
* @default null
*/
callbackOnCreateTemplates: ((template: Types.strToEl) => void) | null;
}
// @todo rename
export interface Notice {
response: boolean;
notice: string;
}
export interface State {
choices: Choice[];
groups: Group[];
items: Item[];
loading: boolean;
}

View File

@ -1,214 +0,0 @@
/**
* @param {number} min
* @param {number} max
* @returns {number}
*/
export const getRandomNumber = (min, max) =>
Math.floor(Math.random() * (max - min) + min);
/**
* @param {number} length
* @returns {string}
*/
export const generateChars = length =>
Array.from({ length }, () => getRandomNumber(0, 36).toString(36)).join('');
/**
* @param {HTMLInputElement | HTMLSelectElement} element
* @param {string} prefix
* @returns {string}
*/
export const generateId = (element, prefix) => {
let id =
element.id ||
(element.name && `${element.name}-${generateChars(2)}`) ||
generateChars(4);
id = id.replace(/(:|\.|\[|\]|,)/g, '');
id = `${prefix}-${id}`;
return id;
};
/**
* @param {any} obj
* @returns {string}
*/
export const getType = obj => Object.prototype.toString.call(obj).slice(8, -1);
/**
* @param {string} type
* @param {any} obj
* @returns {boolean}
*/
export const isType = (type, obj) =>
obj !== undefined && obj !== null && getType(obj) === type;
/**
* @param {HTMLElement} element
* @param {HTMLElement} [wrapper={HTMLDivElement}]
* @returns {HTMLElement}
*/
export const wrap = (element, wrapper = document.createElement('div')) => {
if (element.nextSibling) {
element.parentNode.insertBefore(wrapper, element.nextSibling);
} else {
element.parentNode.appendChild(wrapper);
}
return wrapper.appendChild(element);
};
/**
* @param {Element} startEl
* @param {string} selector
* @param {1 | -1} direction
* @returns {Element | undefined}
*/
export const getAdjacentEl = (startEl, selector, direction = 1) => {
if (!(startEl instanceof Element) || typeof selector !== 'string') {
return undefined;
}
const prop = `${direction > 0 ? 'next' : 'previous'}ElementSibling`;
let sibling = startEl[prop];
while (sibling) {
if (sibling.matches(selector)) {
return sibling;
}
sibling = sibling[prop];
}
return sibling;
};
/**
* @param {Element} element
* @param {Element} parent
* @param {-1 | 1} direction
* @returns {boolean}
*/
export const isScrolledIntoView = (element, parent, direction = 1) => {
if (!element) {
return false;
}
let isVisible;
if (direction > 0) {
// In view from bottom
isVisible =
parent.scrollTop + parent.offsetHeight >=
element.offsetTop + element.offsetHeight;
} else {
// In view from top
isVisible = element.offsetTop >= parent.scrollTop;
}
return isVisible;
};
/**
* @param {any} value
* @returns {any}
*/
export const sanitise = value => {
if (typeof value !== 'string') {
return value;
}
return value
.replace(/&/g, '&amp;')
.replace(/>/g, '&rt;')
.replace(/</g, '&lt;')
.replace(/"/g, '&quot;');
};
/**
* @returns {() => (str: string) => Element}
*/
export const strToEl = (() => {
const tmpEl = document.createElement('div');
return str => {
const cleanedInput = str.trim();
tmpEl.innerHTML = cleanedInput;
const firldChild = tmpEl.children[0];
while (tmpEl.firstChild) {
tmpEl.removeChild(tmpEl.firstChild);
}
return firldChild;
};
})();
/**
* @param {{ label?: string, value: string }} a
* @param {{ label?: string, value: string }} b
* @returns {number}
*/
export const sortByAlpha = (
{ value, label = value },
{ value: value2, label: label2 = value2 },
) =>
label.localeCompare(label2, [], {
sensitivity: 'base',
ignorePunctuation: true,
numeric: true,
});
/**
* @param {{ score: number }} a
* @param {{ score: number }} b
*/
export const sortByScore = (a, b) => a.score - b.score;
/**
* @param {HTMLElement} element
* @param {string} type
* @param {object} customArgs
*/
export const dispatchEvent = (element, type, customArgs = null) => {
const event = new CustomEvent(type, {
detail: customArgs,
bubbles: true,
cancelable: true,
});
return element.dispatchEvent(event);
};
/**
* @param {array} array
* @param {any} value
* @param {string} [key="value"]
* @returns {boolean}
*/
export const existsInArray = (array, value, key = 'value') =>
array.some(item => {
if (typeof value === 'string') {
return item[key] === value.trim();
}
return item[key] === value;
});
/**
* @param {any} obj
* @returns {any}
*/
export const cloneObject = obj => JSON.parse(JSON.stringify(obj));
/**
* Returns an array of keys present on the first but missing on the second object
* @param {object} a
* @param {object} b
* @returns {string[]}
*/
export const diff = (a, b) => {
const aKeys = Object.keys(a).sort();
const bKeys = Object.keys(b).sort();
return aKeys.filter(i => bKeys.indexOf(i) < 0);
};

View File

@ -1,3 +1,4 @@
/* eslint-disable no-new-wrappers */
import { expect } from 'chai';
import { stub } from 'sinon';
import {
@ -140,19 +141,19 @@ describe('utils', () => {
it('sorts by label alphabetically', () => {
const values = [
{ label: 'The Strokes' },
{ label: 'Arctic Monkeys' },
{ label: 'Oasis' },
{ label: 'Tame Impala' },
{ value: '0', label: 'The Strokes' },
{ value: '0', label: 'Arctic Monkeys' },
{ value: '0', label: 'Oasis' },
{ value: '0', label: 'Tame Impala' },
];
const output = values.sort(sortByAlpha);
expect(output).to.eql([
{ label: 'Arctic Monkeys' },
{ label: 'Oasis' },
{ label: 'Tame Impala' },
{ label: 'The Strokes' },
{ value: '0', label: 'Arctic Monkeys' },
{ value: '0', label: 'Oasis' },
{ value: '0', label: 'Tame Impala' },
{ value: '0', label: 'The Strokes' },
]);
});
});
@ -185,12 +186,12 @@ describe('utils', () => {
const fakeElement = {
dispatchEvent: stub(),
};
const eventType = 'testEvent';
const eventType = 'addItem';
const customArgs = {
testing: true,
};
dispatchEvent(fakeElement, eventType, customArgs);
dispatchEvent(fakeElement as any, eventType, customArgs);
expect(fakeElement.dispatchEvent.called).to.equal(true);
const event = fakeElement.dispatchEvent.lastCall.args[0];

180
src/scripts/lib/utils.ts Normal file
View File

@ -0,0 +1,180 @@
import { EventMap, Choice } from '../interfaces';
/* eslint-disable @typescript-eslint/no-explicit-any */
export const getRandomNumber = (min: number, max: number): number =>
Math.floor(Math.random() * (max - min) + min);
export const generateChars = (length: number): string =>
Array.from({ length }, () => getRandomNumber(0, 36).toString(36)).join('');
export const generateId = (
element: HTMLInputElement | HTMLSelectElement,
prefix: string,
): string => {
let id =
element.id ||
(element.name && `${element.name}-${generateChars(2)}`) ||
generateChars(4);
id = id.replace(/(:|\.|\[|\]|,)/g, '');
id = `${prefix}-${id}`;
return id;
};
export const getType = (obj: any): string =>
Object.prototype.toString.call(obj).slice(8, -1);
export const isType = (type: string, obj: any): boolean =>
obj !== undefined && obj !== null && getType(obj) === type;
export const wrap = (
element: HTMLElement,
wrapper: HTMLElement = document.createElement('div'),
): HTMLElement => {
if (element.nextSibling) {
element.parentNode &&
element.parentNode.insertBefore(wrapper, element.nextSibling);
} else {
element.parentNode && element.parentNode.appendChild(wrapper);
}
return wrapper.appendChild(element);
};
export const getAdjacentEl = (
startEl: Element,
selector: string,
direction = 1,
): Element => {
const prop = `${direction > 0 ? 'next' : 'previous'}ElementSibling`;
let sibling = startEl[prop];
while (sibling) {
if (sibling.matches(selector)) {
return sibling;
}
sibling = sibling[prop];
}
return sibling;
};
export const isScrolledIntoView = (
element: HTMLElement,
parent: HTMLElement,
direction = 1,
): boolean => {
if (!element) {
return false;
}
let isVisible;
if (direction > 0) {
// In view from bottom
isVisible =
parent.scrollTop + parent.offsetHeight >=
element.offsetTop + element.offsetHeight;
} else {
// In view from top
isVisible = element.offsetTop >= parent.scrollTop;
}
return isVisible;
};
export const sanitise = <T>(value: T | string): T | string => {
if (typeof value !== 'string') {
return value;
}
return value
.replace(/&/g, '&amp;')
.replace(/>/g, '&rt;')
.replace(/</g, '&lt;')
.replace(/"/g, '&quot;');
};
export const strToEl = ((): ((str: string) => Element) => {
const tmpEl = document.createElement('div');
return (str): Element => {
const cleanedInput = str.trim();
tmpEl.innerHTML = cleanedInput;
const firldChild = tmpEl.children[0];
while (tmpEl.firstChild) {
tmpEl.removeChild(tmpEl.firstChild);
}
return firldChild;
};
})();
interface RecordToCompare {
value: string;
label?: string;
}
export const sortByAlpha = (
{ value, label = value }: RecordToCompare,
{ value: value2, label: label2 = value2 }: RecordToCompare,
): number =>
label.localeCompare(label2, [], {
sensitivity: 'base',
ignorePunctuation: true,
numeric: true,
});
export const sortByScore = (
a: Pick<Choice, 'score'>,
b: Pick<Choice, 'score'>,
): number => {
const { score: scoreA = 0 } = a;
const { score: scoreB = 0 } = b;
return scoreA - scoreB;
};
export const dispatchEvent = (
element: HTMLElement,
type: keyof EventMap,
customArgs: object | null = null,
): boolean => {
const event = new CustomEvent(type, {
detail: customArgs,
bubbles: true,
cancelable: true,
});
return element.dispatchEvent(event);
};
export const existsInArray = (
array: any[],
value: string,
key = 'value',
): boolean =>
array.some(item => {
if (typeof value === 'string') {
return item[key] === value.trim();
}
return item[key] === value;
});
export const cloneObject = (obj: object): object =>
JSON.parse(JSON.stringify(obj));
/**
* Returns an array of keys present on the first but missing on the second object
*/
export const diff = (
a: Record<string, any>,
b: Record<string, any>,
): string[] => {
const aKeys = Object.keys(a).sort();
const bKeys = Object.keys(b).sort();
return aKeys.filter(i => bKeys.indexOf(i) < 0);
};

View File

@ -1,110 +0,0 @@
export const defaultState = [];
export default function choices(state = defaultState, action) {
switch (action.type) {
case 'ADD_CHOICE': {
/*
A disabled choice appears in the choice dropdown but cannot be selected
A selected choice has been added to the passed input's value (added as an item)
An active choice appears within the choice dropdown
*/
return [
...state,
{
id: action.id,
elementId: action.elementId,
groupId: action.groupId,
value: action.value,
label: action.label || action.value,
disabled: action.disabled || false,
selected: false,
active: true,
score: 9999,
customProperties: action.customProperties,
placeholder: action.placeholder || false,
keyCode: null,
},
];
}
case 'ADD_ITEM': {
// If all choices need to be activated
if (action.activateOptions) {
return state.map(obj => {
const choice = obj;
choice.active = action.active;
return choice;
});
}
// When an item is added and it has an associated choice,
// we want to disable it so it can't be chosen again
if (action.choiceId > -1) {
return state.map(obj => {
const choice = obj;
if (choice.id === parseInt(action.choiceId, 10)) {
choice.selected = true;
}
return choice;
});
}
return state;
}
case 'REMOVE_ITEM': {
// When an item is removed and it has an associated choice,
// we want to re-enable it so it can be chosen again
if (action.choiceId > -1) {
return state.map(obj => {
const choice = obj;
if (choice.id === parseInt(action.choiceId, 10)) {
choice.selected = false;
}
return choice;
});
}
return state;
}
case 'FILTER_CHOICES': {
return state.map(obj => {
const choice = obj;
// Set active state based on whether choice is
// within filtered results
choice.active = action.results.some(({ item, score }) => {
if (item.id === choice.id) {
choice.score = score;
return true;
}
return false;
});
return choice;
});
}
case 'ACTIVATE_CHOICES': {
return state.map(obj => {
const choice = obj;
choice.active = action.active;
return choice;
});
}
case 'CLEAR_CHOICES': {
return defaultState;
}
default: {
return state;
}
}
}

View File

@ -1,21 +1,22 @@
import { expect } from 'chai';
import choices, { defaultState } from './choices';
import { Choice } from '../interfaces';
describe('reducers/choices', () => {
it('should return same state when no action matches', () => {
expect(choices(defaultState, {})).to.equal(defaultState);
expect(choices(defaultState, {} as any)).to.equal(defaultState);
});
describe('when choices do not exist', () => {
describe('ADD_CHOICE', () => {
const value = 'test';
const label = 'test';
const id = 'test';
const groupId = 'test';
const id = 1;
const groupId = 1;
const disabled = false;
const elementId = 'test';
const customProperties = 'test';
const placeholder = 'test';
const elementId = 1;
const customProperties = { test: true };
const placeholder = true;
describe('passing expected values', () => {
it('adds choice', () => {
@ -32,7 +33,6 @@ describe('reducers/choices', () => {
selected: false,
active: true,
score: 9999,
keyCode: null,
},
];
@ -68,14 +68,13 @@ describe('reducers/choices', () => {
selected: false,
active: true,
score: 9999,
keyCode: null,
},
];
const actualResponse = choices(undefined, {
type: 'ADD_CHOICE',
value,
label: null,
label: undefined,
id,
groupId,
disabled,
@ -103,14 +102,13 @@ describe('reducers/choices', () => {
selected: false,
active: true,
score: 9999,
keyCode: null,
},
];
const actualResponse = choices(undefined, {
type: 'ADD_CHOICE',
value,
label: null,
label: undefined,
id,
groupId,
disabled,
@ -143,7 +141,6 @@ describe('reducers/choices', () => {
score: 9999,
customProperties: null,
placeholder: false,
keyCode: null,
},
{
id: 2,
@ -157,7 +154,6 @@ describe('reducers/choices', () => {
score: 9999,
customProperties: null,
placeholder: false,
keyCode: null,
},
];
});
@ -178,9 +174,7 @@ describe('reducers/choices', () => {
type: 'FILTER_CHOICES',
results: [
{
item: {
id,
},
item: { id } as Choice,
score,
},
],
@ -227,7 +221,8 @@ describe('reducers/choices', () => {
});
describe('ADD_ITEM', () => {
it('disables choice if action has choice id', () => {
describe('when action has a choice id', () => {
it('disables choice corresponding with id', () => {
const id = 2;
const clonedState = state.slice(0);
const expectedResponse = [
@ -247,25 +242,13 @@ describe('reducers/choices', () => {
expect(actualResponse).to.eql(expectedResponse);
});
it('activates all choices if activateOptions flag passed', () => {
const clonedState = state.slice(0);
const actualResponse = choices(clonedState, {
type: 'ADD_ITEM',
activateOptions: true,
active: true,
});
expect(actualResponse[0].active).to.equal(true);
expect(actualResponse[1].active).to.equal(true);
});
describe('neither of the above conditions are satisified', () => {
describe('when action has no choice id', () => {
it('returns state', () => {
const clonedState = state.slice(0);
const actualResponse = choices(clonedState, {
type: 'ADD_ITEM',
activateOptions: false,
choiceId: undefined,
});

View File

@ -0,0 +1,127 @@
import { Choice } from '../interfaces';
import {
AddChoiceAction,
FilterChoicesAction,
ActivateChoicesAction,
ClearChoicesAction,
} from '../actions/choices';
import { AddItemAction, RemoveItemAction } from '../actions/items';
export const defaultState = [];
type ActionTypes =
| AddChoiceAction
| FilterChoicesAction
| ActivateChoicesAction
| ClearChoicesAction
| AddItemAction
| RemoveItemAction;
export default function choices(
state: Choice[] = defaultState,
action: ActionTypes,
): Choice[] {
switch (action.type) {
case 'ADD_CHOICE': {
const addChoiceAction = action as AddChoiceAction;
const choice = {
id: addChoiceAction.id,
elementId: addChoiceAction.elementId,
groupId: addChoiceAction.groupId,
value: addChoiceAction.value,
label: addChoiceAction.label || addChoiceAction.value,
disabled: addChoiceAction.disabled || false,
selected: false,
active: true,
score: 9999,
customProperties: addChoiceAction.customProperties,
placeholder: addChoiceAction.placeholder || false,
};
/*
A disabled choice appears in the choice dropdown but cannot be selected
A selected choice has been added to the passed input's value (added as an item)
An active choice appears within the choice dropdown
*/
return [...state, choice as Choice];
}
case 'ADD_ITEM': {
const addItemAction = action as AddItemAction;
// When an item is added and it has an associated choice,
// we want to disable it so it can't be chosen again
if (addItemAction.choiceId > -1) {
return state.map(obj => {
const choice = obj;
if (choice.id === parseInt(`${addItemAction.choiceId}`, 10)) {
choice.selected = true;
}
return choice;
});
}
return state;
}
case 'REMOVE_ITEM': {
const removeItemAction = action as RemoveItemAction;
// When an item is removed and it has an associated choice,
// we want to re-enable it so it can be chosen again
if (removeItemAction.choiceId && removeItemAction.choiceId > -1) {
return state.map(obj => {
const choice = obj;
if (choice.id === parseInt(`${removeItemAction.choiceId}`, 10)) {
choice.selected = false;
}
return choice;
});
}
return state;
}
case 'FILTER_CHOICES': {
const filterChoicesAction = action as FilterChoicesAction;
return state.map(obj => {
const choice = obj;
// Set active state based on whether choice is
// within filtered results
choice.active = filterChoicesAction.results.some(({ item, score }) => {
if (item.id === choice.id) {
choice.score = score;
return true;
}
return false;
});
return choice;
});
}
case 'ACTIVATE_CHOICES': {
const activateChoicesAction = action as ActivateChoicesAction;
return state.map(obj => {
const choice = obj;
choice.active = activateChoicesAction.active;
return choice;
});
}
case 'CLEAR_CHOICES': {
return defaultState;
}
default: {
return state;
}
}
}

View File

@ -1,19 +0,0 @@
export const defaultState = {
loading: false,
};
const general = (state = defaultState, action) => {
switch (action.type) {
case 'SET_IS_LOADING': {
return {
loading: action.isLoading,
};
}
default: {
return state;
}
}
};
export default general;

View File

@ -1,25 +0,0 @@
export const defaultState = [];
export default function groups(state = defaultState, action) {
switch (action.type) {
case 'ADD_GROUP': {
return [
...state,
{
id: action.id,
value: action.value,
active: action.active,
disabled: action.disabled,
},
];
}
case 'CLEAR_CHOICES': {
return [];
}
default: {
return state;
}
}
}

View File

@ -3,13 +3,13 @@ import groups, { defaultState } from './groups';
describe('reducers/groups', () => {
it('should return same state when no action matches', () => {
expect(groups(defaultState, {})).to.equal(defaultState);
expect(groups(defaultState, {} as any)).to.equal(defaultState);
});
describe('when groups do not exist', () => {
describe('ADD_GROUP', () => {
it('adds group', () => {
const id = '1';
const id = 1;
const value = 'Group one';
const active = true;
const disabled = false;

View File

@ -0,0 +1,36 @@
import { Group, State } from '../interfaces';
import { AddGroupAction } from '../actions/groups';
import { ClearChoicesAction } from '../actions/choices';
export const defaultState = [];
type ActionTypes = AddGroupAction | ClearChoicesAction;
export default function groups(
state: Group[] = defaultState,
action: ActionTypes,
): State['groups'] {
switch (action.type) {
case 'ADD_GROUP': {
const addGroupAction = action as AddGroupAction;
return [
...state,
{
id: addGroupAction.id,
value: addGroupAction.value,
active: addGroupAction.active,
disabled: addGroupAction.disabled,
},
];
}
case 'CLEAR_CHOICES': {
return [];
}
default: {
return state;
}
}
}

View File

@ -1,9 +1,10 @@
import { createStore } from 'redux';
import { expect } from 'chai';
import rootReducer from './index';
import rootReducer from '.';
import groups from './groups';
import choices from './choices';
import items from './items';
import loading from './loading';
describe('reducers/rootReducer', () => {
const store = createStore(rootReducer);
@ -11,9 +12,10 @@ describe('reducers/rootReducer', () => {
it('returns expected reducers', () => {
const state = store.getState();
expect(state.groups).to.equal(groups(undefined, {}));
expect(state.choices).to.equal(choices(undefined, {}));
expect(state.items).to.equal(items(undefined, {}));
expect(state.groups).to.equal(groups(undefined, {} as any));
expect(state.choices).to.equal(choices(undefined, {} as any));
expect(state.items).to.equal(items(undefined, {} as any));
expect(state.loading).to.equal(loading(undefined, {} as any));
});
describe('CLEAR_ALL', () => {
@ -33,9 +35,7 @@ describe('reducers/rootReducer', () => {
items: [],
groups: [],
choices: [],
general: {
loading: false,
},
});
});
});

View File

@ -2,24 +2,31 @@ import { combineReducers } from 'redux';
import items from './items';
import groups from './groups';
import choices from './choices';
import general from './general';
import loading from './loading';
import { cloneObject } from '../lib/utils';
export const defaultState = {
groups: [],
items: [],
choices: [],
loading: false,
};
const appReducer = combineReducers({
items,
groups,
choices,
general,
loading,
});
const rootReducer = (passedState, action) => {
const rootReducer = (passedState, action): object => {
let state = passedState;
// If we are clearing all items, groups and options we reassign
// state and then pass that state to our proper reducer. This isn't
// mutating our actual state
// See: http://stackoverflow.com/a/35641992
if (action.type === 'CLEAR_ALL') {
state = undefined;
state = defaultState;
} else if (action.type === 'RESET_TO') {
return cloneObject(action.state);
}

View File

@ -1,58 +0,0 @@
export const defaultState = [];
export default function items(state = defaultState, action) {
switch (action.type) {
case 'ADD_ITEM': {
// Add object to items array
const newState = [
...state,
{
id: action.id,
choiceId: action.choiceId,
groupId: action.groupId,
value: action.value,
label: action.label,
active: true,
highlighted: false,
customProperties: action.customProperties,
placeholder: action.placeholder || false,
keyCode: null,
},
];
return newState.map(obj => {
const item = obj;
item.highlighted = false;
return item;
});
}
case 'REMOVE_ITEM': {
// Set item to inactive
return state.map(obj => {
const item = obj;
if (item.id === action.id) {
item.active = false;
}
return item;
});
}
case 'HIGHLIGHT_ITEM': {
return state.map(obj => {
const item = obj;
if (item.id === action.id) {
item.highlighted = action.highlighted;
}
return item;
});
}
default: {
return state;
}
}
}

View File

@ -1,9 +1,10 @@
import { expect } from 'chai';
import items, { defaultState } from './items';
import { RemoveItemAction } from '../actions/items';
describe('reducers/items', () => {
it('should return same state when no action matches', () => {
expect(items(defaultState, {})).to.equal(defaultState);
expect(items(defaultState, {} as any)).to.equal(defaultState);
});
describe('when items do not exist', () => {
@ -16,7 +17,7 @@ describe('reducers/items', () => {
const customProperties = {
property: 'value',
};
const placeholder = 'This is a placeholder';
const placeholder = true;
const keyCode = 10;
describe('passing expected values', () => {
@ -148,7 +149,7 @@ describe('reducers/items', () => {
const actualResponse = items(clonedState, {
type: 'REMOVE_ITEM',
id,
});
} as RemoveItemAction);
expect(actualResponse).to.eql(expectedResponse);
});

View File

@ -0,0 +1,73 @@
import { Item, State } from '../interfaces';
import {
AddItemAction,
RemoveItemAction,
HighlightItemAction,
} from '../actions/items';
export const defaultState = [];
type ActionTypes = AddItemAction | RemoveItemAction | HighlightItemAction;
export default function items(
state: Item[] = defaultState,
action: ActionTypes,
): State['items'] {
switch (action.type) {
case 'ADD_ITEM': {
const addItemAction = action as AddItemAction;
// Add object to items array
const newState = [
...state,
{
id: addItemAction.id,
choiceId: addItemAction.choiceId,
groupId: addItemAction.groupId,
value: addItemAction.value,
label: addItemAction.label,
active: true,
highlighted: false,
customProperties: addItemAction.customProperties,
placeholder: addItemAction.placeholder || false,
keyCode: null,
},
];
return newState.map((obj: Item) => {
const item = obj;
item.highlighted = false;
return item;
});
}
case 'REMOVE_ITEM': {
// Set item to inactive
return state.map(obj => {
const item = obj;
if (item.id === action.id) {
item.active = false;
}
return item;
});
}
case 'HIGHLIGHT_ITEM': {
const highlightItemAction = action as HighlightItemAction;
return state.map(obj => {
const item = obj;
if (item.id === highlightItemAction.id) {
item.highlighted = highlightItemAction.highlighted;
}
return item;
});
}
default: {
return state;
}
}
}

View File

@ -1,16 +1,14 @@
import { expect } from 'chai';
import general, { defaultState } from './general';
import general, { defaultState } from './loading';
describe('reducers/general', () => {
describe('reducers/loading', () => {
it('should return same state when no action matches', () => {
expect(general(defaultState, {})).to.equal(defaultState);
expect(general(defaultState, {} as any)).to.equal(defaultState);
});
describe('SET_IS_LOADING', () => {
it('sets loading state', () => {
const expectedState = {
loading: true,
};
const expectedState = true;
const actualState = general(undefined, {
type: 'SET_IS_LOADING',

View File

@ -0,0 +1,23 @@
import { SetIsLoadingAction } from '../actions/misc';
import { State } from '../interfaces';
export const defaultState = false;
type ActionTypes = SetIsLoadingAction;
const general = (
state = defaultState,
action: ActionTypes,
): State['loading'] => {
switch (action.type) {
case 'SET_IS_LOADING': {
return action.isLoading;
}
default: {
return state;
}
}
};
export default general;

View File

@ -33,7 +33,7 @@ describe('reducers/store', () => {
describe('subscribe', () => {
it('wraps redux subscribe method', () => {
const onChange = () => {};
const onChange = (): void => {};
expect(subscribeStub.callCount).to.equal(0);
instance.subscribe(onChange);
expect(subscribeStub.callCount).to.equal(1);

View File

@ -1,108 +1,93 @@
import { createStore } from 'redux';
/* eslint-disable @typescript-eslint/no-explicit-any */
import { createStore, Store as IStore, AnyAction } from 'redux';
import rootReducer from '../reducers/index';
/**
* @typedef {import('../../../types/index').Choices.Choice} Choice
* @typedef {import('../../../types/index').Choices.Group} Group
* @typedef {import('../../../types/index').Choices.Item} Item
*/
import { Choice, Group, Item, State } from '../interfaces';
export default class Store {
_store: IStore;
constructor() {
this._store = createStore(
rootReducer,
window.__REDUX_DEVTOOLS_EXTENSION__ &&
window.__REDUX_DEVTOOLS_EXTENSION__(),
(window as any).__REDUX_DEVTOOLS_EXTENSION__ &&
(window as any).__REDUX_DEVTOOLS_EXTENSION__(),
);
}
/**
* Subscribe store to function call (wrapped Redux method)
* @param {Function} onChange Function to trigger when state changes
* @return
*/
subscribe(onChange) {
subscribe(onChange: () => void): void {
this._store.subscribe(onChange);
}
/**
* Dispatch event to store (wrapped Redux method)
* @param {{ type: string, [x: string]: any }} action Action to trigger
* @return
*/
dispatch(action) {
dispatch(action: AnyAction): void {
this._store.dispatch(action);
}
/**
* Get store object (wrapping Redux method)
* @returns {object} State
*/
get state() {
get state(): State {
return this._store.getState();
}
/**
* Get items from store
* @returns {Item[]} Item objects
*/
get items() {
get items(): Item[] {
return this.state.items;
}
/**
* Get active items from store
* @returns {Item[]} Item objects
*/
get activeItems() {
get activeItems(): Item[] {
return this.items.filter(item => item.active === true);
}
/**
* Get highlighted items from store
* @returns {Item[]} Item objects
*/
get highlightedActiveItems() {
get highlightedActiveItems(): Item[] {
return this.items.filter(item => item.active && item.highlighted);
}
/**
* Get choices from store
* @returns {Choice[]} Option objects
*/
get choices() {
get choices(): Choice[] {
return this.state.choices;
}
/**
* Get active choices from store
* @returns {Choice[]} Option objects
*/
get activeChoices() {
get activeChoices(): Choice[] {
return this.choices.filter(choice => choice.active === true);
}
/**
* Get selectable choices from store
* @returns {Choice[]} Option objects
*/
get selectableChoices() {
get selectableChoices(): Choice[] {
return this.choices.filter(choice => choice.disabled !== true);
}
/**
* Get choices that can be searched (excluding placeholders)
* @returns {Choice[]} Option objects
*/
get searchableChoices() {
get searchableChoices(): Choice[] {
return this.selectableChoices.filter(choice => choice.placeholder !== true);
}
/**
* Get placeholder choice from store
* @returns {Choice | undefined} Found placeholder
*/
get placeholderChoice() {
get placeholderChoice(): Choice | undefined {
return [...this.choices]
.reverse()
.find(choice => choice.placeholder === true);
@ -110,17 +95,15 @@ export default class Store {
/**
* Get groups from store
* @returns {Group[]} Group objects
*/
get groups() {
get groups(): Group[] {
return this.state.groups;
}
/**
* Get active groups from store
* @returns {Group[]} Group objects
*/
get activeGroups() {
get activeGroups(): Group[] {
const { groups, choices } = this;
return groups.filter(group => {
@ -135,27 +118,22 @@ export default class Store {
/**
* Get loading state from store
* @returns {boolean} Loading State
*/
isLoading() {
return this.state.general.loading;
isLoading(): boolean {
return this.state.loading;
}
/**
* Get single choice by it's ID
* @param {string} id
* @returns {Choice | undefined} Found choice
*/
getChoiceById(id) {
getChoiceById(id: string): Choice | undefined {
return this.activeChoices.find(choice => choice.id === parseInt(id, 10));
}
/**
* Get group by group id
* @param {number} id Group ID
* @returns {Group | undefined} Group data
*/
getGroupById(id) {
getGroupById(id: number): Group | undefined {
return this.groups.find(group => group.id === id);
}
}

View File

@ -3,11 +3,10 @@ import templates from './templates';
import { strToEl } from './lib/utils';
/**
*
* @param {HTMLElement} element1
* @param {HTMLElement} element2
*/
function expectEqualElements(element1, element2) {
function expectEqualElements(element1, element2): void {
expect(element1.tagName).to.equal(element2.tagName);
expect(element1.attributes.length).to.equal(element2.attributes.length);
expect(Object.keys(element1.dataset)).to.have.members(
@ -516,11 +515,10 @@ describe('templates', () => {
};
it('returns expected html', () => {
const value = 'test';
const expectedOutput = strToEl(
`<div class="${classes.list} ${classes.listDropdown}" aria-expanded="false"></div>`,
);
const actualOutput = templates.dropdown(classes, value);
const actualOutput = templates.dropdown(classes);
expectEqualElements(actualOutput, expectedOutput);
});

View File

@ -1,31 +1,19 @@
import { ClassNames, Item, Choice, Group, PassedElement } from './interfaces';
/**
* Helpers to create HTML elements used by Choices
* Can be overridden by providing `callbackOnCreateTemplates` option
* @typedef {import('../../types/index').Choices.Templates} Templates
* @typedef {import('../../types/index').Choices.ClassNames} ClassNames
* @typedef {import('../../types/index').Choices.Options} Options
* @typedef {import('../../types/index').Choices.Item} Item
* @typedef {import('../../types/index').Choices.Choice} Choice
* @typedef {import('../../types/index').Choices.Group} Group
*/
export const TEMPLATES = /** @type {Templates} */ ({
/**
* @param {Partial<ClassNames>} classNames
* @param {"ltr" | "rtl" | "auto"} dir
* @param {boolean} isSelectElement
* @param {boolean} isSelectOneElement
* @param {boolean} searchEnabled
* @param {"select-one" | "select-multiple" | "text"} passedElementType
*/
const templates = {
containerOuter(
{ containerOuter },
dir,
isSelectElement,
isSelectOneElement,
searchEnabled,
passedElementType,
) {
{ containerOuter }: Pick<ClassNames, 'containerOuter'>,
dir: HTMLElement['dir'],
isSelectElement: boolean,
isSelectOneElement: boolean,
searchEnabled: boolean,
passedElementType: PassedElement['type'],
): HTMLDivElement {
const div = Object.assign(document.createElement('div'), {
className: containerOuter,
});
@ -53,43 +41,48 @@ export const TEMPLATES = /** @type {Templates} */ ({
return div;
},
/**
* @param {Partial<ClassNames>} classNames
*/
containerInner({ containerInner }) {
containerInner({
containerInner,
}: Pick<ClassNames, 'containerInner'>): HTMLDivElement {
return Object.assign(document.createElement('div'), {
className: containerInner,
});
},
/**
* @param {Partial<ClassNames>} classNames
* @param {boolean} isSelectOneElement
*/
itemList({ list, listSingle, listItems }, isSelectOneElement) {
itemList(
{
list,
listSingle,
listItems,
}: Pick<ClassNames, 'list' | 'listSingle' | 'listItems'>,
isSelectOneElement: boolean,
): HTMLDivElement {
return Object.assign(document.createElement('div'), {
className: `${list} ${isSelectOneElement ? listSingle : listItems}`,
});
},
/**
* @param {Partial<ClassNames>} classNames
* @param {string} value
*/
placeholder({ placeholder }, value) {
placeholder(
{ placeholder }: Pick<ClassNames, 'placeholder'>,
value: string,
): HTMLDivElement {
return Object.assign(document.createElement('div'), {
className: placeholder,
innerHTML: value,
});
},
/**
* @param {Partial<ClassNames>} classNames
* @param {Item} item
* @param {boolean} removeItemButton
*/
item(
{ item, button, highlightedState, itemSelectable, placeholder },
{
item,
button,
highlightedState,
itemSelectable,
placeholder,
}: Pick<
ClassNames,
'item' | 'button' | 'highlightedState' | 'itemSelectable' | 'placeholder'
>,
{
id,
value,
@ -99,9 +92,9 @@ export const TEMPLATES = /** @type {Templates} */ ({
disabled,
highlighted,
placeholder: isPlaceholder,
},
removeItemButton,
) {
}: Item,
removeItemButton: boolean,
): HTMLDivElement {
const div = Object.assign(document.createElement('div'), {
className: item,
innerHTML: label,
@ -151,11 +144,10 @@ export const TEMPLATES = /** @type {Templates} */ ({
return div;
},
/**
* @param {Partial<ClassNames>} classNames
* @param {boolean} isSelectOneElement
*/
choiceList({ list }, isSelectOneElement) {
choiceList(
{ list }: Pick<ClassNames, 'list'>,
isSelectOneElement: boolean,
): HTMLDivElement {
const div = Object.assign(document.createElement('div'), {
className: list,
});
@ -168,11 +160,14 @@ export const TEMPLATES = /** @type {Templates} */ ({
return div;
},
/**
* @param {Partial<ClassNames>} classNames
* @param {Group} group
*/
choiceGroup({ group, groupHeading, itemDisabled }, { id, value, disabled }) {
choiceGroup(
{
group,
groupHeading,
itemDisabled,
}: Pick<ClassNames, 'group' | 'groupHeading' | 'itemDisabled'>,
{ id, value, disabled }: Group,
): HTMLDivElement {
const div = Object.assign(document.createElement('div'), {
className: `${group} ${disabled ? itemDisabled : ''}`,
});
@ -199,11 +194,6 @@ export const TEMPLATES = /** @type {Templates} */ ({
return div;
},
/**
* @param {Partial<ClassNames>} classNames
* @param {Choice} choice
* @param {Options['itemSelectText']} selectText
*/
choice(
{
item,
@ -212,7 +202,15 @@ export const TEMPLATES = /** @type {Templates} */ ({
selectedState,
itemDisabled,
placeholder,
},
}: Pick<
ClassNames,
| 'item'
| 'itemChoice'
| 'itemSelectable'
| 'selectedState'
| 'itemDisabled'
| 'placeholder'
>,
{
id,
value,
@ -222,9 +220,9 @@ export const TEMPLATES = /** @type {Templates} */ ({
disabled: isDisabled,
selected: isSelected,
placeholder: isPlaceholder,
},
selectText,
) {
}: Choice,
selectText: string,
): HTMLDivElement {
const div = Object.assign(document.createElement('div'), {
id: elementId,
innerHTML: label,
@ -239,7 +237,7 @@ export const TEMPLATES = /** @type {Templates} */ ({
div.classList.add(placeholder);
}
div.setAttribute('role', groupId > 0 ? 'treeitem' : 'option');
div.setAttribute('role', groupId && groupId > 0 ? 'treeitem' : 'option');
Object.assign(div.dataset, {
choice: '',
@ -260,11 +258,10 @@ export const TEMPLATES = /** @type {Templates} */ ({
return div;
},
/**
* @param {Partial<ClassNames>} classNames
* @param {string} placeholderValue
*/
input({ input, inputCloned }, placeholderValue) {
input(
{ input, inputCloned }: Pick<ClassNames, 'input' | 'inputCloned'>,
placeholderValue: string,
): HTMLInputElement {
const inp = Object.assign(document.createElement('input'), {
type: 'text',
className: `${input} ${inputCloned}`,
@ -280,10 +277,10 @@ export const TEMPLATES = /** @type {Templates} */ ({
return inp;
},
/**
* @param {Partial<ClassNames>} classNames
*/
dropdown({ list, listDropdown }) {
dropdown({
list,
listDropdown,
}: Pick<ClassNames, 'list' | 'listDropdown'>): HTMLDivElement {
const div = document.createElement('div');
div.classList.add(list, listDropdown);
@ -292,13 +289,16 @@ export const TEMPLATES = /** @type {Templates} */ ({
return div;
},
/**
*
* @param {Partial<ClassNames>} classNames
* @param {string} innerHTML
* @param {"no-choices" | "no-results" | ""} type
*/
notice({ item, itemChoice, noResults, noChoices }, innerHTML, type = '') {
notice(
{
item,
itemChoice,
noResults,
noChoices,
}: Pick<ClassNames, 'item' | 'itemChoice' | 'noResults' | 'noChoices'>,
innerHTML: string,
type: 'no-choices' | 'no-results' | '' = '',
): HTMLDivElement {
const classes = [item, itemChoice];
if (type === 'no-choices') {
@ -313,19 +313,23 @@ export const TEMPLATES = /** @type {Templates} */ ({
});
},
/**
* @param {Item} option
*/
option({ label, value, customProperties, active, disabled }) {
option({
label,
value,
customProperties,
active,
disabled,
}: Item): HTMLOptionElement {
const opt = new Option(label, value, false, active);
if (customProperties) {
opt.dataset.customProperties = customProperties;
opt.dataset.customProperties = `${customProperties}`;
}
opt.disabled = disabled;
opt.disabled = !!disabled;
return opt;
},
});
};
export default TEMPLATES;
export default templates;

18
tsconfig.json Normal file
View File

@ -0,0 +1,18 @@
{
"compilerOptions": {
"baseUrl": "/",
"outDir": "./dist/",
"allowJs": true,
"sourceMap": true,
"module": "commonjs",
"esModuleInterop": true,
"target": "es5",
"lib": ["es2017", "dom"],
"noUnusedLocals": true,
"noUnusedParameters": true,
"noImplicitAny": false,
"strictNullChecks": true,
"types": ["cypress"]
},
"include": ["./src/**/*", "./cypress/**/*"]
}

1041
types/index.d.ts vendored

File diff suppressed because it is too large Load Diff

View File

@ -8,12 +8,6 @@ const exclude = /node_modules/;
*/
module.exports = {
entry: ['./src/scripts/choices'],
output: {
library: 'Choices',
libraryTarget: 'window',
libraryExport: 'default',
globalObject: 'window',
},
module: {
rules: [
{
@ -28,13 +22,28 @@ module.exports = {
},
{
loader: 'babel-loader',
test: /\.js?$/,
test: /\.ts?$/,
include,
exclude,
options: {
babelrc: true,
},
},
{
loader: 'ts-loader',
test: /\.ts?$/,
include,
exclude
}
],
},
resolve: {
extensions: [".ts", ".js"]
},
output: {
library: 'Choices',
libraryTarget: 'window',
libraryExport: 'default',
globalObject: 'window',
},
};