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": { "parser": "@typescript-eslint/parser",
"ecmaVersion": 2020 "plugins": ["@typescript-eslint", "prettier", "sort-class-members"],
},
"extends": [ "extends": [
"airbnb-base", "airbnb-base",
"plugin:prettier/recommended", "plugin:prettier/recommended",
"plugin:compat/recommended" "plugin:compat/recommended",
"plugin:@typescript-eslint/recommended"
], ],
"plugins": ["prettier", "sort-class-members"],
"env": { "env": {
"es6": true, "es6": true,
"browser": true "browser": true,
"mocha": true,
"cypress/globals": true
},
"parserOptions": {
"project": "./tsconfig.json",
"ecmaVersion": 2020
}, },
"rules": { "rules": {
"import/prefer-default-export": "off", "import/prefer-default-export": "off",
@ -54,18 +59,23 @@
], ],
"accessorPairPositioning": "getThenSet" "accessorPairPositioning": "getThenSet"
} }
] ],
"lines-between-class-members": "off",
"@typescript-eslint/no-namespace": "off"
}, },
"overrides": [ "overrides": [
{ {
"files": ["*.test.js"], "files": ["*.test.ts"],
"env": { "env": {
"mocha": true "mocha": true
}, },
"rules": { "rules": {
"no-restricted-syntax": "off", "no-restricted-syntax": "off",
"compat/compat": "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.classList",
"Element.prototype.closest", "Element.prototype.closest",
"Element.prototype.dataset" "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 - name: run eslint
run: | run: |
CHANGED_JS=$(git --no-pager diff --name-only ..origin/master | grep '^src\/scripts\/.*\.js$' | xargs ls -d 2>/dev/null | paste -sd " " -) 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_JS}) ]]; then CHANGED_JS="src/scripts"; fi if [[ -z $(sed -e 's/[[:space:]]*$//' <<<${CHANGED_TS}) ]]; then CHANGED_TS="src/scripts"; fi
echo $CHANGED_JS echo $CHANGED_TS
node node_modules/eslint/bin/eslint.js $CHANGED_JS node node_modules/eslint/bin/eslint.js $CHANGED_TS
- name: Lint JS bundle - name: Lint JS bundle
run: | 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: require:
- '@babel/register' - 'ts-node/register'
- './config/jsdom.js' - './config/jsdom.js'
exit: true exit: true
spec: src/**/**/*.test.ts
spec: src/**/*.test.js extension:
- ts
- js

View File

@ -59,5 +59,8 @@
"fileMatch": [".prettierrc.json"], "fileMatch": [".prettierrc.json"],
"url": "http://json.schemastore.org/prettierrc" "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==", "integrity": "sha512-+iTbntw2IZPb/anVDbypzfQa+ay64MW0Zo8aJ8gZPWMMK6/OubMVb6lUPMagqjOPnmtauXnFCACVl3O7ogjeqQ==",
"dev": true "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": { "@types/events": {
"version": "3.0.0", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/@types/events/-/events-3.0.0.tgz", "resolved": "https://registry.npmjs.org/@types/events/-/events-3.0.0.tgz",
@ -967,12 +979,24 @@
"@types/node": "*" "@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": { "@types/minimatch": {
"version": "3.0.3", "version": "3.0.3",
"resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.3.tgz", "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.3.tgz",
"integrity": "sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA==", "integrity": "sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA==",
"dev": true "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": { "@types/node": {
"version": "12.11.2", "version": "12.11.2",
"resolved": "https://registry.npmjs.org/@types/node/-/node-12.11.2.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-12.11.2.tgz",
@ -985,12 +1009,109 @@
"integrity": "sha512-f5j5b/Gf71L+dbqxIpQ4Z2WlmI/mPJ0fOkGGmFgtb6sAu97EPczzbS3/tJKxmcYDj55OX6ssqwDAWOHIYDRDGA==", "integrity": "sha512-f5j5b/Gf71L+dbqxIpQ4Z2WlmI/mPJ0fOkGGmFgtb6sAu97EPczzbS3/tJKxmcYDj55OX6ssqwDAWOHIYDRDGA==",
"dev": true "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": { "@types/sizzle": {
"version": "2.3.2", "version": "2.3.2",
"resolved": "https://registry.npmjs.org/@types/sizzle/-/sizzle-2.3.2.tgz", "resolved": "https://registry.npmjs.org/@types/sizzle/-/sizzle-2.3.2.tgz",
"integrity": "sha512-7EJYyKTL7tFR8+gDbB6Wwz/arpGa0Mywk1TJbNzKzHtzbwVmY4HR9WqS5VV7dsBUKQmPNr192jHr/VpBluj/hg==", "integrity": "sha512-7EJYyKTL7tFR8+gDbB6Wwz/arpGa0Mywk1TJbNzKzHtzbwVmY4HR9WqS5VV7dsBUKQmPNr192jHr/VpBluj/hg==",
"dev": true "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": { "@webassemblyjs/ast": {
"version": "1.8.5", "version": "1.8.5",
"resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.8.5.tgz", "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.8.5.tgz",
@ -1413,6 +1534,12 @@
"readable-stream": "^2.0.6" "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": { "argparse": {
"version": "1.0.10", "version": "1.0.10",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
@ -3681,9 +3808,9 @@
} }
}, },
"eslint": { "eslint": {
"version": "6.6.0", "version": "6.8.0",
"resolved": "https://registry.npmjs.org/eslint/-/eslint-6.6.0.tgz", "resolved": "https://registry.npmjs.org/eslint/-/eslint-6.8.0.tgz",
"integrity": "sha512-PpEBq7b6qY/qrOmpYQ/jTMDYfuQMELR4g4WI1M/NaSDDD/bdcMb+dj4Hgks7p41kW2caXsPsEZAEAyAgjVVC0g==", "integrity": "sha512-K+Iayyo2LtyYhDSYwz5D5QdWw0hCacNzyq1Y821Xna2xSJj7cijoLLYmLxTQgcgZ9mC61nryMy9S7GRbYpI5Ig==",
"dev": true, "dev": true,
"requires": { "requires": {
"@babel/code-frame": "^7.0.0", "@babel/code-frame": "^7.0.0",
@ -3701,7 +3828,7 @@
"file-entry-cache": "^5.0.1", "file-entry-cache": "^5.0.1",
"functional-red-black-tree": "^1.0.1", "functional-red-black-tree": "^1.0.1",
"glob-parent": "^5.0.0", "glob-parent": "^5.0.0",
"globals": "^11.7.0", "globals": "^12.1.0",
"ignore": "^4.0.6", "ignore": "^4.0.6",
"import-fresh": "^3.0.0", "import-fresh": "^3.0.0",
"imurmurhash": "^0.1.4", "imurmurhash": "^0.1.4",
@ -3714,7 +3841,7 @@
"minimatch": "^3.0.4", "minimatch": "^3.0.4",
"mkdirp": "^0.5.1", "mkdirp": "^0.5.1",
"natural-compare": "^1.4.0", "natural-compare": "^1.4.0",
"optionator": "^0.8.2", "optionator": "^0.8.3",
"progress": "^2.0.0", "progress": "^2.0.0",
"regexpp": "^2.0.1", "regexpp": "^2.0.1",
"semver": "^6.1.2", "semver": "^6.1.2",
@ -3731,16 +3858,39 @@
"integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==",
"dev": true "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": { "import-fresh": {
"version": "3.1.0", "version": "3.2.1",
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.1.0.tgz", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.2.1.tgz",
"integrity": "sha512-PpuksHKGt8rXfWEr9m9EHIpgyyaltBy8+eF6GJM0QCAxMgxCfucMF3mjecK2QsJr0amJW7gTqh5/wht0z2UhEQ==", "integrity": "sha512-6e1q1cnWP2RXD9/keSkxHScg508CdXqXWgWBaETNhyuBFz+kUZlKboh+ISK+bU++DmbHimVBrOz/zzPe0sZ3sQ==",
"dev": true, "dev": true,
"requires": { "requires": {
"parent-module": "^1.0.0", "parent-module": "^1.0.0",
"resolve-from": "^4.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": { "resolve-from": {
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", "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", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.0.1.tgz",
"integrity": "sha512-VTyMAUfdm047mwKl+u79WIdrZxtFtn+nBxHeb844XBQ9uMNTuTHdx2hc5RiAJYqwTj3wc/xe5HLSdJSkJ+WfZw==", "integrity": "sha512-VTyMAUfdm047mwKl+u79WIdrZxtFtn+nBxHeb844XBQ9uMNTuTHdx2hc5RiAJYqwTj3wc/xe5HLSdJSkJ+WfZw==",
"dev": true "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": { "eslint-plugin-cypress": {
"version": "2.7.0", "version": "2.8.1",
"resolved": "https://registry.npmjs.org/eslint-plugin-cypress/-/eslint-plugin-cypress-2.7.0.tgz", "resolved": "https://registry.npmjs.org/eslint-plugin-cypress/-/eslint-plugin-cypress-2.8.1.tgz",
"integrity": "sha512-52Lq5ePCD/8jc536e1RqtLfj33BAy1s7BlYgCjbG39J5kqUitcTlRY5i3NRoeAyPHueDwETsq0eASF44ugLosQ==", "integrity": "sha512-jDpcP+MmjmqQO/x3bwIXgp4cl7Q66RYS5/IsuOQP4Qo2sEqE3DI8tTxBQ1EhnV5qEDd2Z2TYHR+5vYI6oCN4uw==",
"dev": true, "dev": true,
"requires": { "requires": {
"globals": "^11.12.0" "globals": "^11.12.0"
@ -5870,9 +6026,9 @@
"dev": true "dev": true
}, },
"fuse.js": { "fuse.js": {
"version": "3.4.5", "version": "3.4.6",
"resolved": "https://registry.npmjs.org/fuse.js/-/fuse.js-3.4.5.tgz", "resolved": "https://registry.npmjs.org/fuse.js/-/fuse.js-3.4.6.tgz",
"integrity": "sha512-s9PGTaQIkT69HaeoTVjwGsLfb8V8ScJLx5XGFcKHg0MqLUH/UZ4EKOtqtXX9k7AFqCGxD1aJmYb8Q5VYDibVRQ==" "integrity": "sha512-H6aJY4UpLFwxj1+5nAvufom5b2BT2v45P1MkPvdGIK8fWjQx/7o6tTT1+ALV0yawQvbmvCF0ufl2et8eJ7v7Cg=="
}, },
"gauge": { "gauge": {
"version": "2.7.4", "version": "2.7.4",
@ -6653,9 +6809,9 @@
"dev": true "dev": true
}, },
"inquirer": { "inquirer": {
"version": "7.0.0", "version": "7.0.1",
"resolved": "https://registry.npmjs.org/inquirer/-/inquirer-7.0.0.tgz", "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-7.0.1.tgz",
"integrity": "sha512-rSdC7zelHdRQFkWnhsMu2+2SO41mpv2oF2zy4tMhmiLWkcKbOAs87fWAJhVXttKVwhdZvymvnuM95EyEXg2/tQ==", "integrity": "sha512-V1FFQ3TIO15det8PijPLFR9M9baSlnRs9nL7zWu1MNVA2T9YVl9ZbrHJhYs7e9X8jeMZ3lr2JH/rdHFgNCBdYw==",
"dev": true, "dev": true,
"requires": { "requires": {
"ansi-escapes": "^4.2.1", "ansi-escapes": "^4.2.1",
@ -6667,25 +6823,25 @@
"lodash": "^4.17.15", "lodash": "^4.17.15",
"mute-stream": "0.0.8", "mute-stream": "0.0.8",
"run-async": "^2.2.0", "run-async": "^2.2.0",
"rxjs": "^6.4.0", "rxjs": "^6.5.3",
"string-width": "^4.1.0", "string-width": "^4.1.0",
"strip-ansi": "^5.1.0", "strip-ansi": "^5.1.0",
"through": "^2.3.6" "through": "^2.3.6"
}, },
"dependencies": { "dependencies": {
"ansi-escapes": { "ansi-escapes": {
"version": "4.2.1", "version": "4.3.0",
"resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.2.1.tgz", "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.0.tgz",
"integrity": "sha512-Cg3ymMAdN10wOk/VYfLV7KCQyv7EDirJ64500sU7n9UlmioEtDuU5Gd+hj73hXSU/ex7tHJSssmyftDdkMLO8Q==", "integrity": "sha512-EiYhwo0v255HUL6eDyuLrXEkTi7WwVCLAw+SeOQ7M7qdun1z1pum4DEm/nuqIVbPvi9RPPc9k9LbyBv6H0DwVg==",
"dev": true, "dev": true,
"requires": { "requires": {
"type-fest": "^0.5.2" "type-fest": "^0.8.1"
} }
}, },
"ansi-regex": { "ansi-regex": {
"version": "4.1.0", "version": "5.0.0",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz",
"integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==",
"dev": true "dev": true
}, },
"cli-cursor": { "cli-cursor": {
@ -6747,14 +6903,25 @@
} }
}, },
"string-width": { "string-width": {
"version": "4.1.0", "version": "4.2.0",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.1.0.tgz", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz",
"integrity": "sha512-NrX+1dVVh+6Y9dnQ19pR0pP4FiEIlUvdTGn8pw6CKTNq5sgib2nIhmUNT5TAmhWmvKr3WcxBcP3E8nWezuipuQ==", "integrity": "sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==",
"dev": true, "dev": true,
"requires": { "requires": {
"emoji-regex": "^8.0.0", "emoji-regex": "^8.0.0",
"is-fullwidth-code-point": "^3.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": { "strip-ansi": {
@ -6764,12 +6931,20 @@
"dev": true, "dev": true,
"requires": { "requires": {
"ansi-regex": "^4.1.0" "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": { "type-fest": {
"version": "0.5.2", "version": "0.8.1",
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.5.2.tgz", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz",
"integrity": "sha512-DWkS49EQKVX//Tbupb9TFa19c7+MK1XmzkrZUR8TAktmE/DizXoaoJV6TZ/tSIPXipqNiRI6CyAe7x69Jb6RSw==", "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==",
"dev": true "dev": true
} }
} }
@ -8064,6 +8239,12 @@
"integrity": "sha1-7dFMgk4sycHgsKG0K7UhBRakJDg=", "integrity": "sha1-7dFMgk4sycHgsKG0K7UhBRakJDg=",
"dev": true "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": { "log-symbols": {
"version": "2.2.0", "version": "2.2.0",
"resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-2.2.0.tgz", "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-2.2.0.tgz",
@ -8133,6 +8314,12 @@
"semver": "^5.6.0" "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": { "mamacro": {
"version": "0.0.3", "version": "0.0.3",
"resolved": "https://registry.npmjs.org/mamacro/-/mamacro-0.0.3.tgz", "resolved": "https://registry.npmjs.org/mamacro/-/mamacro-0.0.3.tgz",
@ -10794,9 +10981,9 @@
"dev": true "dev": true
}, },
"prettier": { "prettier": {
"version": "1.18.2", "version": "1.19.1",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-1.18.2.tgz", "resolved": "https://registry.npmjs.org/prettier/-/prettier-1.19.1.tgz",
"integrity": "sha512-OeHeMc0JhFE9idD4ZdtNibzY0+TPHSpSSb9h8FqtP+YnoZZ1sl8Vc9b1sasjfymH3SonAF4QcA2+mzHPhMvIiw==", "integrity": "sha512-s7PoyDv/II1ObgQunCbB9PdLmUcBZcnWOcxDh7O0N/UwDEsHyqkW+Qh28jW+mVuCdx7gLB0BotYI1Y6uI9iyew==",
"dev": true "dev": true
}, },
"prettier-linter-helpers": { "prettier-linter-helpers": {
@ -13195,12 +13382,63 @@
"glob": "^7.1.2" "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": { "tslib": {
"version": "1.10.0", "version": "1.10.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.10.0.tgz", "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.10.0.tgz",
"integrity": "sha512-qOebF53frne81cf0S9B41ByenJ3/IuH8yJKngAX35CmiZySA0khhkovshKK+jGCaMnVomla7gVlIcc3EvKPbTQ==", "integrity": "sha512-qOebF53frne81cf0S9B41ByenJ3/IuH8yJKngAX35CmiZySA0khhkovshKK+jGCaMnVomla7gVlIcc3EvKPbTQ==",
"dev": true "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": { "tty-browserify": {
"version": "0.0.0", "version": "0.0.0",
"resolved": "https://registry.npmjs.org/tty-browserify/-/tty-browserify-0.0.0.tgz", "resolved": "https://registry.npmjs.org/tty-browserify/-/tty-browserify-0.0.0.tgz",
@ -13259,6 +13497,12 @@
"integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=", "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=",
"dev": true "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": { "uglify-js": {
"version": "3.6.3", "version": "3.6.3",
"resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.6.3.tgz", "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": { "wordwrap": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", "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": { "scripts": {
"start": "run-p js:watch css:watch", "start": "run-p js:watch css:watch",
"build": "run-p js:build css:build", "build": "run-p js:build css:build",
"lint": "eslint src/scripts", "lint": "eslint src/scripts/**/*.ts",
"bundlesize": "bundlesize", "bundlesize": "bundlesize",
"cypress:run": "cypress run", "cypress:run": "cypress run",
"cypress:open": "cypress open", "cypress:open": "cypress open",
"cypress:ci": "cypress run --record --group $GITHUB_REF --ci-build-id $GITHUB_SHA", "cypress:ci": "cypress run --record --group $GITHUB_REF --ci-build-id $GITHUB_SHA",
"test": "run-s test:unit test:e2e", "test": "run-s test:unit test:e2e",
"test:unit": "NODE_ENV=test mocha", "test:unit": "TS_NODE_TRANSPILE_ONLY=true NODE_ENV=test mocha",
"test:unit:watch": "NODE_ENV=test mocha --watch --inspect=5556", "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:unit:coverage": "NODE_ENV=test nyc --reporter=lcov --reporter=text --reporter=text-summary mocha",
"test:e2e": "run-p --race start cypress:run", "test:e2e": "run-p --race start cypress:run",
"js:watch": "cross-env NODE_ENV=development node server.js", "js:watch": "cross-env NODE_ENV=development node server.js",
@ -56,6 +56,12 @@
"@babel/core": "^7.6.4", "@babel/core": "^7.6.4",
"@babel/preset-env": "^7.6.3", "@babel/preset-env": "^7.6.3",
"@babel/register": "^7.6.2", "@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", "autoprefixer": "^9.6.5",
"babel-loader": "^8.0.6", "babel-loader": "^8.0.6",
"bundlesize": "^0.18.0", "bundlesize": "^0.18.0",
@ -63,12 +69,12 @@
"cross-env": "^6.0.3", "cross-env": "^6.0.3",
"csso-cli": "^3.0.0", "csso-cli": "^3.0.0",
"cypress": "3.6.0", "cypress": "3.6.0",
"eslint": "^6.6.0", "eslint": "^6.8.0",
"eslint-config-airbnb-base": "^14.0.0", "eslint-config-airbnb-base": "^14.0.0",
"eslint-config-prettier": "^6.5.0", "eslint-config-prettier": "^6.5.0",
"eslint-loader": "^3.0.2", "eslint-loader": "^3.0.2",
"eslint-plugin-compat": "3.3.0", "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-import": "^2.18.2",
"eslint-plugin-prettier": "^3.1.1", "eslint-plugin-prettier": "^3.1.1",
"eslint-plugin-sort-class-members": "^1.6.0", "eslint-plugin-sort-class-members": "^1.6.0",
@ -82,9 +88,12 @@
"npm-run-all": "^4.1.5", "npm-run-all": "^4.1.5",
"nyc": "^14.1.1", "nyc": "^14.1.1",
"postcss-cli": "^6.1.3", "postcss-cli": "^6.1.3",
"prettier": "^1.18.2", "prettier": "^1.19.1",
"sinon": "^7.5.0", "sinon": "^7.5.0",
"sinon-chai": "^3.3.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": "^4.41.2",
"webpack-cli": "^3.3.9", "webpack-cli": "^3.3.9",
"webpack-dev-middleware": "^3.7.2", "webpack-dev-middleware": "^3.7.2",
@ -92,7 +101,7 @@
}, },
"dependencies": { "dependencies": {
"deepmerge": "^4.2.0", "deepmerge": "^4.2.0",
"fuse.js": "^3.4.5", "fuse.js": "^3.4.6",
"redux": "^4.0.4" "redux": "^4.0.4"
}, },
"npmName": "choices.js", "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" /> <meta name="theme-color" content="#ffffff" />
<!-- Ignore these --> <!-- Ignore these -->
<link <link rel="stylesheet" href="../../assets/styles/base.min.css" />
rel="stylesheet"
href="../../assets/styles/base.min.css?version=6.0.3"
/>
<!-- End ignore these --> <!-- End ignore these -->
<!-- Choices includes --> <!-- Choices includes -->
<link <link rel="stylesheet" href="../../assets/styles/choices.min.css" />
rel="stylesheet" <script src="../../assets/scripts/choices.min.js"></script>
href="../../assets/styles/choices.min.css?version=6.0.3"
/>
<script src="../../assets/scripts/choices.min.js?version=6.0.3"></script>
<!-- End Choices includes --> <!-- End Choices includes -->
</head> </head>

View File

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

View File

@ -43,18 +43,12 @@
<meta name="theme-color" content="#ffffff" /> <meta name="theme-color" content="#ffffff" />
<!-- Ignore these --> <!-- Ignore these -->
<link <link rel="stylesheet" href="../../assets/styles/base.min.css" />
rel="stylesheet"
href="../../assets/styles/base.min.css?version=6.0.3"
/>
<!-- End ignore these --> <!-- End ignore these -->
<!-- Choices includes --> <!-- Choices includes -->
<link <link rel="stylesheet" href="../../assets/styles/choices.min.css" />
rel="stylesheet" <script src="../../assets/scripts/choices.min.js"></script>
href="../../assets/styles/choices.min.css?version=6.0.3"
/>
<script src="../../assets/scripts/choices.min.js?version=6.0.3"></script>
<!-- End Choices includes --> <!-- End Choices includes -->
</head> </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', () => { it('returns ADD_CHOICE action', () => {
const value = 'test'; const value = 'test';
const label = 'test'; const label = 'test';
const id = 'test'; const id = 1;
const groupId = 'test'; const groupId = 1;
const disabled = false; const disabled = false;
const elementId = 'test'; const elementId = 1;
const customProperties = 'test'; const customProperties = { test: true };
const placeholder = 'test'; const placeholder = true;
const keyCode = 10; const keyCode = 10;
const expectedAction = { const expectedAction: actions.AddChoiceAction = {
type: 'ADD_CHOICE', type: 'ADD_CHOICE',
value, value,
label, label,
@ -46,7 +46,7 @@ describe('actions/choices', () => {
describe('filterChoices action', () => { describe('filterChoices action', () => {
it('returns FILTER_CHOICES action', () => { it('returns FILTER_CHOICES action', () => {
const results = Array(10); const results = Array(10);
const expectedAction = { const expectedAction: actions.FilterChoicesAction = {
type: 'FILTER_CHOICES', type: 'FILTER_CHOICES',
results, results,
}; };
@ -58,7 +58,7 @@ describe('actions/choices', () => {
describe('activateChoices action', () => { describe('activateChoices action', () => {
describe('not passing active parameter', () => { describe('not passing active parameter', () => {
it('returns ACTIVATE_CHOICES action', () => { it('returns ACTIVATE_CHOICES action', () => {
const expectedAction = { const expectedAction: actions.ActivateChoicesAction = {
type: 'ACTIVATE_CHOICES', type: 'ACTIVATE_CHOICES',
active: true, active: true,
}; };
@ -70,7 +70,7 @@ describe('actions/choices', () => {
describe('passing active parameter', () => { describe('passing active parameter', () => {
it('returns ACTIVATE_CHOICES action', () => { it('returns ACTIVATE_CHOICES action', () => {
const active = true; const active = true;
const expectedAction = { const expectedAction: actions.ActivateChoicesAction = {
type: 'ACTIVATE_CHOICES', type: 'ACTIVATE_CHOICES',
active, active,
}; };
@ -82,7 +82,7 @@ describe('actions/choices', () => {
describe('clearChoices action', () => { describe('clearChoices action', () => {
it('returns CLEAR_CHOICES action', () => { it('returns CLEAR_CHOICES action', () => {
const expectedAction = { const expectedAction: actions.ClearChoicesAction = {
type: 'CLEAR_CHOICES', 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', () => { describe('addGroup action', () => {
it('returns ADD_GROUP action', () => { it('returns ADD_GROUP action', () => {
const value = 'test'; const value = 'test';
const id = 'test'; const id = 1;
const active = true; const active = true;
const disabled = false; const disabled = false;
const expectedAction = {
const expectedAction: actions.AddGroupAction = {
type: 'ADD_GROUP', type: 'ADD_GROUP',
value, value,
id, 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', () => { it('returns ADD_ITEM action', () => {
const value = 'test'; const value = 'test';
const label = 'test'; const label = 'test';
const id = '1234'; const id = 1;
const choiceId = '1234'; const choiceId = 1;
const groupId = 'test'; const groupId = 1;
const customProperties = { test: true }; const customProperties = { test: true };
const placeholder = true; const placeholder = true;
const keyCode = 10; const keyCode = 10;
const expectedAction = { const expectedAction: actions.AddItemAction = {
type: 'ADD_ITEM', type: 'ADD_ITEM',
value, value,
label, label,
@ -42,9 +42,10 @@ describe('actions/items', () => {
describe('removeItem action', () => { describe('removeItem action', () => {
it('returns REMOVE_ITEM action', () => { it('returns REMOVE_ITEM action', () => {
const id = '1234'; const id = 1;
const choiceId = '1'; const choiceId = 1;
const expectedAction = {
const expectedAction: actions.RemoveItemAction = {
type: 'REMOVE_ITEM', type: 'REMOVE_ITEM',
id, id,
choiceId, choiceId,
@ -56,10 +57,10 @@ describe('actions/items', () => {
describe('highlightItem action', () => { describe('highlightItem action', () => {
it('returns HIGHLIGHT_ITEM action', () => { it('returns HIGHLIGHT_ITEM action', () => {
const id = '1234'; const id = 1;
const highlighted = true; const highlighted = true;
const expectedAction = { const expectedAction: actions.HighlightItemAction = {
type: 'HIGHLIGHT_ITEM', type: 'HIGHLIGHT_ITEM',
id, id,
highlighted, 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 { expect } from 'chai';
import * as actions from './misc'; import * as actions from './misc';
import { State } from '../interfaces';
describe('actions/misc', () => { describe('actions/misc', () => {
describe('clearAll action', () => { describe('clearAll action', () => {
it('returns CLEAR_ALL action', () => { it('returns CLEAR_ALL action', () => {
const expectedAction = { const expectedAction: actions.ClearAllAction = {
type: 'CLEAR_ALL', type: 'CLEAR_ALL',
}; };
@ -14,8 +15,13 @@ describe('actions/misc', () => {
describe('resetTo action', () => { describe('resetTo action', () => {
it('returns RESET_TO action', () => { it('returns RESET_TO action', () => {
const state = { test: true }; const state: State = {
const expectedAction = { choices: [],
items: [],
groups: [],
loading: false,
};
const expectedAction: actions.ResetToAction = {
type: 'RESET_TO', type: 'RESET_TO',
state, state,
}; };
@ -27,7 +33,7 @@ describe('actions/misc', () => {
describe('setIsLoading action', () => { describe('setIsLoading action', () => {
describe('setting loading state to true', () => { describe('setting loading state to true', () => {
it('returns expected action', () => { it('returns expected action', () => {
const expectedAction = { const expectedAction: actions.SetIsLoadingAction = {
type: 'SET_IS_LOADING', type: 'SET_IS_LOADING',
isLoading: true, isLoading: true,
}; };
@ -38,7 +44,7 @@ describe('actions/misc', () => {
describe('setting loading state to false', () => { describe('setting loading state to false', () => {
it('returns expected action', () => { it('returns expected action', () => {
const expectedAction = { const expectedAction: actions.SetIsLoadingAction = {
type: 'SET_IS_LOADING', type: 'SET_IS_LOADING',
isLoading: false, 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 sinonChai from 'sinon-chai';
import Choices from './choices'; import Choices from './choices';
import { EVENTS, ACTION_TYPES, DEFAULT_CONFIG, KEY_CODES } from './constants'; import { EVENTS, ACTION_TYPES, DEFAULT_CONFIG, KEY_CODES } from './constants';
import { WrappedSelect, WrappedInput } from './components/index'; import { WrappedSelect, WrappedInput } from './components/index';
import { removeItem } from './actions/items'; import { removeItem } from './actions/items';
import { Item, Choice, Group } from './interfaces';
import templates from './templates';
chai.use(sinonChai); chai.use(sinonChai);
@ -28,12 +31,6 @@ describe('choices', () => {
instance = null; instance = null;
}); });
const returnsInstance = () => {
it('returns this', () => {
expect(output).to.eql(instance);
});
};
describe('constructor', () => { describe('constructor', () => {
describe('config', () => { describe('config', () => {
describe('not passing config options', () => { describe('not passing config options', () => {
@ -88,7 +85,7 @@ describe('choices', () => {
`; `;
instance = new Choices('[data-choice]', { instance = new Choices('[data-choice]', {
renderSelectedChoices: 'test', renderSelectedChoices: 'test' as any,
}); });
expect(instance.config.renderSelectedChoices).to.equal('auto'); expect(instance.config.renderSelectedChoices).to.equal('auto');
@ -211,7 +208,7 @@ describe('choices', () => {
<input data-choice type="text" id="input-1" /> <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); expect(instance.passedElement).to.be.an.instanceOf(WrappedInput);
}); });
@ -223,7 +220,7 @@ describe('choices', () => {
<select data-choice id="select-1"></select> <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); expect(instance.passedElement).to.be.an.instanceOf(WrappedSelect);
}); });
@ -386,8 +383,8 @@ describe('choices', () => {
expect(clearStoreSpy.called).to.equal(true); expect(clearStoreSpy.called).to.equal(true);
}); });
it('nullifys templates config', () => { it('restes templates config', () => {
expect(instance._templates).to.equal(null); expect(instance._templates).to.deep.equal(templates);
}); });
it('resets initialise flag', () => { it('resets initialise flag', () => {
@ -423,7 +420,9 @@ describe('choices', () => {
output = instance.enable(); output = instance.enable();
}); });
returnsInstance(output); it('returns this', () => {
expect(output).to.eql(instance);
});
it('returns early', () => { it('returns early', () => {
expect(passedElementEnableSpy.called).to.equal(false); expect(passedElementEnableSpy.called).to.equal(false);
@ -481,7 +480,9 @@ describe('choices', () => {
output = instance.disable(); output = instance.disable();
}); });
returnsInstance(output); it('returns this', () => {
expect(output).to.eql(instance);
});
it('returns early', () => { it('returns early', () => {
expect(removeEventListenersSpy.called).to.equal(false); expect(removeEventListenersSpy.called).to.equal(false);
@ -638,7 +639,9 @@ describe('choices', () => {
output = instance.hideDropdown(); output = instance.hideDropdown();
}); });
returnsInstance(output); it('returns this', () => {
expect(output).to.eql(instance);
});
it('returns early', () => { it('returns early', () => {
expect(containerOuterCloseSpy.called).to.equal(false); expect(containerOuterCloseSpy.called).to.equal(false);
@ -735,7 +738,9 @@ describe('choices', () => {
output = instance.highlightItem(); output = instance.highlightItem();
}); });
returnsInstance(output); it('returns this', () => {
expect(output).to.eql(instance);
});
it('returns early', () => { it('returns early', () => {
expect(passedElementTriggerEventStub.called).to.equal(false); expect(passedElementTriggerEventStub.called).to.equal(false);
@ -745,7 +750,7 @@ describe('choices', () => {
}); });
describe('item passed', () => { describe('item passed', () => {
const item = { const item: Item = {
id: 1234, id: 1234,
value: 'Test', value: 'Test',
label: 'Test', label: 'Test',
@ -756,7 +761,9 @@ describe('choices', () => {
output = instance.highlightItem(item, true); output = instance.highlightItem(item, true);
}); });
returnsInstance(output); it('returns this', () => {
expect(output).to.eql(instance);
});
it('dispatches highlightItem action with correct arguments', () => { it('dispatches highlightItem action with correct arguments', () => {
expect(storeDispatchSpy.called).to.equal(true); expect(storeDispatchSpy.called).to.equal(true);
@ -817,7 +824,9 @@ describe('choices', () => {
expect(passedElementTriggerEventStub.called).to.equal(false); 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(); output = instance.unhighlightItem();
}); });
returnsInstance(output); it('returns this', () => {
expect(output).to.eql(instance);
});
it('returns early', () => { it('returns early', () => {
expect(passedElementTriggerEventStub.called).to.equal(false); expect(passedElementTriggerEventStub.called).to.equal(false);
@ -860,7 +871,7 @@ describe('choices', () => {
}); });
describe('item passed', () => { describe('item passed', () => {
const item = { const item: Item = {
id: 1234, id: 1234,
value: 'Test', value: 'Test',
label: 'Test', label: 'Test',
@ -871,7 +882,9 @@ describe('choices', () => {
output = instance.unhighlightItem(item, true); output = instance.unhighlightItem(item, true);
}); });
returnsInstance(output); it('returns this', () => {
expect(output).to.eql(instance);
});
it('dispatches highlightItem action with correct arguments', () => { it('dispatches highlightItem action with correct arguments', () => {
expect(storeDispatchSpy.called).to.equal(true); expect(storeDispatchSpy.called).to.equal(true);
@ -932,7 +945,9 @@ describe('choices', () => {
expect(passedElementTriggerEventStub.called).to.equal(false); expect(passedElementTriggerEventStub.called).to.equal(false);
}); });
returnsInstance(output); it('returns this', () => {
expect(output).to.eql(instance);
});
}); });
}); });
}); });
@ -966,7 +981,9 @@ describe('choices', () => {
storeGetItemsStub.reset(); storeGetItemsStub.reset();
}); });
returnsInstance(output); it('returns this', () => {
expect(output).to.eql(instance);
});
it('highlights each item in store', () => { it('highlights each item in store', () => {
expect(highlightItemStub.callCount).to.equal(items.length); expect(highlightItemStub.callCount).to.equal(items.length);
@ -1004,7 +1021,9 @@ describe('choices', () => {
storeGetItemsStub.reset(); storeGetItemsStub.reset();
}); });
returnsInstance(output); it('returns this', () => {
expect(output).to.eql(instance);
});
it('unhighlights each item in store', () => { it('unhighlights each item in store', () => {
expect(unhighlightItemStub.callCount).to.equal(items.length); expect(unhighlightItemStub.callCount).to.equal(items.length);
@ -1027,7 +1046,9 @@ describe('choices', () => {
instance._store.dispatch.reset(); instance._store.dispatch.reset();
}); });
returnsInstance(output); it('returns this', () => {
expect(output).to.eql(instance);
});
it('dispatches clearChoices action', () => { it('dispatches clearChoices action', () => {
expect(storeDispatchStub.lastCall.args[0]).to.eql({ expect(storeDispatchStub.lastCall.args[0]).to.eql({
@ -1050,7 +1071,9 @@ describe('choices', () => {
instance._store.dispatch.reset(); instance._store.dispatch.reset();
}); });
returnsInstance(output); it('returns this', () => {
expect(output).to.eql(instance);
});
it('dispatches clearAll action', () => { it('dispatches clearAll action', () => {
expect(storeDispatchStub.lastCall.args[0]).to.eql({ expect(storeDispatchStub.lastCall.args[0]).to.eql({
@ -1075,7 +1098,9 @@ describe('choices', () => {
instance._store.dispatch.reset(); instance._store.dispatch.reset();
}); });
returnsInstance(output); it('returns this', () => {
expect(output).to.eql(instance);
});
describe('text element', () => { describe('text element', () => {
beforeEach(() => { beforeEach(() => {
@ -1164,14 +1189,14 @@ describe('choices', () => {
const handleLoadingStateSpy = spy(choice, '_handleLoadingState'); const handleLoadingStateSpy = spy(choice, '_handleLoadingState');
let fetcherCalled = false; let fetcherCalled = false;
const fetcher = async inst => { const fetcher = async (inst): Promise<Choice[]> => {
expect(inst).to.eq(choice); expect(inst).to.eq(choice);
fetcherCalled = true; fetcherCalled = true;
await new Promise(resolve => setTimeout(resolve, 800)); await new Promise(resolve => setTimeout(resolve, 800));
return [ return [
{ label: 'l1', value: 'v1', customProperties: 'prop1' }, { label: 'l1', value: 'v1', customProperties: { prop1: true } },
{ label: 'l2', value: 'v2', customProperties: 'prop2' }, { label: 'l2', value: 'v2', customProperties: { prop2: false } },
]; ];
}; };
expect(choice._store.choices.length).to.equal(0); expect(choice._store.choices.length).to.equal(0);
@ -1182,7 +1207,9 @@ describe('choices', () => {
expect(handleLoadingStateSpy.callCount).to.equal(2); expect(handleLoadingStateSpy.callCount).to.equal(2);
expect(choice._store.choices[1].value).to.equal('v2'); expect(choice._store.choices[1].value).to.equal('v2');
expect(choice._store.choices[1].label).to.equal('l2'); 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); output = instance.setValue(values);
}); });
returnsInstance(output); it('returns this', () => {
expect(output).to.eql(instance);
});
it('returns early', () => { it('returns early', () => {
expect(setChoiceOrItemStub.called).to.equal(false); expect(setChoiceOrItemStub.called).to.equal(false);
@ -1224,7 +1253,9 @@ describe('choices', () => {
output = instance.setValue(values); output = instance.setValue(values);
}); });
returnsInstance(output); it('returns this', () => {
expect(output).to.eql(instance);
});
it('sets each value', () => { it('sets each value', () => {
expect(setChoiceOrItemStub.callCount).to.equal(2); expect(setChoiceOrItemStub.callCount).to.equal(2);
@ -1252,7 +1283,9 @@ describe('choices', () => {
output = instance.setChoiceByValue([]); output = instance.setChoiceByValue([]);
}); });
returnsInstance(output); it('returns this', () => {
expect(output).to.eql(instance);
});
it('returns early', () => { it('returns early', () => {
expect(findAndSelectChoiceByValueStub.called).to.equal(false); expect(findAndSelectChoiceByValueStub.called).to.equal(false);
@ -1272,7 +1305,9 @@ describe('choices', () => {
output = instance.setChoiceByValue(value); output = instance.setChoiceByValue(value);
}); });
returnsInstance(output); it('returns this', () => {
expect(output).to.eql(instance);
});
it('sets each choice with same value', () => { it('sets each choice with same value', () => {
expect(findAndSelectChoiceByValueStub.called).to.equal(true); expect(findAndSelectChoiceByValueStub.called).to.equal(true);
@ -1289,7 +1324,9 @@ describe('choices', () => {
output = instance.setChoiceByValue(values); output = instance.setChoiceByValue(values);
}); });
returnsInstance(output); it('returns this', () => {
expect(output).to.eql(instance);
});
it('sets each choice with same value', () => { it('sets each choice with same value', () => {
expect(findAndSelectChoiceByValueStub.callCount).to.equal(2); expect(findAndSelectChoiceByValueStub.callCount).to.equal(2);
@ -1509,7 +1546,9 @@ describe('choices', () => {
output = instance.removeHighlightedItems(); output = instance.removeHighlightedItems();
}); });
returnsInstance(output); it('returns this', () => {
expect(output).to.eql(instance);
});
it('removes each highlighted item in store', () => { it('removes each highlighted item in store', () => {
expect(removeItemStub.callCount).to.equal(2); expect(removeItemStub.callCount).to.equal(2);
@ -1521,7 +1560,9 @@ describe('choices', () => {
output = instance.removeHighlightedItems(true); output = instance.removeHighlightedItems(true);
}); });
returnsInstance(output); it('returns this', () => {
expect(output).to.eql(instance);
});
it('triggers event with item value', () => { it('triggers event with item value', () => {
expect(triggerChangeStub.callCount).to.equal(2); expect(triggerChangeStub.callCount).to.equal(2);
@ -1538,19 +1579,23 @@ describe('choices', () => {
let containerOuterRemoveLoadingStateStub; let containerOuterRemoveLoadingStateStub;
const value = 'value'; const value = 'value';
const label = 'label'; const label = 'label';
const choices = [ const choices: Choice[] = [
{ {
id: 1, id: 1,
value: '1', value: '1',
label: 'Test 1', label: 'Test 1',
selected: false,
disabled: false,
}, },
{ {
id: 2, id: 2,
value: '2', value: '2',
label: 'Test 2', label: 'Test 2',
selected: false,
disabled: true,
}, },
]; ];
const groups = [ const groups: Group[] = [
{ {
...choices[0], ...choices[0],
choices, choices,
@ -1634,10 +1679,10 @@ describe('choices', () => {
expect(call.args[0]).to.eql({ expect(call.args[0]).to.eql({
value: choices[index][value], value: choices[index][value],
label: choices[index][label], label: choices[index][label],
isSelected: choices[index].selected, isSelected: !!choices[index].selected,
isDisabled: choices[index].disabled, isDisabled: !!choices[index].disabled,
customProperties: choices[index].customProperties, customProperties: choices[index].customProperties,
placeholder: choices[index].placeholder, placeholder: !!choices[index].placeholder,
}); });
}); });
}); });
@ -1679,7 +1724,7 @@ describe('choices', () => {
describe('private methods', () => { describe('private methods', () => {
describe('_createGroupsFragment', () => { describe('_createGroupsFragment', () => {
let _createChoicesFragmentStub; let _createChoicesFragmentStub;
const choices = [ const choices: Choice[] = [
{ {
id: 1, id: 1,
selected: true, selected: true,
@ -1703,7 +1748,7 @@ describe('choices', () => {
}, },
]; ];
const groups = [ const groups: Group[] = [
{ {
id: 2, id: 2,
value: 'Group 2', value: 'Group 2',
@ -1925,12 +1970,12 @@ describe('choices', () => {
}); });
describe('when a placeholder option is not defined', () => { describe('when a placeholder option is not defined', () => {
it('returns false', () => { it('returns null', () => {
instance._isSelectElement = true; instance._isSelectElement = true;
instance.passedElement.placeholderOption = undefined; instance.passedElement.placeholderOption = undefined;
const value = instance._generatePlaceholderValue(); 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', () => { describe('when the placeholder attribute is not defined on the passed element', () => {
it('returns false', () => { it('returns null', () => {
instance._isSelectElement = false; instance._isSelectElement = false;
instance.config.placeholder = true; instance.config.placeholder = true;
instance.config.placeholderValue = undefined; instance.config.placeholderValue = undefined;
@ -1981,32 +2026,25 @@ describe('choices', () => {
}; };
const value = instance._generatePlaceholderValue(); const value = instance._generatePlaceholderValue();
expect(value).to.equal(false); expect(value).to.equal(null);
}); });
}); });
}); });
}); });
describe('when the placeholder config option is set to false', () => { describe('when the placeholder config option is set to false', () => {
it('returns false', () => { it('returns null', () => {
instance._isSelectElement = false; instance._isSelectElement = false;
instance.config.placeholder = false; instance.config.placeholder = false;
const value = instance._generatePlaceholderValue(); const value = instance._generatePlaceholderValue();
expect(value).to.equal(false); expect(value).to.equal(null);
}); });
}); });
}); });
}); });
describe('_getTemplate', () => { 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', () => { describe('when passing a template key', () => {
it('returns the generated template for the given template key', () => { it('returns the generated template for the given template key', () => {
const templateKey = 'test'; const templateKey = 'test';
@ -2028,130 +2066,96 @@ describe('choices', () => {
}); });
describe('_onKeyDown', () => { describe('_onKeyDown', () => {
let activeItems;
let hasItems;
let hasActiveDropdown;
let hasFocussedInput;
beforeEach(() => { beforeEach(() => {
instance.showDropdown = stub(); instance.showDropdown = stub();
instance._onAKey = stub(); instance._onSelectKey = stub();
instance._onEnterKey = stub(); instance._onEnterKey = stub();
instance._onEscapeKey = stub(); instance._onEscapeKey = stub();
instance._onDirectionKey = stub(); instance._onDirectionKey = stub();
instance._onDeleteKey = stub(); instance._onDeleteKey = stub();
({ activeItems } = instance._store);
hasItems = instance.itemList.hasChildren();
hasActiveDropdown = instance.dropdown.isActive;
hasFocussedInput = instance.input.isFocussed;
}); });
const scenarios = [ describe('direction key', () => {
{ const keyCodes = [
keyCode: KEY_CODES.BACK_KEY, KEY_CODES.UP_KEY,
expectedFunctionCall: '_onDeleteKey', KEY_CODES.DOWN_KEY,
}, KEY_CODES.PAGE_UP_KEY,
{ KEY_CODES.PAGE_DOWN_KEY,
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('when called with a keydown event', () => { keyCodes.forEach(keyCode => {
scenarios.forEach(({ keyCode, expectedFunctionCall }) => { it(`calls _onDirectionKey with the expected arguments`, () => {
describe(`when the keyCode is ${keyCode}`, () => { const event = {
it(`calls ${expectedFunctionCall} with the expected arguments`, () => { keyCode,
const mockEvent = { };
keyCode,
};
instance._onKeyDown(mockEvent); instance._onKeyDown(event);
expect(instance[expectedFunctionCall]).to.have.been.calledWith({ expect(instance._onDirectionKey).to.have.been.calledWith(
event: mockEvent, event,
activeItems: instance._store.activeItems, hasActiveDropdown,
hasActiveDropdown: instance.dropdown.isActive, );
hasFocusedInput: instance.input.isFocussed,
hasItems: instance.itemList.hasChildren(),
});
});
}); });
}); });
});
describe('select input', () => { describe('select key', () => {
describe('when the dropdown is not active', () => { it(`calls _onSelectKey with the expected arguments`, () => {
describe('when the key was alpha-numeric', () => { const event = {
beforeEach(() => { keyCode: KEY_CODES.A_KEY,
instance._isTextElement = false; };
instance.dropdown.isActive = false;
});
it('shows the dropdown', () => { instance._onKeyDown(event);
instance._onKeyDown({
keyCode: KEY_CODES.A_KEY,
});
expect(instance.showDropdown).to.have.been.calledWith(); expect(instance._onSelectKey).to.have.been.calledWith(
}); event,
hasItems,
);
});
});
describe('when the input is not focussed', () => { describe('enter key', () => {
beforeEach(() => { it(`calls _onEnterKey with the expected arguments`, () => {
instance.input.isFocussed = false; const event = {
}); keyCode: KEY_CODES.ENTER_KEY,
};
it('updates the input value with the character corresponding to the key code', () => { instance._onKeyDown(event);
instance._onKeyDown({
keyCode: KEY_CODES.A_KEY,
});
expect(instance.input.value).to.contain('a'); expect(instance._onEnterKey).to.have.been.calledWith(
}); event,
}); activeItems,
hasActiveDropdown,
);
});
});
describe('when the input is focussed', () => { describe('delete key', () => {
beforeEach(() => { const keyCodes = [KEY_CODES.DELETE_KEY, KEY_CODES.BACK_KEY];
instance.input.isFocussed = true;
});
it('does not update the input value', () => { keyCodes.forEach(keyCode => {
instance._onKeyDown({ it(`calls _onDeleteKey with the expected arguments`, () => {
keyCode: KEY_CODES.A_KEY, const event = {
}); keyCode,
};
expect(instance.input.value).to.not.contain('a'); instance._onKeyDown(event);
});
});
});
describe('when the input was not alpha-numeric', () => { expect(instance._onDeleteKey).to.have.been.calledWith(
beforeEach(() => { event,
instance._isTextElement = false; activeItems,
instance.dropdown.isActive = false; hasFocussedInput,
}); );
it('does not show the dropdown', () => {
instance._onKeyDown({
keyCode: KEY_CODES.DELETE_KEY,
});
expect(instance.showDropdown).to.not.have.been.called;
});
});
}); });
}); });
}); });

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,17 +1,11 @@
import { dispatchEvent } from '../lib/utils'; import { dispatchEvent } from '../lib/utils';
import { ClassNames, EventMap } from '../interfaces';
/**
* @typedef {import('../../../types/index').Choices.passedElement} passedElement
* @typedef {import('../../../types/index').Choices.ClassNames} ClassNames
*/
export default class WrappedElement { export default class WrappedElement {
/** element: HTMLInputElement | HTMLSelectElement;
* @param {{ classNames: ClassNames;
* element: HTMLInputElement | HTMLSelectElement, isDisabled: boolean;
* classNames: ClassNames,
* }} args
*/
constructor({ element, classNames }) { constructor({ element, classNames }) {
this.element = element; this.element = element;
this.classNames = classNames; this.classNames = classNames;
@ -26,24 +20,24 @@ export default class WrappedElement {
this.isDisabled = false; this.isDisabled = false;
} }
get isActive() { get isActive(): boolean {
return this.element.dataset.choice === 'active'; return this.element.dataset.choice === 'active';
} }
get dir() { get dir(): string {
return this.element.dir; return this.element.dir;
} }
get value() { get value(): string {
return this.element.value; return this.element.value;
} }
set value(value) { set value(value: string) {
// you must define setter here otherwise it will be readonly property // you must define setter here otherwise it will be readonly property
this.element.value = value; this.element.value = value;
} }
conceal() { conceal(): void {
// Hide passed input // Hide passed input
this.element.classList.add(this.classNames.input); this.element.classList.add(this.classNames.input);
this.element.hidden = true; this.element.hidden = true;
@ -61,7 +55,7 @@ export default class WrappedElement {
this.element.setAttribute('data-choice', 'active'); this.element.setAttribute('data-choice', 'active');
} }
reveal() { reveal(): void {
// Reinstate passed element // Reinstate passed element
this.element.classList.remove(this.classNames.input); this.element.classList.remove(this.classNames.input);
this.element.hidden = false; this.element.hidden = false;
@ -83,19 +77,19 @@ export default class WrappedElement {
this.element.value = this.element.value; // eslint-disable-line no-self-assign this.element.value = this.element.value; // eslint-disable-line no-self-assign
} }
enable() { enable(): void {
this.element.removeAttribute('disabled'); this.element.removeAttribute('disabled');
this.element.disabled = false; this.element.disabled = false;
this.isDisabled = false; this.isDisabled = false;
} }
disable() { disable(): void {
this.element.setAttribute('disabled', ''); this.element.setAttribute('disabled', '');
this.element.disabled = true; this.element.disabled = true;
this.isDisabled = true; this.isDisabled = true;
} }
triggerEvent(eventType, data) { triggerEvent<K extends keyof EventMap>(eventType: K, data?: object): void {
dispatchEvent(this.element, eventType, data); 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', () => { describe('inherited methods', () => {
['conceal', 'reveal', 'enable', 'disable'].forEach(method => { const methods: string[] = ['conceal', 'reveal', 'enable', 'disable'];
methods.forEach(method => {
describe(method, () => { describe(method, () => {
beforeEach(() => { beforeEach(() => {
stub(WrappedElement.prototype, method); stub(WrappedElement.prototype, method as keyof WrappedElement);
}); });
afterEach(() => { afterEach(() => {
@ -54,27 +56,11 @@ describe('components/wrappedInput', () => {
}); });
describe('value setter', () => { describe('value setter', () => {
const data = [ it('sets the value of the input to the given value', () => {
{ const newValue = 'Value 1, Value 2, Value 3';
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', () => {
expect(instance.element.value).to.equal(''); expect(instance.element.value).to.equal('');
instance.value = data; instance.value = newValue;
expect(instance.value).to.equal( expect(instance.value).to.equal(newValue);
`Value 1${delimiter}Value 2${delimiter}Value 3`,
);
}); });
}); });
}); });

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

View File

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

View File

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

View File

@ -1,12 +1,13 @@
import { sanitise, sortByAlpha } from './lib/utils'; import { sanitise, sortByAlpha } from './lib/utils';
import {
Options,
ClassNames,
EventMap,
ActionType,
KeyCodeMap,
} from './interfaces';
/** export const DEFAULT_CLASSNAMES: ClassNames = {
* @typedef {import('../../types/index').Choices.ClassNames} ClassNames
* @typedef {import('../../types/index').Choices.Options} Options
*/
/** @type {ClassNames} */
export const DEFAULT_CLASSNAMES = {
containerOuter: 'choices', containerOuter: 'choices',
containerInner: 'choices__inner', containerInner: 'choices__inner',
input: 'choices__input', input: 'choices__input',
@ -35,8 +36,7 @@ export const DEFAULT_CLASSNAMES = {
noChoices: 'has-no-choices', noChoices: 'has-no-choices',
}; };
/** @type {Options} */ export const DEFAULT_CONFIG: Options = {
export const DEFAULT_CONFIG = {
items: [], items: [],
choices: [], choices: [],
silent: false, silent: false,
@ -83,7 +83,7 @@ export const DEFAULT_CONFIG = {
classNames: DEFAULT_CLASSNAMES, classNames: DEFAULT_CLASSNAMES,
}; };
export const EVENTS = { export const EVENTS: Record<keyof EventMap, keyof EventMap> = {
showDropdown: 'showDropdown', showDropdown: 'showDropdown',
hideDropdown: 'hideDropdown', hideDropdown: 'hideDropdown',
change: 'change', change: 'change',
@ -93,9 +93,10 @@ export const EVENTS = {
removeItem: 'removeItem', removeItem: 'removeItem',
highlightItem: 'highlightItem', highlightItem: 'highlightItem',
highlightChoice: 'highlightChoice', highlightChoice: 'highlightChoice',
unhighlightItem: 'unhighlightItem',
}; };
export const ACTION_TYPES = { export const ACTION_TYPES: Record<ActionType, ActionType> = {
ADD_CHOICE: 'ADD_CHOICE', ADD_CHOICE: 'ADD_CHOICE',
FILTER_CHOICES: 'FILTER_CHOICES', FILTER_CHOICES: 'FILTER_CHOICES',
ACTIVATE_CHOICES: 'ACTIVATE_CHOICES', ACTIVATE_CHOICES: 'ACTIVATE_CHOICES',
@ -105,9 +106,11 @@ export const ACTION_TYPES = {
REMOVE_ITEM: 'REMOVE_ITEM', REMOVE_ITEM: 'REMOVE_ITEM',
HIGHLIGHT_ITEM: 'HIGHLIGHT_ITEM', HIGHLIGHT_ITEM: 'HIGHLIGHT_ITEM',
CLEAR_ALL: 'CLEAR_ALL', 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, BACK_KEY: 46,
DELETE_KEY: 8, DELETE_KEY: 8,
ENTER_KEY: 13, ENTER_KEY: 13,
@ -119,8 +122,9 @@ export const KEY_CODES = {
PAGE_DOWN_KEY: 34, PAGE_DOWN_KEY: 34,
}; };
export const TEXT_TYPE = 'text'; export const TEXT_TYPE: HTMLInputElement['type'] = 'text';
export const SELECT_ONE_TYPE = 'select-one'; export const SELECT_ONE_TYPE: HTMLSelectElement['type'] = 'select-one';
export const SELECT_MULTIPLE_TYPE = 'select-multiple'; export const SELECT_MULTIPLE_TYPE: HTMLSelectElement['type'] =
'select-multiple';
export const SCROLLING_SPEED = 4; 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 { expect } from 'chai';
import { stub } from 'sinon'; import { stub } from 'sinon';
import { import {
@ -140,19 +141,19 @@ describe('utils', () => {
it('sorts by label alphabetically', () => { it('sorts by label alphabetically', () => {
const values = [ const values = [
{ label: 'The Strokes' }, { value: '0', label: 'The Strokes' },
{ label: 'Arctic Monkeys' }, { value: '0', label: 'Arctic Monkeys' },
{ label: 'Oasis' }, { value: '0', label: 'Oasis' },
{ label: 'Tame Impala' }, { value: '0', label: 'Tame Impala' },
]; ];
const output = values.sort(sortByAlpha); const output = values.sort(sortByAlpha);
expect(output).to.eql([ expect(output).to.eql([
{ label: 'Arctic Monkeys' }, { value: '0', label: 'Arctic Monkeys' },
{ label: 'Oasis' }, { value: '0', label: 'Oasis' },
{ label: 'Tame Impala' }, { value: '0', label: 'Tame Impala' },
{ label: 'The Strokes' }, { value: '0', label: 'The Strokes' },
]); ]);
}); });
}); });
@ -185,12 +186,12 @@ describe('utils', () => {
const fakeElement = { const fakeElement = {
dispatchEvent: stub(), dispatchEvent: stub(),
}; };
const eventType = 'testEvent'; const eventType = 'addItem';
const customArgs = { const customArgs = {
testing: true, testing: true,
}; };
dispatchEvent(fakeElement, eventType, customArgs); dispatchEvent(fakeElement as any, eventType, customArgs);
expect(fakeElement.dispatchEvent.called).to.equal(true); expect(fakeElement.dispatchEvent.called).to.equal(true);
const event = fakeElement.dispatchEvent.lastCall.args[0]; 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 { expect } from 'chai';
import choices, { defaultState } from './choices'; import choices, { defaultState } from './choices';
import { Choice } from '../interfaces';
describe('reducers/choices', () => { describe('reducers/choices', () => {
it('should return same state when no action matches', () => { 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('when choices do not exist', () => {
describe('ADD_CHOICE', () => { describe('ADD_CHOICE', () => {
const value = 'test'; const value = 'test';
const label = 'test'; const label = 'test';
const id = 'test'; const id = 1;
const groupId = 'test'; const groupId = 1;
const disabled = false; const disabled = false;
const elementId = 'test'; const elementId = 1;
const customProperties = 'test'; const customProperties = { test: true };
const placeholder = 'test'; const placeholder = true;
describe('passing expected values', () => { describe('passing expected values', () => {
it('adds choice', () => { it('adds choice', () => {
@ -32,7 +33,6 @@ describe('reducers/choices', () => {
selected: false, selected: false,
active: true, active: true,
score: 9999, score: 9999,
keyCode: null,
}, },
]; ];
@ -68,14 +68,13 @@ describe('reducers/choices', () => {
selected: false, selected: false,
active: true, active: true,
score: 9999, score: 9999,
keyCode: null,
}, },
]; ];
const actualResponse = choices(undefined, { const actualResponse = choices(undefined, {
type: 'ADD_CHOICE', type: 'ADD_CHOICE',
value, value,
label: null, label: undefined,
id, id,
groupId, groupId,
disabled, disabled,
@ -103,14 +102,13 @@ describe('reducers/choices', () => {
selected: false, selected: false,
active: true, active: true,
score: 9999, score: 9999,
keyCode: null,
}, },
]; ];
const actualResponse = choices(undefined, { const actualResponse = choices(undefined, {
type: 'ADD_CHOICE', type: 'ADD_CHOICE',
value, value,
label: null, label: undefined,
id, id,
groupId, groupId,
disabled, disabled,
@ -143,7 +141,6 @@ describe('reducers/choices', () => {
score: 9999, score: 9999,
customProperties: null, customProperties: null,
placeholder: false, placeholder: false,
keyCode: null,
}, },
{ {
id: 2, id: 2,
@ -157,7 +154,6 @@ describe('reducers/choices', () => {
score: 9999, score: 9999,
customProperties: null, customProperties: null,
placeholder: false, placeholder: false,
keyCode: null,
}, },
]; ];
}); });
@ -178,9 +174,7 @@ describe('reducers/choices', () => {
type: 'FILTER_CHOICES', type: 'FILTER_CHOICES',
results: [ results: [
{ {
item: { item: { id } as Choice,
id,
},
score, score,
}, },
], ],
@ -227,45 +221,34 @@ describe('reducers/choices', () => {
}); });
describe('ADD_ITEM', () => { describe('ADD_ITEM', () => {
it('disables choice if action has choice id', () => { describe('when action has a choice id', () => {
const id = 2; it('disables choice corresponding with id', () => {
const clonedState = state.slice(0); const id = 2;
const expectedResponse = [ const clonedState = state.slice(0);
{ const expectedResponse = [
...state[0], {
}, ...state[0],
{ },
...state[1], {
selected: true, ...state[1],
}, selected: true,
]; },
];
const actualResponse = choices(clonedState, { const actualResponse = choices(clonedState, {
type: 'ADD_ITEM', type: 'ADD_ITEM',
choiceId: id, choiceId: id,
});
expect(actualResponse).to.eql(expectedResponse);
}); });
expect(actualResponse).to.eql(expectedResponse);
}); });
it('activates all choices if activateOptions flag passed', () => { describe('when action has no choice id', () => {
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', () => {
it('returns state', () => { it('returns state', () => {
const clonedState = state.slice(0); const clonedState = state.slice(0);
const actualResponse = choices(clonedState, { const actualResponse = choices(clonedState, {
type: 'ADD_ITEM', type: 'ADD_ITEM',
activateOptions: false,
choiceId: undefined, 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', () => { describe('reducers/groups', () => {
it('should return same state when no action matches', () => { 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('when groups do not exist', () => {
describe('ADD_GROUP', () => { describe('ADD_GROUP', () => {
it('adds group', () => { it('adds group', () => {
const id = '1'; const id = 1;
const value = 'Group one'; const value = 'Group one';
const active = true; const active = true;
const disabled = false; 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 { createStore } from 'redux';
import { expect } from 'chai'; import { expect } from 'chai';
import rootReducer from './index'; import rootReducer from '.';
import groups from './groups'; import groups from './groups';
import choices from './choices'; import choices from './choices';
import items from './items'; import items from './items';
import loading from './loading';
describe('reducers/rootReducer', () => { describe('reducers/rootReducer', () => {
const store = createStore(rootReducer); const store = createStore(rootReducer);
@ -11,9 +12,10 @@ describe('reducers/rootReducer', () => {
it('returns expected reducers', () => { it('returns expected reducers', () => {
const state = store.getState(); const state = store.getState();
expect(state.groups).to.equal(groups(undefined, {})); expect(state.groups).to.equal(groups(undefined, {} as any));
expect(state.choices).to.equal(choices(undefined, {})); expect(state.choices).to.equal(choices(undefined, {} as any));
expect(state.items).to.equal(items(undefined, {})); expect(state.items).to.equal(items(undefined, {} as any));
expect(state.loading).to.equal(loading(undefined, {} as any));
}); });
describe('CLEAR_ALL', () => { describe('CLEAR_ALL', () => {
@ -33,9 +35,7 @@ describe('reducers/rootReducer', () => {
items: [], items: [],
groups: [], groups: [],
choices: [], choices: [],
general: { loading: false,
loading: false,
},
}); });
}); });
}); });

View File

@ -2,24 +2,31 @@ import { combineReducers } from 'redux';
import items from './items'; import items from './items';
import groups from './groups'; import groups from './groups';
import choices from './choices'; import choices from './choices';
import general from './general'; import loading from './loading';
import { cloneObject } from '../lib/utils'; import { cloneObject } from '../lib/utils';
export const defaultState = {
groups: [],
items: [],
choices: [],
loading: false,
};
const appReducer = combineReducers({ const appReducer = combineReducers({
items, items,
groups, groups,
choices, choices,
general, loading,
}); });
const rootReducer = (passedState, action) => { const rootReducer = (passedState, action): object => {
let state = passedState; let state = passedState;
// If we are clearing all items, groups and options we reassign // If we are clearing all items, groups and options we reassign
// state and then pass that state to our proper reducer. This isn't // state and then pass that state to our proper reducer. This isn't
// mutating our actual state // mutating our actual state
// See: http://stackoverflow.com/a/35641992 // See: http://stackoverflow.com/a/35641992
if (action.type === 'CLEAR_ALL') { if (action.type === 'CLEAR_ALL') {
state = undefined; state = defaultState;
} else if (action.type === 'RESET_TO') { } else if (action.type === 'RESET_TO') {
return cloneObject(action.state); 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 { expect } from 'chai';
import items, { defaultState } from './items'; import items, { defaultState } from './items';
import { RemoveItemAction } from '../actions/items';
describe('reducers/items', () => { describe('reducers/items', () => {
it('should return same state when no action matches', () => { 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', () => { describe('when items do not exist', () => {
@ -16,7 +17,7 @@ describe('reducers/items', () => {
const customProperties = { const customProperties = {
property: 'value', property: 'value',
}; };
const placeholder = 'This is a placeholder'; const placeholder = true;
const keyCode = 10; const keyCode = 10;
describe('passing expected values', () => { describe('passing expected values', () => {
@ -148,7 +149,7 @@ describe('reducers/items', () => {
const actualResponse = items(clonedState, { const actualResponse = items(clonedState, {
type: 'REMOVE_ITEM', type: 'REMOVE_ITEM',
id, id,
}); } as RemoveItemAction);
expect(actualResponse).to.eql(expectedResponse); 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 { 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', () => { 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', () => { describe('SET_IS_LOADING', () => {
it('sets loading state', () => { it('sets loading state', () => {
const expectedState = { const expectedState = true;
loading: true,
};
const actualState = general(undefined, { const actualState = general(undefined, {
type: 'SET_IS_LOADING', 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', () => { describe('subscribe', () => {
it('wraps redux subscribe method', () => { it('wraps redux subscribe method', () => {
const onChange = () => {}; const onChange = (): void => {};
expect(subscribeStub.callCount).to.equal(0); expect(subscribeStub.callCount).to.equal(0);
instance.subscribe(onChange); instance.subscribe(onChange);
expect(subscribeStub.callCount).to.equal(1); 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'; import rootReducer from '../reducers/index';
import { Choice, Group, Item, State } from '../interfaces';
/**
* @typedef {import('../../../types/index').Choices.Choice} Choice
* @typedef {import('../../../types/index').Choices.Group} Group
* @typedef {import('../../../types/index').Choices.Item} Item
*/
export default class Store { export default class Store {
_store: IStore;
constructor() { constructor() {
this._store = createStore( this._store = createStore(
rootReducer, rootReducer,
window.__REDUX_DEVTOOLS_EXTENSION__ && (window as any).__REDUX_DEVTOOLS_EXTENSION__ &&
window.__REDUX_DEVTOOLS_EXTENSION__(), (window as any).__REDUX_DEVTOOLS_EXTENSION__(),
); );
} }
/** /**
* Subscribe store to function call (wrapped Redux method) * 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); this._store.subscribe(onChange);
} }
/** /**
* Dispatch event to store (wrapped Redux method) * 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); this._store.dispatch(action);
} }
/** /**
* Get store object (wrapping Redux method) * Get store object (wrapping Redux method)
* @returns {object} State
*/ */
get state() { get state(): State {
return this._store.getState(); return this._store.getState();
} }
/** /**
* Get items from store * Get items from store
* @returns {Item[]} Item objects
*/ */
get items() { get items(): Item[] {
return this.state.items; return this.state.items;
} }
/** /**
* Get active items from store * Get active items from store
* @returns {Item[]} Item objects
*/ */
get activeItems() { get activeItems(): Item[] {
return this.items.filter(item => item.active === true); return this.items.filter(item => item.active === true);
} }
/** /**
* Get highlighted items from store * Get highlighted items from store
* @returns {Item[]} Item objects
*/ */
get highlightedActiveItems() { get highlightedActiveItems(): Item[] {
return this.items.filter(item => item.active && item.highlighted); return this.items.filter(item => item.active && item.highlighted);
} }
/** /**
* Get choices from store * Get choices from store
* @returns {Choice[]} Option objects
*/ */
get choices() { get choices(): Choice[] {
return this.state.choices; return this.state.choices;
} }
/** /**
* Get active choices from store * Get active choices from store
* @returns {Choice[]} Option objects
*/ */
get activeChoices() { get activeChoices(): Choice[] {
return this.choices.filter(choice => choice.active === true); return this.choices.filter(choice => choice.active === true);
} }
/** /**
* Get selectable choices from store * Get selectable choices from store
* @returns {Choice[]} Option objects
*/ */
get selectableChoices() { get selectableChoices(): Choice[] {
return this.choices.filter(choice => choice.disabled !== true); return this.choices.filter(choice => choice.disabled !== true);
} }
/** /**
* Get choices that can be searched (excluding placeholders) * 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); return this.selectableChoices.filter(choice => choice.placeholder !== true);
} }
/** /**
* Get placeholder choice from store * Get placeholder choice from store
* @returns {Choice | undefined} Found placeholder
*/ */
get placeholderChoice() { get placeholderChoice(): Choice | undefined {
return [...this.choices] return [...this.choices]
.reverse() .reverse()
.find(choice => choice.placeholder === true); .find(choice => choice.placeholder === true);
@ -110,17 +95,15 @@ export default class Store {
/** /**
* Get groups from store * Get groups from store
* @returns {Group[]} Group objects
*/ */
get groups() { get groups(): Group[] {
return this.state.groups; return this.state.groups;
} }
/** /**
* Get active groups from store * Get active groups from store
* @returns {Group[]} Group objects
*/ */
get activeGroups() { get activeGroups(): Group[] {
const { groups, choices } = this; const { groups, choices } = this;
return groups.filter(group => { return groups.filter(group => {
@ -135,27 +118,22 @@ export default class Store {
/** /**
* Get loading state from store * Get loading state from store
* @returns {boolean} Loading State
*/ */
isLoading() { isLoading(): boolean {
return this.state.general.loading; return this.state.loading;
} }
/** /**
* Get single choice by it's ID * 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)); return this.activeChoices.find(choice => choice.id === parseInt(id, 10));
} }
/** /**
* Get group by group id * 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); return this.groups.find(group => group.id === id);
} }
} }

View File

@ -3,11 +3,10 @@ import templates from './templates';
import { strToEl } from './lib/utils'; import { strToEl } from './lib/utils';
/** /**
*
* @param {HTMLElement} element1 * @param {HTMLElement} element1
* @param {HTMLElement} element2 * @param {HTMLElement} element2
*/ */
function expectEqualElements(element1, element2) { function expectEqualElements(element1, element2): void {
expect(element1.tagName).to.equal(element2.tagName); expect(element1.tagName).to.equal(element2.tagName);
expect(element1.attributes.length).to.equal(element2.attributes.length); expect(element1.attributes.length).to.equal(element2.attributes.length);
expect(Object.keys(element1.dataset)).to.have.members( expect(Object.keys(element1.dataset)).to.have.members(
@ -516,11 +515,10 @@ describe('templates', () => {
}; };
it('returns expected html', () => { it('returns expected html', () => {
const value = 'test';
const expectedOutput = strToEl( const expectedOutput = strToEl(
`<div class="${classes.list} ${classes.listDropdown}" aria-expanded="false"></div>`, `<div class="${classes.list} ${classes.listDropdown}" aria-expanded="false"></div>`,
); );
const actualOutput = templates.dropdown(classes, value); const actualOutput = templates.dropdown(classes);
expectEqualElements(actualOutput, expectedOutput); 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 * Helpers to create HTML elements used by Choices
* Can be overridden by providing `callbackOnCreateTemplates` option * 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} */ ({ const 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
*/
containerOuter( containerOuter(
{ containerOuter }, { containerOuter }: Pick<ClassNames, 'containerOuter'>,
dir, dir: HTMLElement['dir'],
isSelectElement, isSelectElement: boolean,
isSelectOneElement, isSelectOneElement: boolean,
searchEnabled, searchEnabled: boolean,
passedElementType, passedElementType: PassedElement['type'],
) { ): HTMLDivElement {
const div = Object.assign(document.createElement('div'), { const div = Object.assign(document.createElement('div'), {
className: containerOuter, className: containerOuter,
}); });
@ -53,43 +41,48 @@ export const TEMPLATES = /** @type {Templates} */ ({
return div; return div;
}, },
/** containerInner({
* @param {Partial<ClassNames>} classNames containerInner,
*/ }: Pick<ClassNames, 'containerInner'>): HTMLDivElement {
containerInner({ containerInner }) {
return Object.assign(document.createElement('div'), { return Object.assign(document.createElement('div'), {
className: containerInner, className: containerInner,
}); });
}, },
/** itemList(
* @param {Partial<ClassNames>} classNames {
* @param {boolean} isSelectOneElement list,
*/ listSingle,
itemList({ list, listSingle, listItems }, isSelectOneElement) { listItems,
}: Pick<ClassNames, 'list' | 'listSingle' | 'listItems'>,
isSelectOneElement: boolean,
): HTMLDivElement {
return Object.assign(document.createElement('div'), { return Object.assign(document.createElement('div'), {
className: `${list} ${isSelectOneElement ? listSingle : listItems}`, className: `${list} ${isSelectOneElement ? listSingle : listItems}`,
}); });
}, },
/** placeholder(
* @param {Partial<ClassNames>} classNames { placeholder }: Pick<ClassNames, 'placeholder'>,
* @param {string} value value: string,
*/ ): HTMLDivElement {
placeholder({ placeholder }, value) {
return Object.assign(document.createElement('div'), { return Object.assign(document.createElement('div'), {
className: placeholder, className: placeholder,
innerHTML: value, innerHTML: value,
}); });
}, },
/**
* @param {Partial<ClassNames>} classNames
* @param {Item} item
* @param {boolean} removeItemButton
*/
item( item(
{ item, button, highlightedState, itemSelectable, placeholder }, {
item,
button,
highlightedState,
itemSelectable,
placeholder,
}: Pick<
ClassNames,
'item' | 'button' | 'highlightedState' | 'itemSelectable' | 'placeholder'
>,
{ {
id, id,
value, value,
@ -99,9 +92,9 @@ export const TEMPLATES = /** @type {Templates} */ ({
disabled, disabled,
highlighted, highlighted,
placeholder: isPlaceholder, placeholder: isPlaceholder,
}, }: Item,
removeItemButton, removeItemButton: boolean,
) { ): HTMLDivElement {
const div = Object.assign(document.createElement('div'), { const div = Object.assign(document.createElement('div'), {
className: item, className: item,
innerHTML: label, innerHTML: label,
@ -151,11 +144,10 @@ export const TEMPLATES = /** @type {Templates} */ ({
return div; return div;
}, },
/** choiceList(
* @param {Partial<ClassNames>} classNames { list }: Pick<ClassNames, 'list'>,
* @param {boolean} isSelectOneElement isSelectOneElement: boolean,
*/ ): HTMLDivElement {
choiceList({ list }, isSelectOneElement) {
const div = Object.assign(document.createElement('div'), { const div = Object.assign(document.createElement('div'), {
className: list, className: list,
}); });
@ -168,11 +160,14 @@ export const TEMPLATES = /** @type {Templates} */ ({
return div; return div;
}, },
/** choiceGroup(
* @param {Partial<ClassNames>} classNames {
* @param {Group} group group,
*/ groupHeading,
choiceGroup({ group, groupHeading, itemDisabled }, { id, value, disabled }) { itemDisabled,
}: Pick<ClassNames, 'group' | 'groupHeading' | 'itemDisabled'>,
{ id, value, disabled }: Group,
): HTMLDivElement {
const div = Object.assign(document.createElement('div'), { const div = Object.assign(document.createElement('div'), {
className: `${group} ${disabled ? itemDisabled : ''}`, className: `${group} ${disabled ? itemDisabled : ''}`,
}); });
@ -199,11 +194,6 @@ export const TEMPLATES = /** @type {Templates} */ ({
return div; return div;
}, },
/**
* @param {Partial<ClassNames>} classNames
* @param {Choice} choice
* @param {Options['itemSelectText']} selectText
*/
choice( choice(
{ {
item, item,
@ -212,7 +202,15 @@ export const TEMPLATES = /** @type {Templates} */ ({
selectedState, selectedState,
itemDisabled, itemDisabled,
placeholder, placeholder,
}, }: Pick<
ClassNames,
| 'item'
| 'itemChoice'
| 'itemSelectable'
| 'selectedState'
| 'itemDisabled'
| 'placeholder'
>,
{ {
id, id,
value, value,
@ -222,9 +220,9 @@ export const TEMPLATES = /** @type {Templates} */ ({
disabled: isDisabled, disabled: isDisabled,
selected: isSelected, selected: isSelected,
placeholder: isPlaceholder, placeholder: isPlaceholder,
}, }: Choice,
selectText, selectText: string,
) { ): HTMLDivElement {
const div = Object.assign(document.createElement('div'), { const div = Object.assign(document.createElement('div'), {
id: elementId, id: elementId,
innerHTML: label, innerHTML: label,
@ -239,7 +237,7 @@ export const TEMPLATES = /** @type {Templates} */ ({
div.classList.add(placeholder); div.classList.add(placeholder);
} }
div.setAttribute('role', groupId > 0 ? 'treeitem' : 'option'); div.setAttribute('role', groupId && groupId > 0 ? 'treeitem' : 'option');
Object.assign(div.dataset, { Object.assign(div.dataset, {
choice: '', choice: '',
@ -260,11 +258,10 @@ export const TEMPLATES = /** @type {Templates} */ ({
return div; return div;
}, },
/** input(
* @param {Partial<ClassNames>} classNames { input, inputCloned }: Pick<ClassNames, 'input' | 'inputCloned'>,
* @param {string} placeholderValue placeholderValue: string,
*/ ): HTMLInputElement {
input({ input, inputCloned }, placeholderValue) {
const inp = Object.assign(document.createElement('input'), { const inp = Object.assign(document.createElement('input'), {
type: 'text', type: 'text',
className: `${input} ${inputCloned}`, className: `${input} ${inputCloned}`,
@ -280,10 +277,10 @@ export const TEMPLATES = /** @type {Templates} */ ({
return inp; return inp;
}, },
/** dropdown({
* @param {Partial<ClassNames>} classNames list,
*/ listDropdown,
dropdown({ list, listDropdown }) { }: Pick<ClassNames, 'list' | 'listDropdown'>): HTMLDivElement {
const div = document.createElement('div'); const div = document.createElement('div');
div.classList.add(list, listDropdown); div.classList.add(list, listDropdown);
@ -292,13 +289,16 @@ export const TEMPLATES = /** @type {Templates} */ ({
return div; return div;
}, },
/** notice(
* {
* @param {Partial<ClassNames>} classNames item,
* @param {string} innerHTML itemChoice,
* @param {"no-choices" | "no-results" | ""} type noResults,
*/ noChoices,
notice({ item, itemChoice, noResults, noChoices }, innerHTML, type = '') { }: Pick<ClassNames, 'item' | 'itemChoice' | 'noResults' | 'noChoices'>,
innerHTML: string,
type: 'no-choices' | 'no-results' | '' = '',
): HTMLDivElement {
const classes = [item, itemChoice]; const classes = [item, itemChoice];
if (type === 'no-choices') { if (type === 'no-choices') {
@ -313,19 +313,23 @@ export const TEMPLATES = /** @type {Templates} */ ({
}); });
}, },
/** option({
* @param {Item} option label,
*/ value,
option({ label, value, customProperties, active, disabled }) { customProperties,
active,
disabled,
}: Item): HTMLOptionElement {
const opt = new Option(label, value, false, active); const opt = new Option(label, value, false, active);
if (customProperties) { if (customProperties) {
opt.dataset.customProperties = customProperties; opt.dataset.customProperties = `${customProperties}`;
} }
opt.disabled = disabled;
opt.disabled = !!disabled;
return opt; 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 = { module.exports = {
entry: ['./src/scripts/choices'], entry: ['./src/scripts/choices'],
output: {
library: 'Choices',
libraryTarget: 'window',
libraryExport: 'default',
globalObject: 'window',
},
module: { module: {
rules: [ rules: [
{ {
@ -28,13 +22,28 @@ module.exports = {
}, },
{ {
loader: 'babel-loader', loader: 'babel-loader',
test: /\.js?$/, test: /\.ts?$/,
include, include,
exclude, exclude,
options: { options: {
babelrc: true, babelrc: true,
}, },
}, },
{
loader: 'ts-loader',
test: /\.ts?$/,
include,
exclude
}
], ],
}, },
resolve: {
extensions: [".ts", ".js"]
},
output: {
library: 'Choices',
libraryTarget: 'window',
libraryExport: 'default',
globalObject: 'window',
},
}; };